爬虫总结_python

2019年11月12日 阅读数:104
这篇文章主要向大家介绍爬虫总结_python,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

 

import sqlite3html

Python 的一个很是大的优势是很容易写很容易跑起来,缺点就是不少不那么著名的(甚至一些著名的)程序和库都不像 C 和 C++ 那边那样专业、可靠(固然这也有动态类型 vs 静态类型的缘由)。前端

首先,爬虫属于IO密集型程序(网络IO和磁盘IO),这类程序的瓶颈大多在网络和磁盘读写的速度上,多线程 在必定程度上能够加速爬虫的效率,可是这个“加速”没法超过min(出口带宽,磁盘写的速度),并且,关于Python的多线程,因为GIL的存在,其实是有一些初学者不容易发现的坑的。

算法设计、人工智能、统计分析、编程语言设计那些应用数学关系紧密的方向。

 python写爬虫,没有使用Scrapy吗?item属性定义明确,pipelines来作入库处理,怎么会出现sql错误 ?python

作爬虫难点是解析页面和反爬,你居然还没到这一步就挂了,还能说啥?试试scrapy吧,若是规模大须要调度就上redis。react

你用了sqllite作存储,应该是个规模很小的项目?那不如直接存文件。上了规模建议mongodb,pymongo很靠谱!linux

 sql注入爬虫,好像很好玩啊git

useragent 模仿百度("Baiduspider..."),2. IP每爬半个小时就换一个IP代理。
小黎也发现了对应的变化,因而在 Nginx 上设置了一个频率限制,每分钟超过120次请求的再屏蔽IP。 同时考虑到百度家的爬虫有可能会被误伤,想一想市场部门每个月几十万的投放,因而写了个脚本,经过 hostname 检查下这个 ip 是否是真的百度家的,对这些 ip 设置一个白名单。
随机1-3秒爬一次,爬10次休息10秒,天天只在8-12,18-20点爬,隔几天还休息一下。
小黎看着新的日志头都大了,再设定规则不当心会误伤真实用户,因而准备换了一个思路,当3个小时的总请求超过50次的时候弹出一个验证码弹框,没有准确正确输入的话就把 IP 记录进黑名单。
图像识别(关键词 PIL,tesseract),再对验证码进行了二值化,分词,模式训练以后,识别了小黎的验证码(关于验证码,验证码的识别,验证码的反识别也是一个恢弘壮丽的斗争史
验证码被攻破后,和开发同窗商量了变化下开发模式,数据并再也不直接渲染,而是由前端同窗异步获取,而且经过 js 的加密库生成动态的 token,同时加密库再进行混淆(比较重要的步骤的确有网站这样作,参见微博的登录流程)。
内置浏览器引擎的爬虫(关键词:PhantomJS,Selenium),在浏览器引擎中js 加密脚本算出了正确的结果
不要只看 Web 网站,还有 App 和 H5,他们的反爬虫措施通常比较少
若是真的对性能要求很高,能够考虑多线程(一些成熟的框架如 scrapy都已支持),甚至分布式
selenium + xvfb = headless spider in linux.
加验证码,限制请求频次。破解办法依然有,前端代码或者说用户正常浏览能作的,爬虫都能作,应对爬虫没什么绝对可行的办法,想爬的迟早能爬到,最可能是成本有差别
cnproxy之类的代理分享网站抓代理ip和端口
爬取搜狗微信公众号搜索的非公开接口,没作宣传的时候,流量不大,用的比较好,宣传后,用的人多了,就发现被反爬虫了,一直500
经过chrome的开发者工具分析搜狗页面的请求,发现不是经过ip反爬,而是cookie里的几个关键字段,应对方法就是写个定时任务,维护一个cookie池,定时更新cookie,爬的时候随机取cookie,如今基本不会出现500


因此应对反爬虫,先分析服务器是经过什么来反爬,经过ip就用代理,经过cookie就换cookie,针对性的构建request。
 
 好像没有验证user agent,cookie是直接解析response头部的set-cookie来的,
 维持本身的cookie池
 
 
 
 
 

抓取大多数状况属于get请求,即直接从对方服务器上获取数据。github

首先,Python中自带urllib及urllib2这两个模块,基本上能知足通常的页面抓取。另外,requests也是很是有用的包,与此相似的,还有httplib2等等。web

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和传输数据,多个参数用&链接。ajax

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)
 
限制频率: Requests,Urllib2均可以使用time库的sleep()函数:
import time
time.sleep(1)
有时还会检查是否带Referer信息还会检查你的Referer是否合法,通常再加上Referer。
 
 
4. 对于断线重连

很少说。redis

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
网易新闻排行榜: https://github.com/lining0806/NewsSpider
 
 ajax的js请求地址:
 这里,若使用Google Chrome分析”请求“对应的连接(方法:右键→审查元素→Network→清空,点击”加载更多“,出现对应的GET连接寻找Type为text/html的,点击,查看get参数或者复制Request URL),
 

 

 

 

 

主要涉及的库

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 以及 随手搜的网页,不一一列举。

 

 

 

 

 

 

 

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. 更方便地多线程

总结一文的确说起了一个简单的多线程模板,可是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想一想怎么进行多线程调用最方便呢?

一、用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来进行批量网址处理的文章不错,由浅入深,深刻浅出,能够一看。

二、设计一个简单的多线程抓取类

仍是以为在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. 一些琐碎的经验

一、链接池:

opener.open和urllib2.urlopen同样,都会新建一个http请求。一般状况下这不是什么问题,由于线性环境下,一秒钟可能 也就新生成一个请求;然而在多线程环境下,每秒钟能够是几十上百个请求,这么干只要几分钟,正常的有理智的服务器必定会封禁你的。

然而在正常的html请求时,保持同时和服务器几十个链接又是很正常的一件事,因此彻底能够手动维护一个HttpConnection的池,而后每次抓取时从链接池里面选链接进行链接便可。

这里有一个取巧的方法,就是利用squid作代理服务器来进行抓取,则squid会自动为你维护链接池,还附带数据缓存功能,并且squid原本就是我每一个服务器上面必装的东东,何须再自找麻烦写链接池呢。

二、设定线程的栈大小

栈大小的设定将很是显著地影响python的内存占用,python多线程不设置这个值会致使程序占用大量内存,这对openvz的vps来讲很是致命。stack_size必须大于32768,实际上应该总要32768*2以上

from threading import stack_size
stack_size(32768*16)

三、设置失败后自动重试

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

四、设置超时

import socket socket.setdefaulttimeout(10) #设置10秒后链接超时

五、登录

登录更加简化了,首先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. 更方便地多线程

总结一文的确说起了一个简单的多线程模板,可是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想一想怎么进行多线程调用最方便呢?

一、用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来进行批量网址处理的文章不错,由浅入深,深刻浅出,能够一看。

二、设计一个简单的多线程抓取类

仍是以为在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