原文:http://www.jianshu.com/p/c37c46de3168
作者:Rabin_xie
全文约 5900 字,读完可能需要 9 分钟。
Selenium 的 Webdriver 爬取动态网页效果虽然不错,但效率方面并不如人意。最近一直研究如何提高动态页面爬虫的效率,方法无非高并发和分布式两种。过程中有很多收获,也踩了不少坑,在此一并做个总结。以下大致是这段时间的学习路线。
一、 Scrapy+phantomJS
Scrapy 是一个高效的异步爬虫框架,使用比较广泛,文档也很完备,开发人员能快速地实现高性能爬虫。关于 Scrapy 的基本使用这里就不再赘述了, 这篇 Scrapy 读书笔记 挺不错的。然而 Scrapy 在默认的情况下只能获取静态的网页内容,因此必须进一步定制开发。
Scrapy 结合 phantomJS 似乎是个不错的选择。phantomJS 是一个没有页面的浏览器,能渲染动态页面并且相对轻量。因此,我们需要修改 Scrapy 的网页请求模块,让 phantomJS 请求网页,以达到获取动态网页的目的。一番调研之后,发现大致有三种定制方法:
1. 每个 url 请求两次。在回调函数中舍弃掉返回的 response
内容,然后用 phantomJS 再次请求 response.url
,这次的请求由于没有构造 Request
对象,当然就没有回调函数了,然后阻塞等待结果返回即可。这个方法会对同一个 url 请求两次,第一次是 Scrapy 默认的 HTTP 请求,第二次则是 phantomJS 的请求,当然第二次获取到的就是动态网页了。这个方法比较适合快速实现小规模动态爬虫,在默认的 Scrapy 项目基础上,只需要简单修改回调函数就可以了。
2. 自定义下载中间件( downloadMiddleware
)。 downloadMiddleware
对从 scheduler
送来的 Request
对象在请求之前进行预处理,可以实现添加 headers
, user_agent
,还有 cookie
等功能 。但也可以通过中间件直接返回 HtmlResponse
对象,略过请求的模块,直接扔给 response
的回调函数处理。代码如下:
class CustomMetaMiddleware(object):
def process_request(self,request,spider):
dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap["phantomjs.page.settings.loadImages"] = False
dcap["phantomjs.page.settings.resourceTimeout"] = 10
driver = webdriver.PhantomJS("E:xx\xx\xx",desired_capabilities=dcap)
driver.get(request.url)
body = driver.page_source.encode('utf8')
url = driver.current_url
driver.quit()
return HtmlResponse(request.url,body=body)
改完代码后,记得修改 settings 配置。但这个方法有个很大的问题---- 不能实现异步爬取。由于直接在下载中间件中请求网页,而 Scrapy 在这里却不是异步的,只能实现阻塞式的逐个网页下载。当然,如果不追求高并发的话,这也是个快速部署动态爬虫的方法。
3.自定义 downloader
。 downloader
是 Scrapy 发起 HTTP 请求的模块,这模块实现了异步请求,因此自定义 downloader
是最完美的实现。但是要编写一个自定义的 downloader
比较麻烦,必须按照 Twisted 的一些规范,所幸网上有一些开源的 downloader
,在这基础上改改就比较容易了。 这篇文章 详解了 downloader
的开发,非常不错!
一些坑和心得
通过代码运行 Scrapy 是个很有用的方法,即通过 CrawlerProcess
类运行爬虫,但是给 Spider 传递 settings
参数却是一个很大的坑,这个问题绕了我很长时间,最后的解决方法是修改 PYTHONPATH
和 SCRAPY_SETTINGS_MODULE
环境变量,加上爬虫项目的目录,这样 Python 才能找到配置文件。
设置 DOWNLOAD_TIMEOUT
选项,其默认值是 180 秒,相对较长,可以设置得短一些提高效率。
PhantomJS 对多进程的支持极不稳定。具体表现在如果一主机同时开了多个 phantomJS 进程,单个 phantomJS 运行结果就会时好时坏,经常出现一些莫名其妙的报错,官方 git 的 issue 上也提到 phantomJS 对多进程的支持很不好。如果真要多进程爬虫的话,推荐 chromedriver。
Scrapy 的优势在于高效的异步请求框架,由于其本身并不支持动态页面爬取,如果对爬虫的效率没有特别高的要求,也没有必要一定用这个框架,毕竟熟悉框架要一定的时间成本,在框架下编程限制也比较多,对一些比较简单的爬虫,有时还不如自己手撸一个。
二、 Scrapy-splash
由于 phantomJS 的多并发短板,Scrapy+phantomJS 的效率受限,因此,这并不是一个特别好的选择。
又一番调研后,发现 splash 似乎是个不错的选择。Splash 是一个 Javascript 渲染服务。它是用 Python 实现的,同时使用了 Twisted 和 QT,并且实现了 HTTP API的轻量浏览器,Twisted(QT)用来让服务具有异步处理能力,以发挥 webkit 的并发能力。
在 Scrapy 中使用 splash 也很简单,详见http://www.cnblogs.com/zhonghuasong/p/5976003.html 。
一般来说,在 Scrapy 中只需要返回一个 SplashRequest
对象即可。比如:
yield SplashRequest(url='http://'+url,callback=self.parse,endpoint='render.html',
args={'wait':2},errback=self.errback_fun, meta={ })
同样也可以返回带 POST 参数的 Request
对象。更简单地,用 urllib
等库构造 POST 请求也没问题,因为这本质上是一个端口代理,可以接受任何的 HTTP 请求。
splash 的内存占用相对较少,但多并发仍然会出现些问题,请求的失败率会大大提高,页面渲染结果偶尔会出现一些问题,同时受制于服务器主机的带宽,速度受限,但总体表现不错,足以应对小规模的动态爬虫。
Splash 的优点也很显著,通过 HTTP API,其他分布式节点能很容易地获得动态页面,并且使得服务器和其他节点之间的耦合降到了最低,扩展变得特别方便。另外,分布式节点不用配置环境就能获得动态页面,相对 phantomJS 复杂的配置来说简单太多了!如果想简单地实现动态页面爬虫,splash 是一个非常好的选择,但受制于单个服务器带宽,速度有限,并且有时渲染效果不是很理想。
三、 chromedriver 并发
无论 phantomJS 还是 splash,稳定性是一方面,在渲染效果和速度上都不及 chromedriver,毕竟 V8 引擎不是盖的!但 chromedriver 缺点也很明显----特别耗内存,而且是有界面的!
有段时间为了爬百度搜索结果,我一开始用 requests
库模拟 POST 请求,虽然效率没问题,但经常被百度封,于是试着改用 phantomJS,当时觉得尽管效率低了点,但毕竟是真正的浏览器,百度应该不会封。后来发现作用也不大,还是经常被封,并且 phantomJS 自身不太稳定,经常报错,多进程并发更是没办法运行。看来只能试一试 chromedriver 了。以前一直忌惮于内存杀手 chrome(开一个 chrome 浏览器,任务管理器里就有很多个 chrome 进程),最后无奈只能祭出这大杀器了。跑了一段时间之后,发现 chrome 的效率还挺不错,占用的内存也没有想象中的大,多并发支持非常好,在我的电脑上同时开 20 来个也没问题,稳定性也不错,而且百度居然就没封!(震惊!!!chrome 居然自带反反爬虫光环!)。但由于程序主要在阿里云主机上跑,有界面的 chromedriver 当时便没有考虑在内,前不久才知道原来可以通过引入虚拟界面,让 chrome 在没有界面的主机上跑.....
Python 的 pyvirtualdisplay
库就能引入虚拟界面。
代码实现也非常简单:
from pyvirtualdisplay import Display
display = Display(visible=0,size=(800,600))
display.start()
driver = webdriver.Chrome()
driver.get('http://www.baidu.com')
经个人测试,发现 chrome 对多进程的支持非常好,渲染速度快,就是内存占用相对较大,可以多进程+分布式提高效率,关键 chrome 不容易被封。
PS. 常用的 chromedriver 关闭图片选项代码:
chromeOptions = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}
chromeOptions.add_experimental_option("prefs",prefs)
driver = webdriver.Chrome(chromedriver_path,chrome_options=chromeOptions)
driver.get(url)
四、Selenium Grid
Selenium Grid 是 Selenium 的单机扩展,允许用户将测试案例分布在几台机器上并行执行。当然,能实现分布式测试,分布式爬虫当然没问题。
Selenium Grid 的机制如图。首先启动一个中央节点(Hub),然后启动多个远程控制节点(rc),并让 rc 在 Hub 上注册自己的信息,包括 rc 自身的系统、支持的 webdriver、最大并发数量等,这样 Hub 节点就知道了所有的 rc 信息,方便以后调度。
Selenium Grid 机制
运行环境搭建好之后,测试或爬虫脚本请求 Hub 的服务端口,Hub 主机根据注册的 rc 节点的当前状态,结合负载均衡原则,将这些测试用例分发到指定的 rc 节点,rc 节点接到命令之后便执行。
from selenium import webdriver
url = "http://localhost:4444/wd/hub"
driver = webdriver.Remote(command_executor = url, desired_capabilities = {'browserName':'chrome'})
driver.get("http://www.baidu.com")
print driver.title
如下图,我在本地建立了一个 Hub 节点,默认端口是 4444,接着用本机注册了两个 rc 节点,端口分别为 5555、6666。通过 hub 服务端口的控制台可以看到,每个节点可以支持 5 个 Firefox 实例、一个 IE 实例和 5 个 Chrome 实例(可以自定义)。由于本机没有安装 Opera 浏览器,当然也就没有 Opera 实例了。
Selenium Grid 控制台页面
Selenium Grid 是个很好的实现分布式测试/动态爬虫的框架,原理和操作也不复杂,有兴趣的同学可以多了解了解。
五、 总结
以上各软件或框架的特点简要如下:
phantomJS 比较轻量,但对多并发支持非常差
chromedriver 渲染速度快,多并发支持较好,但占用内存大
splash 实现了 HTTP API,分布式扩展容易,页面渲染能力一般
Selenium Grid 是专业的测试框架,扩展容易,支持负载均衡等高级特性
所以,分布式 Scrapy+chromedriver或Selenium Grid是实现分布式动态爬虫较好的选择。
题图:pexels,CC0 授权。
点击阅读原文,查看更多 Python 教程和资源。