作为"数据玩家",如果手头上没有数据怎么办?当然是用代码让程序自动化采集数据,但是现在"爬虫"不是那么容易,其中最困难的即是突破网站各种反爬机制。本系列将全面讲解 .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 最关键的功能就是"等待机制",我们可以用来检测各种条件,让代码无缝执行。