爬虫总结_python
import sqlite3
Python 的一个非常大的优点是很容易写很容易跑起来,缺点就是很多不那么著名的(甚至一些著名的)程序和库都不像 C 和 C++ 那边那样专业、可靠(当然这也有动态类型 vs 静态类型的原因)。
算法设计、人工智能、统计分析、编程语言设计那些应用数学关系紧密的方向。
python写爬虫,没有使用Scrapy吗?item属性定义明确,pipelines来做入库处理,怎么会出现sql错误 ?
做爬虫难点是解析页面和反爬,你竟然还没到这一步就挂了,还能说啥?试试scrapy吧,如果规模大需要调度就上redis。
你用了sqllite做存储,应该是个规模很小的项目?那不如直接存文件。上了规模建议mongodb,pymongo很靠谱!
sql注入爬虫,好像很好玩啊
小黎也发现了对应的变化,于是在 Nginx 上设置了一个频率限制,每分钟超过120次请求的再屏蔽IP。 同时考虑到百度家的爬虫有可能会被误伤,想想市场部门每月几十万的投放,于是写了个脚本,通过 hostname 检查下这个 ip 是不是真的百度家的,对这些 ip 设置一个白名单。
小黎看着新的日志头都大了,再设定规则不小心会误伤真实用户,于是准备换了一个思路,当3个小时的总请求超过50次的时候弹出一个验证码弹框,没有准确正确输入的话就把 IP 记录进黑名单。
图像识别(关键词 PIL,tesseract),再对验证码进行了二值化,分词,模式训练之后,识别了小黎的验证码(关于验证码,验证码的识别,验证码的反识别也是一个恢弘壮丽的斗争史
如果真的对性能要求很高,可以考虑多线程(一些成熟的框架如 scrapy都已支持),甚至分布式
通过chrome的开发者工具分析搜狗页面的请求,发现不是通过ip反爬,而是cookie里的几个关键字段,应对方法就是写个定时任务,维护一个cookie池,定时更新cookie,爬的时候随机取cookie,现在基本不会出现500
所以应对反爬虫,先分析服务器是通过什么来反爬,通过ip就用代理,通过cookie就换cookie,针对性的构建request。
抓取大多数情况属于get请求,即直接从对方服务器上获取数据。
首先,Python中自带urllib及urllib2这两个模块,基本上能满足一般的页面抓取。另外,requests也是非常有用的包,与此类似的,还有httplib2等等。
Requests:
import requests
response = requests.get(url)
content = requests.get(url).content
print "response headers:", response.headers
print "content:", content
Urllib2:
import urllib2
response = urllib2.urlopen(url)
content = urllib2.urlopen(url).read()
print "response headers:", response.headers
print "content:", content
Httplib2:
import httplib2
http = httplib2.Http()
response_headers, content = http.request(url, 'GET')
print "response headers:", response_headers
print "content:", content
此外,对于带有查询字段的url,get请求一般会将来请求的数据附在url之后,以?分割url和传输数据,多个参数用&连接。
data = {'data1':'XXXXX', 'data2':'XXXXX'}
Requests:data为dict,json
import requests
response = requests.get(url=url, params=data)
Urllib2:data为string
import urllib, urllib2
data = urllib.urlencode(data)
full_url = url+'?'+data
response = urllib2.urlopen(full_url)
import time
time.sleep(1)
不多说。
def multi_session(session, *arg):
while True:
retryTimes = 20
while retryTimes>0:
try:
return session.post(*arg)
except:
print '.',
retryTimes -= 1
或者
def multi_open(opener, *arg):
while True:
retryTimes = 20
while retryTimes>0:
try:
return opener.open(*arg)
except:
print '.',
retryTimes -= 1
这样我们就可以使用multi_session或multi_open对爬虫抓取的session或opener进行保持。
华尔街新闻: http://wallstreetcn.com/news https://github.com/lining0806/Spider_Python https://github.com/lining0806/Spider
主要涉及的库
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) #当池内的所有任务完成时,即代表爬完了一个网页深度 #迈进下一个深度 |
关于耦合性和函数大小
很显然,一开始我这爬虫代码耦合性非常高,线程池,线程,爬虫的操作,三者均粘合在一块无法分开了。 于是我几乎把时间都用在了重构上面。先是把线程池在爬虫中抽出来,再把线程从线程中抽离出来。使得现在三者都可以是相对独立了。
一开始代码里有不少长函数,一个函数里面做着几个操作,于是我决定把操作从函数中抽离,一个函数就必须如它的命名那般清楚,只做那个操作。
于是函数虽然变多了,但每个函数都很简短,使得代码可读性增强,修改起来容易,同时也增加了代码的可复用性。
关于这一点,重构 参考[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]重构:改善既有代码的设计
[6]各种Documentation 以及 随手搜的网页,不一一列举。
5.验证码的处理
碰到验证码咋办?这里分两种情况处理:
- google那种验证码,凉拌
- 简单的验证码:字符个数有限,只使用了简单的平移或旋转加噪音而没有扭曲的,这种还是有可能可以处理的,一般思路是旋转的转回来,噪音去掉,然 后划分单个字符,划分好了以后再通过特征提取的方法(例如PCA)降维并生成特征库,然后把验证码和特征库进行比较。这个比较复杂,一篇博文是说不完的, 这里就不展开了,具体做法请弄本相关教科书好好研究一下。
- 事实上有些验证码还是很弱的,这里就不点名了,反正我通过2的方法提取过准确度非常高的验证码,所以2事实上是可行的。
6 gzip/deflate支持
现在的网页普遍支持gzip压缩,这往往可以解决大量传输时间,以VeryCD的主页为例,未压缩版本247K,压缩了以后45K,为原来的1/5。这就意味着抓取速度会快5倍。
然而python的urllib/urllib2默认都不支持压缩,要返回压缩格式,必须在request的header里面写明’accept- encoding’,然后读取response后更要检查header查看是否有’content-encoding’一项来判断是否需要解码,很繁琐琐 碎。如何让urllib2自动支持gzip, defalte呢?
其实可以继承BaseHanlder类,然后build_opener的方式来处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import urllib2 from gzip import GzipFile from StringIO import StringIO class ContentEncodingProcessor(urllib2.BaseHandler): """A handler to add gzip capabilities to urllib2 requests """ # add headers to requests def http_request( self , req): req.add_header( "Accept-Encoding" , "gzip, deflate" ) return req # decode def http_response( self , req, resp): old_resp = resp # gzip if resp.headers.get( "content-encoding" ) = = "gzip" : gz = GzipFile( fileobj = StringIO(resp.read()), mode = "r" ) resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) resp.msg = old_resp.msg # deflate if resp.headers.get( "content-encoding" ) = = "deflate" : gz = StringIO( deflate(resp.read()) ) resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) # 'class to add info() and resp.msg = old_resp.msg return resp # deflate support import zlib def deflate(data): # zlib only provides the zlib compress format, not the deflate format; try : # so on top of all there's this workaround: return zlib.decompress(data, - zlib.MAX_WBITS) except zlib.error: return zlib.decompress(data) |
然后就简单了,
encoding_support = ContentEncodingProcessor #直接用opener打开网页,如果服务器支持gzip/defalte则自动解压缩 content = opener.open(url).read() opener = urllib2.build_opener( encoding_support, urllib2.HTTPHandler )
7. 更方便地多线程
总结一文的确提及了一个简单的多线程模板,但是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想想怎么进行多线程调用最方便呢?
1、用twisted进行异步I/O抓取
事实上更高效的抓取并非一定要用多线程,也可以使用异步I/O法:直接用twisted的getPage方法,然后分别加上异步I/O结束时的callback和errback方法即可。例如可以这么干:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from twisted.web.client import getPage from twisted.internet import reactor links = [ 'http://www.verycd.com/topics/%d/' % i for i in range ( 5420 , 5430 ) ] def parse_page(data,url): print len (data),url def fetch_error(error,url): print error.getErrorMessage(),url # 批量抓取链接 for url in links: getPage(url,timeout = 5 ) \ .addCallback(parse_page,url) \ #成功则调用parse_page方法 .addErrback(fetch_error,url) #失败则调用fetch_error方法 reactor.callLater( 5 , reactor.stop) #5秒钟后通知reactor结束程序 reactor.run() |
twisted人如其名,写的代码实在是太扭曲了,非正常人所能接受,虽然这个简单的例子看上去还好;每次写twisted的程序整个人都扭曲了,累得不得了,文档等于没有,必须得看源码才知道怎么整,唉不提了。
如果要支持gzip/deflate,甚至做一些登陆的扩展,就得为twisted写个新的HTTPClientFactory类诸如此类,我这眉头真是大皱,遂放弃。有毅力者请自行尝试。
这篇讲怎么用twisted来进行批量网址处理的文章不错,由浅入深,深入浅出,可以一看。
2、设计一个简单的多线程抓取类
还是觉得在urllib之类python“本土”的东东里面折腾起来更舒服。试想一下,如果有个Fetcher类,你可以这么调用
1
2
3
4
5
6
|
f = Fetcher(threads = 10 ) #设定下载线程数为10 for url in urls: f.push(url) #把所有url推入下载队列 while f.taskleft(): #若还有未完成下载的线程 content = f.pop() #从下载完成队列中取出结果 do_with(content) # 处理content内容 |
这 么个多线程调用简单明了,那么就这么设计吧,首先要有两个队列,用Queue搞定,多线程的基本架构也和“技巧总结”一文类似,push方法和pop方法 都比较好处理,都是直接用Queue的方法,taskleft则是如果有“正在运行的任务”或者”队列中的任务”则为是,也好办,于是代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
import urllib2 from threading import Thread,Lock from Queue import Queue import time class Fetcher: def __init__( self ,threads): self .opener = urllib2.build_opener(urllib2.HTTPHandler) self .lock = Lock() #线程锁 self .q_req = Queue() #任务队列 self .q_ans = Queue() #完成队列 self .threads = threads for i in range (threads): t = Thread(target = self .threadget) t.setDaemon( True ) t.start() self .running = 0 def __del__( self ): #解构时需等待两个队列完成 time.sleep( 0.5 ) self .q_req.join() self .q_ans.join() def taskleft( self ): return self .q_req.qsize() + self .q_ans.qsize() + self .running def push( self ,req): self .q_req.put(req) def pop( self ): return self .q_ans.get() def threadget( self ): while True : req = self .q_req.get() with self .lock: #要保证该操作的原子性,进入critical area self .running + = 1 try : ans = self .opener. open (req).read() except Exception, what: ans = '' print what self .q_ans.put((req,ans)) with self .lock: self .running - = 1 self .q_req.task_done() time.sleep( 0.1 ) # don't spam if __name__ = = "__main__" : links = [ 'http://www.verycd.com/topics/%d/' % i for i in range ( 5420 , 5430 ) ] f = Fetcher(threads = 10 ) for url in links: f.push(url) while f.taskleft(): url,content = f.pop() print url, len (content) |
8. 一些琐碎的经验
1、连接池:
opener.open和urllib2.urlopen一样,都会新建一个http请求。通常情况下这不是什么问题,因为线性环境下,一秒钟可能 也就新生成一个请求;然而在多线程环境下,每秒钟可以是几十上百个请求,这么干只要几分钟,正常的有理智的服务器一定会封禁你的。
然而在正常的html请求时,保持同时和服务器几十个连接又是很正常的一件事,所以完全可以手动维护一个HttpConnection的池,然后每次抓取时从连接池里面选连接进行连接即可。
这里有一个取巧的方法,就是利用squid做代理服务器来进行抓取,则squid会自动为你维护连接池,还附带数据缓存功能,而且squid本来就是我每个服务器上面必装的东东,何必再自找麻烦写连接池呢。
2、设定线程的栈大小
栈大小的设定将非常显著地影响python的内存占用,python多线程不设置这个值会导致程序占用大量内存,这对openvz的vps来说非常致命。stack_size必须大于32768,实际上应该总要32768*2以上
from threading import stack_size stack_size(32768*16)
3、设置失败后自动重试
def get(self,req,retries=3): try: response = self.opener.open(req) data = response.read() except Exception , what: print what,req if retries>0: return self.get(req,retries-1) else: print 'GET Failed',req return '' return data
4、设置超时
import socket socket.setdefaulttimeout(10) #设置10秒后连接超时
5、登陆
登陆更加简化了,首先build_opener中要加入cookie支持,参考“总结”一文;如要登陆VeryCD,给Fetcher新增一个空方法login,并在init()中调用,然后继承Fetcher类并override login方法:
1
2
3
4
5
6
7
8
9
|
def login( self ,username,password): import urllib data = urllib.urlencode({ 'username' :username, 'password' :password, 'continue' : 'http://www.verycd.com/' , 'login_submit' :u '登录' .encode( 'utf-8' ), 'save_cookie' : 1 ,}) url = 'http://www.verycd.com/signin' self .opener. open (url,data).read() |
于是在Fetcher初始化时便会自动登录VeryCD网站。
9. 总结
如此,把上述所有小技巧都糅合起来就和我目前的私藏最终版的Fetcher类相差不远了,它支持多线程,gzip/deflate压缩,超时设置,自动重试,设置栈大小,自动登录等功能;代码简单,使用方便,性能也不俗,可谓居家旅行,杀人放火,咳咳,之必备工具。
之所以说和最终版差得不远,是因为最终版还有一个保留功能“马甲术”:多代理自动选择。看起来好像仅仅是一个random.choice的区别,其实包含了代理获取,代理验证,代理测速等诸多环节,这就是另一个故事了。
5.验证码的处理
碰到验证码咋办?这里分两种情况处理:
- google那种验证码,凉拌
- 简单的验证码:字符个数有限,只使用了简单的平移或旋转加噪音而没有扭曲的,这种还是有可能可以处理的,一般思路是旋转的转回来,噪音去掉,然 后划分单个字符,划分好了以后再通过特征提取的方法(例如PCA)降维并生成特征库,然后把验证码和特征库进行比较。这个比较复杂,一篇博文是说不完的, 这里就不展开了,具体做法请弄本相关教科书好好研究一下。
- 事实上有些验证码还是很弱的,这里就不点名了,反正我通过2的方法提取过准确度非常高的验证码,所以2事实上是可行的。
6 gzip/deflate支持
现在的网页普遍支持gzip压缩,这往往可以解决大量传输时间,以VeryCD的主页为例,未压缩版本247K,压缩了以后45K,为原来的1/5。这就意味着抓取速度会快5倍。
然而python的urllib/urllib2默认都不支持压缩,要返回压缩格式,必须在request的header里面写明’accept- encoding’,然后读取response后更要检查header查看是否有’content-encoding’一项来判断是否需要解码,很繁琐琐 碎。如何让urllib2自动支持gzip, defalte呢?
其实可以继承BaseHanlder类,然后build_opener的方式来处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import urllib2 from gzip import GzipFile from StringIO import StringIO class ContentEncodingProcessor(urllib2.BaseHandler): """A handler to add gzip capabilities to urllib2 requests """ # add headers to requests def http_request( self , req): req.add_header( "Accept-Encoding" , "gzip, deflate" ) return req # decode def http_response( self , req, resp): old_resp = resp # gzip if resp.headers.get( "content-encoding" ) = = "gzip" : gz = GzipFile( fileobj = StringIO(resp.read()), mode = "r" ) resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) resp.msg = old_resp.msg # deflate if resp.headers.get( "content-encoding" ) = = "deflate" : gz = StringIO( deflate(resp.read()) ) resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) # 'class to add info() and resp.msg = old_resp.msg return resp # deflate support import zlib def deflate(data): # zlib only provides the zlib compress format, not the deflate format; try : # so on top of all there's this workaround: return zlib.decompress(data, - zlib.MAX_WBITS) except zlib.error: return zlib.decompress(data) |
然后就简单了,
encoding_support = ContentEncodingProcessor #直接用opener打开网页,如果服务器支持gzip/defalte则自动解压缩 content = opener.open(url).read() opener = urllib2.build_opener( encoding_support, urllib2.HTTPHandler )
7. 更方便地多线程
总结一文的确提及了一个简单的多线程模板,但是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想想怎么进行多线程调用最方便呢?
1、用twisted进行异步I/O抓取
事实上更高效的抓取并非一定要用多线程,也可以使用异步I/O法:直接用twisted的getPage方法,然后分别加上异步I/O结束时的callback和errback方法即可。例如可以这么干:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from twisted.web.client import getPage from twisted.internet import reactor links = [ 'http://www.verycd.com/topics/%d/' % i for i in range ( 5420 , 5430 ) ] def parse_page(data,url): print len (data),url def fetch_error(error,url): print error.getErrorMessage(),url # 批量抓取链接 for url in links: getPage(url,timeout = 5 ) \ .addCallback(parse_page,url) \ #成功则调用parse_page方法 .addErrback(fetch_error,url) #失败则调用fetch_error方法 reactor.callLater( 5 , reactor.stop) #5秒钟后通知reactor结束程序 reactor.run() |
twisted人如其名,写的代码实在是太扭曲了,非正常人所能接受,虽然这个简单的例子看上去还好;每次写twisted的程序整个人都扭曲了,累得不得了,文档等于没有,必须得看源码才知道怎么整,唉不提了。
如果要支持gzip/deflate,甚至做一些登陆的扩展,就得为twisted写个新的HTTPClientFactory类诸如此类,我这眉头真是大皱,遂放弃。有毅力者请自行尝试。
这篇讲怎么用twisted来进行批量网址处理的文章不错,由浅入深,深入浅出,可以一看。
2、设计一个简单的多线程抓取类
还是觉得在urllib之类python“本土”的东东里面折腾起来更舒服。试想一下,如果有个Fetcher类,你可以这么调用
1
2
3
4
5
6
|
f = Fetcher(threads = 10 ) #设定下载线程数为10 for url in urls: f.push(url) #把所有url推入下载队列 while f.taskleft(): #若还有未完成下载的线程 content = f.pop() #从下载完成队列中取出结果 do_with(content) # 处理content内容 |
这 么个多线程调用简单明了,那么就这么设计吧,首先要有两个队列,用Queue搞定,多线程的基本架构也和“技巧总结”一文类似,push方法和pop方法 都比较好处理,都是直接用Queue的方法,taskleft则是如果有“正在运行的任务”或者”队列中的任务”则为是,也好办,于是代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
import urllib2 from threading import Thread,Lock from Queue import Queue import time class Fetcher: def __init__( self ,threads): self .opener = urllib2.build_opener(urllib2.HTTPHandler) self .lock = Lock() #线程锁 self .q_req = Queue() #任务队列 self .q_ans = Queue() #完成队列 self .threads = threads for i in range (threads): t = Thread(target = self .threadget) t.setDaemon( True ) t.start() self .running = 0 def __del__( self ): #解构时需等待两个队列完成 time.sleep( 0.5 ) self .q_req.join() self .q_ans.join() def taskleft( self ): return self .q_req.qsize() + self .q_ans.qsize() + self .running def push( self ,req): self .q_req.put(req) def pop( self ): return self .q_ans.get() def threadget( self ): while True : req = self .q_req.get() with self .lock: #要保证该操作的原子性,进入critical area self .running + = 1 try : ans = self .opener. open (req).read() except Exception, what: ans = '' print what self .q_ans.put((req,ans)) with self .lock: self .running - = 1 self .q_req.task_done() time.sleep( 0.1 ) # don't spam if __name__ = = "__main__" : links = [ 'http://www.verycd.com/topics/%d/' % i for i in range ( 5420 , 5430 ) ] f = Fetcher(threads = 10 ) for url in links: f.push(url) while f.taskleft(): url,content = f.pop() print url, len (content) |
8. 一些琐碎的经验
1、连接池:
opener.open和urllib2.urlopen一样,都会新建一个http请求。通常情况下这不是什么问题,因为线性环境下,一秒钟可能 也就新生成一个请求;然而在多线程环境下,每秒钟可以是几十上百个请求,这么干只要几分钟,正常的有理智的服务器一定会封禁你的。
然而在正常的html请求时,保持同时和服务器几十个连接又是很正常的一件事,所以完全可以手动维护一个HttpConnection的池,然后每次抓取时从连接池里面选连接进行连接即可。
这里有一个取巧的方法,就是利用squid做代理服务器来进行抓取,则squid会自动为你维护连接池,还附带数据缓存功能,而且squid本来就是我每个服务器上面必装的东东,何必再自找麻烦写连接池呢。
2、设定线程的栈大小
栈大小的设定将非常显著地影响python的内存占用,python多线程不设置这个值会导致程序占用大量内存,这对openvz的vps来说非常致命。stack_size必须大于32768,实际上应该总要32768*2以上
from threading import stack_size stack_size(32768*16)
3、设置失败后自动重试
def get(self,req,retries=3): try: response = self.opener.open(req) data = response.read() except Exception , what: print what,req if retries>0: return self.get(req,retries-1) else: print 'GET Failed',req return '' return data
4、设置超时
import socket socket.setdefaulttimeout(10) #设置10秒后连接超时
5、登陆
登陆更加简化了,首先build_opener中要加入cookie支持,参考“总结”一文;如要登陆VeryCD,给Fetcher新增一个空方法login,并在init()中调用,然后继承Fetcher类并override login方法:
1
2
3
4
5
6
7
8
9
|
def login( self ,username,password): import urllib data = urllib.urlencode({ 'username' :username, 'password' :password, 'continue' : 'http://www.verycd.com/' , 'login_submit' :u '登录' .encode( 'utf-8' ), 'save_cookie' : 1 ,}) url = 'http://www.verycd.com/signin' self .opener. open (url,data).read() |
于是在Fetcher初始化时便会自动登录VeryCD网站。
9. 总结
如此,把上述所有小技巧都糅合起来就和我目前的私藏最终版的Fetcher类相差不远了,它支持多线程,gzip/deflate压缩,超时设置,自动重试,设置栈大小,自动登录等功能;代码简单,使用方便,性能也不俗,可谓居家旅行,杀人放火,咳咳,之必备工具。
之所以说和最终版差得不远,是因为最终版还有一个保留功能“马甲术”:多代理自动选择。看起来好像仅仅是一个random.choice的区别,其实包含了代理获取,代理验证,代理测速等诸多环节,这就是另一个故事了。
编译过程中要下载,没编译成功。
svn checkout http://py-leveldb.googlecode.com/svn/trunk/
svn co http://py-leveldb.googlecode.com/svn/trunk/ py-leveldb
重新找到安装包:
wget https://pypi.python.org/packages/source/l/leveldb/leveldb-0.193.tar.gz
tar -zxvf leveldb-0.193.tar.gz
cd leveldb-0.193
python setup.py build
sudo python setup.py install
安装成功
这个爬虫是从outofmemory看到的,只有100行,内容是抓取淘宝商品信息,包括商品名、卖家id、地区、价格等信息,json格式,作者说他曾经抓取到了一千万条信息。
出于对这个爬虫能力的感叹,我好奇的对它进行了分析,发现原理是如此的简单,感叹python
的强大之余,好也把分析的心得记录一下,引为后来的经验。
现在这个爬虫能不能用就没有保证了,不过没有关系,只是作为一个学习的例子。
#-*- coding: UTF-8 -*- import time import leveldb from urllib.parse import quote_plus import re import json import itertools import sys import requests from queue import Queue from threading import Thread URL_BASE = 'http://s.m.taobao.com/search?q={}&n=200&m=api4h5&style=list&page={}' def url_get(url): # print('GET ' + url) header = dict() header['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' header['Accept-Encoding'] = 'gzip,deflate,sdch' header['Accept-Language'] = 'en-US,en;q=0.8' header['Connection'] = 'keep-alive' header['DNT'] = '1' #header['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36' header['User-Agent'] = 'Mozilla/12.0 (compatible; MSIE 8.0; Windows NT)' return requests.get(url, timeout = 5, headers = header).text def item_thread(cate_queue, db_cate, db_item): while True: try: cate = cate_queue.get() post_exist = True try: state = db_cate.Get(cate.encode('utf-8')) if state != b'OK': post_exist = False except: post_exist = False if post_exist == True: print('cate-{}: {} already exists ... Ignore'.format(cate, title)) continue db_cate.Put(cate.encode('utf-8'), b'crawling') for item_page in itertools.count(1): url = URL_BASE.format(quote_plus(cate), item_page) for tr in range(5): try: items_obj = json.loads(url_get(url)) break except KeyboardInterrupt: quit() except Exception as e: if tr == 4: raise e if len(items_obj['listItem']) == 0: break for item in items_obj['listItem']: item_obj = dict( _id = int(item['itemNumId']), name = item['name'], price = float(item['price']), query = cate, category = int(item['category']) if item['category'] != '' else 0, nick = item['nick'], area = item['area']) db_item.Put(str(item_obj['_id']).encode('utf-8'), json.dumps(item_obj, ensure_ascii = False).encode('utf-8')) print('Get {} items from {}: {}'.format(len(items_obj['listItem']), cate, item_page)) if 'nav' in items_obj: for na in items_obj['nav']['navCatList']: try: db_cate.Get(na['name'].encode('utf-8')) except: db_cate.Put(na['name'].encode('utf-8'), b'waiting') db_cate.Put(cate.encode('utf-8'), b'OK') print(cate, 'OK') except KeyboardInterrupt: break except Exception as e: print('An {} exception occured'.format(e)) def cate_thread(cate_queue, db_cate): while True: try: for key, value in db_cate.RangeIter(): if value != b'OK': print('CateThread: put {} into queue'.format(key.decode('utf-8'))) cate_queue.put(key.decode('utf-8')) time.sleep(10) except KeyboardInterrupt: break except Exception as e: print('CateThread: {}'.format(e)) if __name__ == '__main__': db_cate = leveldb.LevelDB('./taobao-cate') db_item = leveldb.LevelDB('./taobao-item') orig_cate = '正装'.encode('gb2312') try: db_cate.Get(orig_cate.encode('utf-8')) except: db_cate.Put(orig_cate.encode('utf-8'), b'waiting') cate_queue = Queue(maxsize = 1000) cate_th = Thread(target = cate_thread, args = (cate_queue, db_cate)) cate_th.start() item_th = [Thread(target = item_thread, args = (cate_queue, db_cate, db_item)) for _ in range(5)] for item_t in item_th: item_t.start() cate_th.join()
分析
一个只有一百行的代码,也不用花太多心思就可以看懂了,不过其中一些有意思的心得还是可以分享下。
- 使用
vim
打开,在使用了fold
功能后,可以清晰的看到代码由import
部分,三个自定义函数和一个main
了,所以可以直接从main
开始看。 main
建立(也可以是已经建立的)了两个数据库,分别是db_cate
和db_item
,还定义了开始时抓取的商品种类(category)orig_cate
。- 先在
db_cate
中尝试访问下orig_cate
,如果没有这个种类,就加入这个种类,属性设置为waiting
,leveldb
就是一个key-value数据库,使用起来非常的方便。 - 建立一个种类的队列
cate_queue
,然后就建立一个种类的线程cate_th
,会调用自己定义的一个函数cate_thread
,参数是队列和种类数据库。 - 再建立5个线程
item_th
,调用定义的item_thread
函数参数是队列和两个数据库。 - 最后会等待线程终止后退出。
- 这时就可以看前面的定义的函数
cate_thread
,这个函数会重复从种类数据库cate_db
中遍历取出种类名,然后看这个种类是不是已经抓取过了,如果没有,就加入到种类队列cate_queue
。 - 再看函数
item_thead
,从种类队列cate_queue
中取出一个种类,再从种类数据库中查看其状态,如果是ok
,就取下一个种类;如果不是ok
,就标记为crawling
,然后就使用这个类别和一个遍历的序号就可以获得一个网址,然后就重复的尝试获取这个页面的数据,再分析,保存到item_db
中,再把种类在cate_db
中标记为ok
,也就是完成,同时,把页面有的种类信息放到cate_db
数据库中。 - 这样这个爬虫就可以一直工作了。
总结
这个爬虫的结构很清晰,一个数据库用来保存种类的状态信息,一个数据库保存获取到的信息,一个队列作为进程间通信的工具,数据库使用key-value
,网页抓取使用requests
。参考这个结构,很多爬虫都可以写出来了。
爬虫总结_python的更多相关文章
- Python爬虫视频教程
├─第1章_[第0周]网络爬虫之前奏 │ ├─第1节_"网络爬虫"课程内容导学 │ │ 第1部分_全课程内容导学.mp4 │ │ 第2部分_全课程内容导学(WS00单元)学习资料. ...
- Python BeautifulSoup4 爬虫基础、多线程学习
针对 崔庆才老师 的 https://ssr1.scrape.center 的爬虫基础练习.Threading多线程库.Time库.json库.BeautifulSoup4 爬虫库.py基本语法
- 设计爬虫Hawk背后的故事
本文写于圣诞节北京下午慵懒的午后.本文偏技术向,不过应该大部分人能看懂. 五年之痒 2016年,能记入个人年终总结的事情没几件,其中一个便是开源了Hawk.我花不少时间优化和推广它,得到的评价还算比较 ...
- Scrapy框架爬虫初探——中关村在线手机参数数据爬取
关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...
- Python 爬虫模拟登陆知乎
在之前写过一篇使用python爬虫爬取电影天堂资源的博客,重点是如何解析页面和提高爬虫的效率.由于电影天堂上的资源获取权限是所有人都一样的,所以不需要进行登录验证操作,写完那篇文章后又花了些时间研究了 ...
- scrapy爬虫docker部署
spider_docker 接我上篇博客,为爬虫引用创建container,包括的模块:scrapy, mongo, celery, rabbitmq,连接https://github.com/Liu ...
- scrapy 知乎用户信息爬虫
zhihu_spider 此项目的功能是爬取知乎用户信息以及人际拓扑关系,爬虫框架使用scrapy,数据存储使用mongo,下载这些数据感觉也没什么用,就当为大家学习scrapy提供一个例子吧.代码地 ...
- 120项改进:开源超级爬虫Hawk 2.0 重磅发布!
沙漠君在历时半年,修改无数bug,更新一票新功能后,在今天隆重推出最新改进的超级爬虫Hawk 2.0! 啥?你不知道Hawk干吗用的? 这是采集数据的挖掘机,网络猎杀的重狙!半年多以前,沙漠君写了一篇 ...
- Python爬虫小白入门(四)PhatomJS+Selenium第一篇
一.前言 在上一篇博文中,我们的爬虫面临着一个问题,在爬取Unsplash网站的时候,由于网站是下拉刷新,并没有分页.所以不能够通过页码获取页面的url来分别发送网络请求.我也尝试了其他方式,比如下拉 ...
随机推荐
- 利用xshell密钥管理服务器远程登录+VIM dd命令操作之伤之再伤
1.打开Xshell界面,中文界面方便操作,菜单栏:工具——新建用户密钥生成向导 2.密钥类型选择RSA,密钥长度选择2048位,单击下一步继续: 3.很快生成公钥对,单击下一步继续: 4.密钥名称可 ...
- 四、Nginx负载均衡upstream
user www; worker_processes ; error_log /usr/local/nginx/logs/error.log crit; pid /usr/local/nginx/lo ...
- IOS 新消息通知提示-声音、震动
一.APNS 1.注册 [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificati ...
- 高级UIKit-04(NSUserDefaults、NSKeyedArchiver、对象归档方法)
[day05_1_UserDefault]:判断应用程序是否是第一次运行 NSUserDefaults:用来保存应用程序的配置信息如:程序运行次数,用户登陆信息等. // 使用系统提供的NSUserD ...
- java web从零单排第二十一期《Hibernate》主键的生成方式,用户增加与显示用户列表
1.新建register.jsp <%@ page language="java" import="java.util.*" pageEncoding=& ...
- 柔性数组-读《深度探索C++对象模型》有感
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- Java7新特性(一)Coin
1.语法糖 数字下划线 2.switch语句中的String 3.multicatch 4.final重抛 对比上份代码 5.try-with-resources(TWR) AutoC ...
- android设置eclipse中的自动提示功能
菜单window->Preferences->Java->Editor->Content Assist->Enable auto activation 选项要打上勾 (并 ...
- C#中System.Globalization.DateTimeFormatInfo.InvariantInfo怎么用
原文 C#中System.Globalization.DateTimeFormatInfo.InvariantInfo怎么用 在开发的时候,碰到下面这样一个问题: 在程序中显示当前系统时间,但是有一 ...
- 同一个form里,不管哪个 submit 都是直接提交form表单里的内容
要达到你的目的,就不能用类型为 submit 的按钮,要用 button,然后加onclick 方法来自定义预处理参数,然后再调用 submit 方法提交表单,比如 <script type=& ...