使用Python + Selenium打造浏览器爬虫

发表于:2018-3-19 08:30

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:aneasystone    来源:aneasystone\\\'s blog

  chrome.proxy 是用于管理 Chrome 浏览器的代理服务器设置的 API,上面的代码通过其提供的方法 chrome.proxy.settings.set() 设置了一个代理服务器地址,mode 的值为 fixed_servers 表示根据下面的 rules 来指定某个固定的代理服务器,代理类型可以是 HTTP 或 HTTPS,还可以是 SOCKS 代理。mode 的值还可以是 direct(无需代理),auto_detect(通过 WPAD 协议自动检测代理),pac_script(通过 PAC 脚本动态选取代理)和 system(使用系统代理)。关于这个 API 的详细说明可以参看 Chrome 的 官方文档,这里有一份 中文翻译。
  通过上面的代码也只是设置了代理服务器的 IP 地址和端口而已,用户名和密码还没有设置,这和使用命令行参数没什么区别。所以还需要下面的第二行代码:
chrome.webRequest.onAuthRequired.addListener(
function (details) {
return {
authCredentials: {
username: "username",
password: "password"
}
};
},
{ urls: ["<all_urls>"] },
[ 'blocking' ]
);
  我们先看看下面这张图,了解下 Chrome 浏览器接受网络请求的整个流程,一个成功的请求会经历一系列的事件(图片来源):
  
  这些事件都是由 chrome.webRequest API 提供,其中的 onAuthRequired 最值得我们注意,它是用于代理身份认证的关键。所有的事件都可以通过 addListener 方法注册一个回调函数作为监听器,当请求需要身份认证时,回调函数返回代理的用户名和密码。除了回调方法,addListener 第二个参数用于指定该代理适用于哪些 url,这里的 <all_urls> 是固定的特殊语法,表示所有的 url,第三个参数字符串 blocking 表示请求将被阻塞,回调函数将以同步的方式执行。这个 API 也可以参考 Chrome 的 官方文档,这里是 中文翻译。
  综上,我们就可以写一个简单的代理插件了,甚至将插件做成动态生成的,然后 Selenium 动态的加载生成的插件。完整的源码在 这里。
  三、Selenium 如何过滤非必要请求?
  Selenium 配合代理,你的爬虫几乎已经无所不能了。上面说过,Selenium 爬虫虽然好用,但有个最大的特点是慢,有时候太慢了也不是办法。由于每次打开一个页面 Selenium 都要等待页面加载完成,包括页面上的图片资源,JS 和 CSS 文件的加载,而且更头疼的是,如果页面上有一些墙外资源,比如来自 Google 或 Facebook 等站点的链接,如果不使用境外代理,浏览器要一直等到这些资源连接超时才算页面加载完成,而这些资源对我们的爬虫没有任何用处。
  我们能不能让 Selenium 过滤掉那些我们不需要的请求呢?
  Yi Zeng 在他的一篇博客 Exclude Selenium WebDriver traffic from Google Analytics 上总结了很多种方法来过滤 Google Analytics 的请求,虽然他的博客是专门针对 Google Analytics 的请求,但其中有很多思路还是很值得我们借鉴的。其中有下面的几种解决方案:
  通过修改 hosts 文件,将 google.com、facebook.com 等重定向到本地,这种方法需要修改系统文件,不方便程序的部署,而且不能动态的添加要过滤的请求;
  禁用浏览器的 JavaScript 功能,譬如 Chrome 支持参数 --disable-javascript 来禁用 JavaScript,但这种方法有很大的局限性,图片和 CSS 资源还是没有过滤掉,而且页面上少了 JavaScript,可能站点的很多功能无法使用了;
  使用浏览器插件,Yi Zeng 的博客中只提到了 Google-Analytics-Opt-out-Add-on 插件用于禁用 Google Analytics,实际上我们很容易想到 AdBlock 插件,这个插件用来过滤页面上的一些广告,这和我们想要的效果有些类似。我们可以自己写一个插件,拦截不需要的请求,相信通过上一节的介绍,也可以做出来。
  使用代理服务器 BrowserMob Proxy,通过代理服务器来拦截不需要的请求,除了 BrowserMob Proxy,还有很多代理软件也具有拦截请求的功能,譬如 Fiddler 的 AutoResponder 或者 通过 whistle 设置 Rules 都可以拦截或修改请求;
  这里虽然方法有很多,但我只推荐最后一种:使用代理服务器 BrowserMob Proxy,BrowserMob Proxy 简称 BMP,可以这么说,BMP 绝对是为 Selenium 为生的,Selenium + BMP 的完美搭配,可以实现很多你绝对想象不出来的功能。
  我之所以推荐 BMP,是由于 BMP 的理念非常巧妙,和传统的代理服务器不一样,它并不是一个简单的代理,而是一个 RESTful 的代理服务,通过 BMP 提供的一套 RESTful 接口,你可以创建或移除代理,设置黑名单或白名单,设置过滤器规则等等,可以说它是一个可编程式的代理服务器。BMP 是使用 Java 语言编写的,它前后经历了两个大版本的迭代,其核心也是从最初的 Jetty 演变为 LittleProxy,使得它更小巧和稳定,你可以从 这里下载 BMP 的可执行文件,在 Windows 系统上,我们直接双击执行 bin 目录下的 browsermob-proxy.bat 文件。
  BMP 启动后,默认在 8080 端口创建代理服务,此时 BMP 还不是一个代理服务器,需要你先创建一个代理:
  curl -X POST http://localhost:8080/proxy
  向 /proxy 接口发送 POST 请求,可以创建一个代理服务器。此时,我们在浏览器访问 http://localhost:8080/proxy 这个地址,可以看到我们已经有了一个代理服务器,端口号为 8081,现在我们就可以使用 127.0.0.1:8081 这个代理了。
  接下来我们要把 Google 的请求拦截掉,BMP 提供了一个 /proxy/[port]/blacklist 接口可以使用,如下:
  curl -X PUT -d 'regex=.*google.*&status=404' http://localhost:8080/proxy/8081/blacklist
  这样所有匹配到 .*google.* 正则的 url,都将直接返回 404 Not Found。
  知道了 BMP 怎么用,再接下来,就是编写代码了。当然我们可以自己写代码来调用 BMP 提供的 RESTful 接口,不过俗话说得好,前人栽树,后人乘凉,早就有人将 BMP 的接口封装好给我们直接使用,譬如 browsermob-proxy-py 是 Python 的实现,我们就来试试它。
