python爬虫总结心得_python爬虫总结

标签:

主要涉及的库

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

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]网页内容的编码检测

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

标签:

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

你可能感兴趣的:(python爬虫总结心得)