码迷,mamicode.com
首页 > 编程语言 > 详细

python爬虫总结

时间:2016-01-28 21:09:44      阅读:354      评论:0      收藏:0      [点我收藏+]

标签:

主要涉及的库

requests 处理网络请求
logging 日志记录
threading 多线程
Queue 用于线程池的实现
argparse shell参数解析
sqlite3 sqlite数据库
BeautifulSoup html页面解析
urlparse 对链接的处理

关于requests

我没有选择使用python的标准库urllib2,urllib2不易于代码维护,修改起来麻烦,而且不易扩展, 总体来说,requests就是简单易用,如requests的介绍所说: built for human beings.

包括但不限于以下几个原因:

  • 自动处理编码问题

    Requests will automatically decode content from the server. Most unicode charsets are seamlessly decoded.

    web的编码实在是不易处理,尤其是显示中文的情况。参考[1]

  • 自动处理gzip压缩
  • 自动处理转向问题
  • 很简单的支持了自定义cookies,header,timeout功能.
  • requests底层用的是urllib3, 线程安全.
  • 扩展能力很强, 例如要访问登录后的页面, 它也能轻易处理.

关于线程池的实现与任务委派

因为以前不了解线程池,线程池的实现在一开始参照了Python Cookbook中关于线程池的例子,参考[2]。
借鉴该例子,一开始我是使用了生产者/消费者的模式,使用任务队列和结果队列,把html源码下载的任务交给任务队列,然后线程池中的线程负责下载,下载完html源码后,放进结果队列,主线程不断从结果队列拿出结果,进行下一步处理。
这样确实可以成功的跑起来,也实现了线程池和任务委派,但却隐藏着一个问题:
做测试时,我指定了新浪爬深度为4的网页, 在爬到第3层时,内存突然爆增,导致程序崩溃。
经过调试发现,正是以上的方法导致的:
多线程并发去下载网页,无论主线程做的是多么不耗时的动作,始终是无法跟上下载的速度的,更何况主线程要负责耗时的文件IO操作,因此,结果队列中的结果没能被及时取出,越存越多却处理不来,导致内存激增。
曾想过用另外的线程池来负责处理结果,可这样该线程池的线程数不好分配,分多了分少了都会有问题,而且程序的实际线程数就多于用户指定的那个线程数了。
因此,干脆让原线程在下载完网页后,不用把结果放进结果队列,而是继续下一步的操作,直到把网页存起来,才结束该线程的任务。
最后就没用到结果队列,一个线程的任务变成:
根据url下载网页—->保存该网页—->抽取该网页的链接(为访问下个深度做准备)—->结束

关于BFS与深度控制

爬虫的BFS算法不难写,利用队列出栈入栈即可,有一个小难点就是对深度的控制,我一开始是这样做的:
用一个flag来标注每一深度的最后一个链接。当访问到最后一个链接时,深度+1。从而控制爬虫深度。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def getHrefsFromURL(root, depth):
    unvisitedHrefs.append(root)
    currentDepth = 1
    lastURL = root
    flag = False
    while currentDepth < depth+1:
        url = unvisitedHrefs.popleft()
        if lastURL == url:
            flag = True
        #解析html源码,获取其中的链接。并把链接append到unvisitedHrefs去
        getHrefs(url)
        if flag:
            flag = False
            currentDepth += 1
            location = unvisitedHrefs[-1]

但是,这个方法会带来一些问题:

  1. 耗性能: 循环中含有两个不常用的判断
  2. 不适合在多线程中使用

因此,在多线程中,我使用了更直接了当的方法:
先把整个深度的链接分配给线程池线程中的线程去处理(处理的内容参考上文), 等待该深度的所有链接处理完,当所有链接处理完时,则表示爬完爬了一个深度的网页。
此时,下一个深度要访问的链接,已经都准备好了。

1
2
3
4
5
6
7
8
9
10
while self.currentDepth < self.depth+1:
    #分配任务,线程池并发下载当前深度的所有页面(该操作不阻塞)
    self._assignCurrentDepthTasks()
    #等待当前线程池完成所有任务
    #使用self.threadPool.taskQueueJoin()可代替以下操作,可无法Ctrl-C Interupt
    while self.threadPool.getTaskLeft():
        time.sleep(10)
    #当池内的所有任务完成时,即代表爬完了一个网页深度
    #迈进下一个深度
    self.currentDepth += 1

关于耦合性和函数大小

很显然,一开始我这爬虫代码耦合性非常高,线程池,线程,爬虫的操作,三者均粘合在一块无法分开了。 于是我几乎把时间都用在了重构上面。先是把线程池在爬虫中抽出来,再把线程从线程中抽离出来。使得现在三者都可以是相对独立了。
一开始代码里有不少长函数,一个函数里面做着几个操作,于是我决定把操作从函数中抽离,一个函数就必须如它的命名那般清楚,只做那个操作。
于是函数虽然变多了,但每个函数都很简短,使得代码可读性增强,修改起来容易,同时也增加了代码的可复用性。
关于这一点,重构 参考[3] 这本书帮了我很大的忙。

一些其它问题

如何匹配keyword?
一开始使用的方法很简单,把源码和关键词都转为小(大)写,在使用find函数:
pageSource.lower().find(keyword.lower())
要把所有字符转为小写,再查找,我始终觉得这样效率不高。
于是发帖寻求帮助, 有人建议说:
使用if keyword.lower() in pageSource.lower()
确实看过文章说in比find高效,可还没解决我的问题.
于是有人建议使用正则的re.I来查找。
我觉得这是个好方法,直觉告诉我正则查找会比较高效率。
可又有人跳出来说正则比较慢,并拿出了数据。。。
有时间我觉得要做个测试,验证一下。

被禁止访问的问题:
访问未停止时,突然某个host禁止了爬虫访问,这个时候unvisited列表中仍然有大量该host的地址,就会导致大量的超时。 因为每次超时,我都设置了重试,timeout=10s, * 3 = 30s 也就是一个链接要等待30s。
若不重试的话,因为开线程多,网速慢,会导致正常的网页也timeout~
这个问题就难以权衡了。

END

经测试,
爬sina.com.cn 二级深度, 共访问约1350个页面,
开10线程与20线程都需要花费约20分钟的时间,时间相差不多.
随便打开了几个页面,均为100k上下的大小, 假设平均页面大小为100k,
则总共为135000k的数据。
ping sina.com.cn 为联通ip,机房测速为联通133k/s,
则:135000/133/60 约等于17分钟
加上处理数据,文件IO,网页10s超时并重试2次的时间,理论时间也比较接近20分钟了。
因此最大的制约条件应该就是网速了。

看着代码进行了回忆和反思,算是总结了。做之前觉得爬虫很容易,没想到也会遇到不少问题,也学到了很多东西,这样的招人题目比做笔试实在多了。
这次用的是多线程,以后可以再试试异步IO,相信也会是不错的挑战。
附: 爬虫源码

ref:
[1]网页内容的编码检测

[2]simplest useful (I hope!) thread pool example
Python Cookbook

[3]重构:改善既有代码的设计

[4]用Python抓网页的注意事项

[5]用python爬虫抓站的一些技巧总结

[6]各种Documentation 以及 随手搜的网页,不一一列举。

python爬虫总结

标签:

原文地址:http://www.cnblogs.com/timdes/p/5167316.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!