from selenium import webdriver
from browsermobproxy import Server
server = Server("D:/browsermob-proxy-2.1.4/bin/browsermob-proxy")
server.start()
proxy = server.create_proxy()
proxy.blacklist(".*google.*", 404)
proxy.blacklist(".*yahoo.*", 404)
proxy.blacklist(".*facebook.*", 404)
proxy.blacklist(".*twitter.*", 404)
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--proxy-server={0}".format(proxy.proxy))
browser = webdriver.Chrome(
executable_path="./drivers/chromedriver.exe",
chrome_options = chrome_options
)
browser.get('http://www.flypeach.com/pc/hk')
server.stop()
browser.quit()
  关键代码在前面几句,首先创建代理,再通过 proxy.blacklist() 将 google、yahoo、facebook、twitter 的资源拦截掉。后面的代码和前一节的代理设置完全一样。执行程序,体会一下,现在这个页面的打开速度快了多少?
  BMP 不仅可以拦截请求,也可以修改请求,这对爬虫来说可能意义不大,但在自动化测试时,可以通过它伪造测试数据还是很有意义的。它提供了两个接口 
/proxy/[port]/filter/request 和 /proxy/[port]/filter/response 用于修改 HTTP 的请求和响应,具体的用法可以参考 官网的文档,此处略过。
proxy.request_interceptor(
'''
request.headers().remove('User-Agent');
request.headers().add('User-Agent', 'My-Custom-User-Agent-String 1.0');
'''
)
proxy.response_interceptor(
'''
if (messageInfo.getOriginalUrl().contains("remote/searchFlights")) {
contents.setTextContents('Hello World');
}
'''
)
  四、Selenium 如何爬取 Ajax 请求?
  到这里,问题变得越来越有意思了。而且我们发现,用 Selenium 做爬虫,中途确实会遇到各种各样的问题,但随着问题的发现到解决,我们花在 Selenium 上面的时间越来越少了,更多的是在研究其他的东西,如浏览器的特性,浏览器插件的编写,可编程式的代理服务器,以此来辅助 Selenium 做的更好。
  还记得前面提到的一个问题吗?如果要爬取的内容在 Ajax 请求的响应中,而在页面上并没有体现,这种情况该如何爬取呢?我们可以直接爬 Ajax 请求吗?事实上,我们很难做到,但不是做不到。
  通过上一节对 BMP 的介绍,我们了解到 BMP 可以拦截并修改请求的报文,我们可以进一步猜想,既然它可以修改报文,那肯定也可以拿到报文,只是这个报文我们的程序该如何得到?上一节我们提到了两个接口 /proxy/[port]/filter/request 和 /proxy/[port]/filter/response,它们可以接受一段 JS 代码来修改 HTTP 的请求和响应,其中我们可以通过 contents.getTextContents() 来访问响应的报文,只是这段代码运行在远程服务器上,和我们的代码在两个完全不同的世界里,如何把它传给我们呢?而且,这段 JS 代码的限制非常严格,我们想通过这个地方拿到这个报文几乎是不可能的。
  但,路总是有的。
  我们回过头来看 BMP 的文档,发现 BMP 提供了两种模式供我们使用:独立模式(Standalone)和 嵌入模式(Embedded Mode)。独立模式就是像上面那样,BMP 作为一个独立的应用服务,我们的程序通过 RESTful 接口与其交互。而嵌入模式则不需要下载 BMP 可执行文件,直接通过包的形式引入到我们的程序中来。可惜的是,嵌入模式只支持 Java 语言,但这也聊胜于无,于是我使用 Java 写了个测试程序尝试了一把。
  首先引入 browsermob-core 包,
  <dependency>
  <groupId>net.lightbody.bmp</groupId>
  <artifactId>browsermob-core</artifactId>
  <version>2.1.5</version>
  </dependency>
  然后参考官网文档写下下面的代码(完整代码见 这里),这里就可以看到嵌入模式的好处了,用于 BMP 拦截处理的代码和我们自己的代码处于同一个环境下,而且 Java 语言具有闭包的特性,我们可以很简单的取到 Ajax 请求的响应报文:
