Python抓取网页的性能问题

抓取网页过程中,软件的性能瓶颈当然是网络连接,这是第一时间可以想到的。此外,通过cProfile.run和pstats.Stats,也可以清楚地看出来。一般来说,可以通过下面几个方法来解决这个问题:

  1. 通过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()
    复制代码
  2. 如果不需要获取完整网页的话,可以在请求的HTTP Header中加入接收Range的头部,只获取一部分网页:
    req = urllib2.Request(url, headers = {"Range": "bytes=0-1023"})

    不过需要注意的一点是有少部分网站不支持Range这个头部,在这个时候需要去掉这个头部重新请求才行

  3. 在请求的HTTP Header中加入接收gzip编码内容的头部:
    req = urllib2.Request(url, headers = {"Accept-Encoding": "gzip"})

    至于如何判断是否返回的真是gzip编码的内容,以及如何解gzip编码的内容,在我另一篇文章里有

  4. 别忘了在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倍,这个事就另说了。

发表回复