原文:http://www.yakergong.net/blog/archives/500
使用scrapy有大概半年了,算是有些经验吧,在这里跟大家讨论一下使用scrapy作为爬虫进行大规模抓取可能遇到的问题。我们抓取的目标是教育网上的网站(目前主要针对.edu.cn和.cas.cn/.cass.cn域名),这半年里抓取了百万以上的url,其实百万url的规模不算大,我们一直在断断续续的修改,还没有开始全面的抓取。
如果想了解scrapy的话,推荐pluskid的scrapy轻松定制网络爬虫,写的很清晰。
关于scrapy涉及的一些不太容易解决的性能问题,请参考使用scrapy进行大规模抓取(二)。
我们对scrapy的修改是基于0.9 stable的(写这篇文章时最新的release是0.10)。我们为了实现定制需求而进行的修改经常破坏了scrapy的代码结构,所以这里就不贴代码了,有兴趣的朋友可以留言或者邮件讨论。
首先说下scrapy的定位吧,我们看来它是为抓取单一网站上固定格式的内容而设计的,比如抓取一个小说网站上的小说或者抓取一个电子商务网站上的商品。好在scrapy结构清晰,可以很方便得修改它来实现更复杂的需求。要进行大规模的抓取,可能要修改scrapy以处理如下问题。
快速的link extractor。python的SGMLParser实在是太慢了,使用SgmlLinkExtractor会让爬虫把大部分的时间都浪费在解析网页上,最好自己写一个link extractor(我们基于lxml写了一个,也可以用soup之类的库)。也可以用正则表达式来写link extractor,速度快,问题是不理解html语义,会把注释里的链接也包含进来。另外基于javascript重定向url也要在这里提取出来。
Spider Trap(?)。我们解决这个问题的方法比较暴力。因为spider trap一般是由动态网页实现的,所以我们最开始的方案就是先通过url是否包含”?”来判断一个网页是否是动态网页,然后取得不包含参数的url地址,对这个地址进行计数,设置一个阈值,超过阈值之后不再抓取。这个方案遇到的困扰在于很多网站开启了url rewrite,使得判别一个页面是否是动态页面很困难。现在的方法是把网页按照所引用的css文件进行聚类,通过控制类里最大能包含的网页数量防止爬虫进入trap后出不来,对不不含css的网页会给一个penalty,限制它能产生的链接数量。这个办法理论上不保证能避免爬虫陷入死循环,但是实际上这个方案工作得挺好,因为绝大多数网页都使用了css,动态网页更是如此。之所以说着方法比较暴力是因为这两种方法阈值都是写死的,不能通过抓取的结果,网页的相似度,网页的重要性进行修改。
增量抓取。一个针对多个网站的爬虫很难一次性把所有网页爬取下来,并且网页也处于不断更新的状态中,爬取是一个动态的过程,爬虫支持增量的抓取是很必要的。大概的流程就是关闭爬虫时保存duplicate filter的数据,保存当前的request队列,爬虫启动时导入duplicate filter,并且用上次request队列的数据作为start url。这里还涉及scrapy一个称得上bug的问题,一旦抓取队列里url过多,关闭scrapy需要很久,有时候要花费几天的时间。我们hack了scrapy的代码,在接收到关闭命令后,保存duplicate filter数据和当前的request队列和已抓取的url列表,然后调用twisted的reactor.stop()强制退出。当前的request队列可以通过scrapy.core.scheduler的pending_requests成员得到。
高效数据存储。抓取的页面多了之后如何存储就成了一个问题,按我们的统计纯html页面的平均大小大概在20~30k之间,百万的页面抓下来之后占用的硬盘空间大概是几十G。ntfs和ext3这些文件系统在小文件过多之后效率都会比较低,需要优化存储效率。我们目前将页面的url进行hash(sha1),以hash值作为文件名,hash值的前几位作为目录名存储页面,以后准备使用HDFS来存储数据。
scrapy的开发者还是挺活跃的,这两天就看到scrapy的blog上有专家写关于ajax抓取的文章,ajax抓取是个挺常出现的问题。目前新版本的scrapy(0.10)支持持久化队列支持增量抓取,爬虫关闭过慢的问题也得到了一定的解决,相信更多特性会逐步开发出来。
我们目前的数据量其实也谈不上大规模,针对这些问题的解决方式也不够完美。这里只是列举一部分可能遇到的问题,实际的问题还有很多,比如编码检测/确定网页的重要性/定期更新。还有一个大问题就是内存泄露,在python里检查内存泄露很难,尤其是对于工作在twisted上的这种持续工作的网络程序,这个以后再谈吧。下面还有一些小建议。
1.如果想要爬取的质量更高,尽量使用宽度优先的策略,在配置里设置 SCHEDULER_ORDER = ‘BFO’
2.修改单爬虫的最大并行请求数 CONCURRENT_REQUESTS_PER_SPIDER
3.修改twisted的线程池大小,默认值是10。参考(Using Threads in Twisted)
在scrapy/core/manage.py爬虫启动前加上
reactor.suggestThreadPoolSize(poolsize)
4.可以开启dns cache来提高性能
在配置里面加上 EXTENSIONS={’scrapy.contrib.resolver.CachingResolver’: 0,}
5.如果自己实现duplicate filter的话注意要保证它是一直可用的,dupfilter里的异常是不会出现在日志文件中的,好像外面做了try-expect处理,我也没仔细看这部分
这是接着之前的(一)写的,上一篇里主要是写了一些解决性能问题的思路。时间过去快半年了,我们抓取的页面也不止百万了。我们在爬虫上也做了一些小改进,比如改善了链接提取器,(一)里提到的四个问题也都有不同程度的改进,但是还是有一些问题迟迟没能解决。
scrapy的问题
爬虫是一个很依赖于网络io的应用,单机的处理能力有限,很快就变成瓶颈。而scrapy并不是一个分布式的设计,在需要大规模爬取的情况下就很成问题。当然可以通过修改Request队列来实现分布式爬取,而且工作量也不算特别大。
scrapy的并行度不高。力图在爬虫里做一些计算性的操作就会影响抓取的速率。这主要是python里的线程机制造成的,因为Python使用了GIL(和Ruby一样),多线程并不会带来太多速度上的提升(除非用Python的C扩展实现自己的模块,这样绕过了GIL)。Summary:Use Python threads if you need to run IO operations in parallel. Do not if you need to run computations in parallel.
scrapy的内存消耗很快。可能是出于性能方面的考虑,pending requests并不是序列化存储在硬盘中,而是放在内存中的(毕竟IO很费时),而且所有Request都放在内存中。你抓取到 百万网页的时候,考虑到单个网页时产生很多链接的,pending request很可能就近千万了,加上脚本语言里的对象本来就有额外成本,再考虑到GC不会立即释放内存,内存占用就相当可观了。这个问题对我们的应用倒也没造成多大困扰,找台二三十G内存的机器就能满足很长时间的需求。
memory应该是脚本语言的硬伤,单机程序来说就更受限了。我记得豆瓣的网站就是用python写的,框架好像用得就是quixote。Davies说内存超过一定量就由master kill掉,再重新fork。持续运行的python程序消耗内存很快,内存泄露也很难调试。
归根到底,这两个问题是根植于语言之中的。
关于如何修改scrapy
前些日子跟几个做爬虫的朋友在五道口交流了一些scrapy的使用经验。谈点建议吧,如果你想修改scrapy适应自己的需求的话,可以先看看文档,能否通过修改配置解决(比如宽度优先/深度优先,限制爬取深度这些都可以通过修改配置解决的)。再contrib和contrib_exp两个目录下有没有实现好的模块,然后再去scrapy snippets看看。如果都不能解决,就去scrapy的google group里问一下,有些实现了的模块并未写在文档中,比如DNS Cache。真的需要自己动手实现了,再查scrapy的在线文档,写的还是挺详细的,关于架构和一些关键的数据结构以及一些常用设置都有说明,有搜索功能(sphinx生成的)。
列举scrapy的google group里经常有人提出的一些需求:
javascript爬取/ajax爬取(跟js的复杂程度正相关,对于复杂的js通用的解决方案应该就是内置js执行引擎)
让爬虫运行在单独线程(see “Run scrapy crawler in a thread“,考虑到Python的多线程机制,这样不太可能有多大的性能提升)
同时运行多个爬虫(新版的scrapy本身就支持这一特性)
防止被网站封禁(不影响人家服务器正常工作是你的义务…自己控制下爬取速率吧)
把Request存储到disk或者database上(pluskid同学讲过)P.S. 推荐
《搜索引擎:原理、技术与系统》
非常好的关于搜索引擎的书,基本原理讲解得全面,也涉及到足够多的细节。相对市面上介绍nutch的书籍而言,它对基本原理的讲解更细节,对自己动手实现一个简单的搜索引擎更 有帮助。比如它会讲如何提取链接,如何对网页消重,甚至讲了一个简单的中文最大正向匹配分词算法。也像douban里的评论里所说,阅读它需要点IR 方面的基础,有些知识它觉得理所当然就一笔带过了。还是有国内教材的作风,重在罗列,不以读者理解为目的。一些不好理解的地方没有做过多阐述。P.S. 我觉得nutch里的爬虫本来就实现得很粗糙。