BrowserMobProxyproxyServer=newBrowserMobProxyServer();
proxyServer.start(0);
proxyServer.addRequestFilter((request,contents,messageInfo)->{
System.out.println("请求开始:"+messageInfo.getOriginalUrl());
returnnull;
});
StringajaxContent=null;
proxyServer.addResponseFilter((response,contents,messageInfo)->{
System.out.println("请求结束:"+messageInfo.getOriginalUrl());
if(messageInfo.getOriginalUrl().contains("ajax")){
ajaxContent=contents.getTextContents();
}
});
  如果你是个 .Net guy,可以使用 Fiddler 提供的 FiddlerCore,FiddlerCore 就相当于 BMP 的嵌入模式,和这里的方法类似。这里有一篇很好的文章讲解了如何使用 .Net 和 FiddlerCore 拦截请求。
  既然在 Java 环境下解决了这个问题,那么 Python 应该也没问题,但是 BMP 的嵌入模式并不支持 Python 怎么办呢?于是我一直在寻找一款基于 Python 的能替代 BMP 的工具,可惜一直不如愿,未能找到满意的。到最后,我几乎要下结论:Python + Selenium 很难实现 Ajax 请求的爬取。
  天无绝人之路,直到我遇到了 har。
  有一天我静下心来把 BMP 的文档翻来覆去看了好几遍,之前我看文档的习惯都是用时再查,但这次把 BMP 的文档从头到尾看了几遍,也是希望能从中寻找点蛛丝马迹。而事实上,还真被我发现了点什么。因为 Python 只能通过 RESTful 接口与 BMP 交互,那么每一个接口我都不能放过,有一个接口引起了我的注意:/proxy/[port]/har。
  这个接口虽然之前也扫过几眼,但当时并不知道这个 har 是什么意思,所以都是一掠而过。但那天心血来潮,特意去查了一下 har 的资料,才发现这是一种特殊的 JSON 格式的归档文件。HAR 全称 HTTP Archive Format,通常用于记录浏览器访问网站的所有交互请求,绝大多数浏览器和 Web 代理都支持这种格式的归档文件,用于分析 HTTP 请求,因为广泛的应用,W3C 甚至还提出 HAR 的规范,目前还在草稿阶段。
  /proxy/[port]/har 接口用于创建一份新的 har 文件,Selenium 启动浏览器后所有的请求都将被记录到这份 har 文件中,然后通过 GET 请求,可以获取到这份 har 文件的内容(JSON 格式)。har 文件的内容类似于下面这样:
  {
  "log": {
  "version" : "1.2",
  "creator" : {},
  "browser" : {},
  "pages": [],
  "entries": [],
  "comment": ""
  }
  }
  其中 entries 数组包含了所有 HTTP 请求的列表,默认情况下 BMP 创建的 har 文件并不包含请求的响应内容,我们可以通过 captureContent 参数来让 BMP 记录响应内容:
  curl -X PUT -d 'captureContent=true' http://localhost:8080/proxy/8081/har
  万事俱备,只欠东风。我们开始写代码,首先通过 proxy.new_har() 创建一份 har 文件:
  proxy.new_har(options={
  'captureContent': True
  })
  然后启动浏览器,访问要爬取的页面,等待页面加载结束,这时我们就可以通过 proxy.har 来访问 har 文件中的请求报文了(完整代码在 这里):
  for entry in proxy.har['log']['entries']:
  if 'remote/searchFlights' in entry['request']['url']:
  result = json.loads(entry['response']['content']['text'])
  for key, item in result['data']['flightInfo'].items():
  print(key)
  总结
  这篇博客总结了 Selenium 的一些基础语法,并尝试使用 Python + Selenium 开发浏览器爬虫。本文还分享了我在实际开发过程中遇到的几个常见问题,并提供了一种或多种解决方案,包括代理的使用,拦截浏览器请求,爬取 Ajax 请求等等。实践出真知,通过一系列问题的提出,到研究,到解决,我学习到了非常多的东西。不仅意识到知识广度的重要性,而且更重要的是知识的聚合和熔炼。我一直认为知识的广度比深度更重要,只有你懂的越多,你才有可能接触更多的东西,你的思路才更放得开;深度固然也重要,但往往会让人局限于自己的漩涡之中。但知识的广度不是天马行空,需要不断的总结提炼,融会贯通,形成自己的知识体系,这样才不至于被繁多的知识点所困扰。
  另外,我也意识到阅读项目文档的重要性,心平气和的将项目文档从头到尾阅读一遍,遇到不懂的,就去查找资料,而不是只挑自己知道或感兴趣的,这样会得到意想不到的收获。
  本文所有源码都在我的 GitHub 上,你可以从 这里 查看完整源码。本人能力有限,文中如有错误,欢迎斧正,望不吝赐教。如有好的想法和问题,也欢迎留言评论。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

精彩评论

  • cfgeshen2000
    2018-3-19 13:18:31

    精品文章,值得推荐

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号