.NET(C#)无头爬虫Selenium系列(02):等待机制

2021-09-01 14:46:16 浏览数 (1)

作为"数据玩家",如果手头上没有数据怎么办?当然是用代码让程序自动化采集数据,但是现在"爬虫"不是那么容易,其中最困难的即是突破网站各种反爬机制。本系列将全面讲解 .NET 中一个非常成熟的库 —— selenium,并教会你如何使用它爬取网络上所需的数据

自动化爬虫虽然方便,但希望大家能顾及网站服务器的承受能力,不要高频率访问网站。并且千万不要采集敏感数据!!否则很容易"从入门到入狱"

本系列大部分案例同时采用 selenium 与 puppeteerSharp 库讲解,并且有 Python 和 C# 2门语言的实现文章,详细请到公众号目录中找到。

前言

上一节入门案例中,我们知道等待机制是一个非常重要的功能,但是上一节中的代码,由于使用等待机制而变得太繁琐。

文章结构如下:

  • 1. 了解等待机制
  • 2. 解决 FindElements 无法等待的问题
  • 3. 打造自己的调用语义(我已经打包成库,在nuget上可以获取)

如果你只想方便使用,可以直接看最后一步关于如何使用即可。

来看看最终调用自己设计的语义调用代码的效果:

  • 左边是上一节案例的实现代码。右边是改造后的
  • 现在的代码语义表达更加简练、稳定(自带等待机制)

机制

想象一下如果是一个机器人帮你从网页上查找某个信息,比较合理的流程是:

  • 让机器人每隔1秒到页面上"按规则"找一下
  • 如果找到,则通知你
  • 如果找不到,下一秒继续
  • 如果超过10秒都找不到,通知你

Selenium 的等待机制同样如此,而上述机制中唯一可以变化的就是"查找规则",这体现为 Wait.Until 的第一个参数接受一个"委托",每隔一段时间,就会执行你的方法。


FindElements 无法等待的原因

这次项目自带 web 服务,启动调试会先启动 web 服务,在浏览器中输入本机 ip 即可浏览本文案例网页,操作看视频:

  • vs 启动调试后,打开浏览器页,输入 "localhost:8081" 出现页面
  • 点击页面上的按钮,下方出现新文本

用"开发者工具",查看元素的标签:

  • 可以看到,新增的内容都是由一个 div 标签包围,他们的共同特征是 class 属性为 "content"

现在用代码对这个页面采集。

导入命名空间

代码语言:javascript复制
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

主要代码如下:

代码语言:javascript复制
private static void FailFindAllContent()
{
    using (var driver = new ChromeDriver())
    {
        var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
        wait.IgnoreExceptionTypes(typeof(WebDriverException));

        driver.Url = @"http://localhost:8081/";
        var all = wait.Until(wd => wd.FindElements(By.ClassName("content")));

        foreach (var item in all)
        {
            Console.WriteLine(item.Text);
        }
        Console.WriteLine("采集完毕!");
    }
}
  • 执行此方法的代码,你会发现啥也没有采集到就直接显示"采集完毕!"
  • 这里的根本问题在于,wd.FindElements 在页面上找不到任何符合条件的元素,但是 wait 对象却没有重复查找
  • 这是因为,wait 对象中的逻辑是,委托中的调用返回 null 或有异常,才被识别为继续等待。但是 FindElements 即使页面没有任何元素,也会返回一个空的集合

知道原因,那么我们很容易就能自己解决这个问题。 定义一个帮助方法:

代码语言:javascript复制
private static Func<IWebDriver, IList<IWebElement>> UntilFindElements(string cssSelector)
{
    IList<IWebElement> UntilFindElements_(IWebDriver driver)
    {
        var res = driver.FindElements(By.CssSelector(cssSelector));
        if (res.Count > 0)
        {
            return res;
        }

        return null;
    }

    return UntilFindElements_;
}
  • C# 现在可以定义嵌套方法,我们在上级方法"UntilFindElements" 下,定义下级方法"UntilFindElements_"
  • 注意下级方法"UntilFindElements_"签名必需是固定的(返回结果是IList ,参数只有一个并且为 IWebDriver)
  • 下级方法"UntilFindElements_"逻辑非常简单,调用 FindElements 并且判断集合个数是否大于0即可。如果没有大于0,则返回 null
  • 上级方法"UntilFindElements"直接返回下级方法"UntilFindElements_"。注意这里并没有调用下级方法,而是直接把下级方法作为结果返回(下级方法名字后面是没有括号的)

调用很简单,原来的代码上,在 wait.Until 里面调用我们的帮助方法:

代码语言:javascript复制
private static void TryFindAllContent()
{
    using (var driver = new ChromeDriver())
    {
        ………………
        var all = wait.Until(UntilFindElements(".content"));
        ………………
    }
}
  • 现在调用此方法,会发现代码被卡住,其实是卡在 wait.Until 中
  • 我们点击页面上的按钮,代码就会继续执行,并显示出结果

打造更加简洁的语义

如果每次使用 Selenium 都要写上这些代码,那真的太麻烦了。不过在 .net 中可以很容易扩展自己的语义。

现在从3个方面简化:

  • 不希望每次都定义 Wait 对象
  • 不希望每次都是先找元素,再操作(点击、输入文本等等)

要做到以上的要求,其实很简单:

  • 自定义一个类型,把 Wait 对象包装在里面
  • 类型中提供4个基本的方法(点击、发送文本、找元素、找所有元素),这些方法自带等待功能,默认使用 css 选择器

我已经简单制作了一个库,nuget安装即可:

代码语言:javascript复制
Install-Package CrystalWind.SeleniumWrapper

使用如下:

代码语言:javascript复制
using CrystalWind.SeleniumWrapper;

private static void GetBaiduSearch()
{
    using (var Wrapper = WrapperBuilder.Create(new ChromeDriver()))
    {
        //进入搜索页面
        Wrapper.Url = @"https://www.baidu.com/";

        Wrapper.WaitForSendKeys("#kw", "爬虫");//在输入框中输入"爬虫"2字
        Wrapper.WaitForClick("#su");//点击按钮
        //找出所有主标题
        var titles = Wrapper.WaitForFindElements("div#content_left h3>a");
        //输出采集结果
        foreach (var item in titles)
        {
            Console.WriteLine(item.Text);
        }
        Console.ReadKey();
    }
}
  • 可以看到,代码基本是与口头表述一一对应
  • 不再需要再写出 wait 对象那段又臭又长的代码
  • 全过程自带等待机制

这个库的源码就在本案例源码中,项目是"CrystalWind.SeleniumWrapper",内容非常简单。


总结

用代码控制 selenium 最关键的功能就是"等待机制",我们可以用来检测各种条件,让代码无缝执行。

0 人点赞