Python抓取网页的性能问题
抓取网页过程中,软件的性能瓶颈当然是网络连接,这是第一时间可以想到的。此外,通过cProfile.run和pstats.Stats,也可以清楚地看出来。一般来说,可以通过下面几个方法来解决这个问题:
- 通过threading与multiprocessing来解决,例如
#urls 包含所有需要扫描的URL #lists包含每个线程扫描的结果的列表的列表lists = [] threads = [] for i in range(10): temp = [] lists.append(temp) t = threading.Thread(target = check_page, args = ([_ for _ in urls if urls.index(_) % 10 == i], temp)) t.start() threads.append(t) for t in threads: t.join()
- 如果不需要获取完整网页的话,可以在请求的HTTP Header中加入接收Range的头部,只获取一部分网页:
req = urllib2.Request(url, headers = {"Range": "bytes=0-1023"})
不过需要注意的一点是有少部分网站不支持Range这个头部,在这个时候需要去掉这个头部重新请求才行
- 在请求的HTTP Header中加入接收gzip编码内容的头部:
req = urllib2.Request(url, headers = {"Accept-Encoding": "gzip"})
至于如何判断是否返回的真是gzip编码的内容,以及如何解gzip编码的内容,在我另一篇文章里有
- 别忘了在urlopen的时候加上timeout
在使用threading和multiprocessing的过程中,感觉这两者至少在扫描网页时性能相差不大,我分别创建了10个线程和10个进程扫描大约8K+个不同网站的页面,三次对比时间相差少于10%,而multiprocessing占用系统内存较多。
其实除了上述方法外,在Python里还有一些可能更好的提高性能的地方。例如使用greenlet、stackless python等支持更好多线程多进程的工具,还可以使用异步IO,例如twisted或者PycURL。不过个人对greenlet不熟,觉得twisted实在太twisted,PycURL不够pythonic,而stackless python怕破坏了其他python程序,因此仍然使用urllib2 + threading方案。当然,因为GIL的问题,python多线程仍然不够快,可是对单线程情况来说,已经有数倍时间的节省了。
除此之外,在多线程环境下如果需要使用lxml来对网页进行解析的时候,时而会出现占用系统所有内存的问题发生,估计是lxml使用的底层的libxslt或者libxml2的库有重入性的问题导致的,这时候需要将lxml解析html的工作放到主线程来做。当然,这会导致系统的性能下降,不过这是lxml库自己的限制,除非启动真正的多进程,不然即使使用multiprocessing也仍然会出错。
为什么不用BeautifulSoup呢?嗯,这是因为它与它依赖的sgmllib有一些特让人郁闷的bug,还因为它解析HTML比lxml.html慢10倍,这个事就另说了。