标签:
requests 处理网络请求
logging 日志记录
threading 多线程
Queue 用于线程池的实现
argparse shell参数解析
sqlite3 sqlite数据库
BeautifulSoup html页面解析
urlparse 对链接的处理
我没有选择使用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]
因为以前不了解线程池,线程池的实现在一开始参照了Python Cookbook中关于线程池的例子,参考[2]。
借鉴该例子,一开始我是使用了生产者/消费者的模式,使用任务队列和结果队列,把html源码下载的任务交给任务队列,然后线程池中的线程负责下载,下载完html源码后,放进结果队列,主线程不断从结果队列拿出结果,进行下一步处理。
这样确实可以成功的跑起来,也实现了线程池和任务委派,但却隐藏着一个问题:
做测试时,我指定了新浪爬深度为4的网页, 在爬到第3层时,内存突然爆增,导致程序崩溃。
经过调试发现,正是以上的方法导致的:
多线程并发去下载网页,无论主线程做的是多么不耗时的动作,始终是无法跟上下载的速度的,更何况主线程要负责耗时的文件IO操作,因此,结果队列中的结果没能被及时取出,越存越多却处理不来,导致内存激增。
曾想过用另外的线程池来负责处理结果,可这样该线程池的线程数不好分配,分多了分少了都会有问题,而且程序的实际线程数就多于用户指定的那个线程数了。
因此,干脆让原线程在下载完网页后,不用把结果放进结果队列,而是继续下一步的操作,直到把网页存起来,才结束该线程的任务。
最后就没用到结果队列,一个线程的任务变成:
根据url下载网页—->保存该网页—->抽取该网页的链接(为访问下个深度做准备)—->结束
爬虫的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 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~
这个问题就难以权衡了。
经测试,
爬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]重构:改善既有代码的设计
[6]各种Documentation 以及 随手搜的网页,不一一列举。
标签:
原文地址:http://www.cnblogs.com/timdes/p/5167316.html