爬蟲總結_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
相關文章
相關標籤/搜索