python crawler(2)
以前寫過一篇使用python爬蟲抓站的一些技巧總結,總結了諸多爬蟲使用的方法;那篇東東現在看來還是挺有用的,但是當時很菜(現在也菜,但是比那時進步了不少),很多東西都不是很優,屬于”只是能用”這么個層次。這篇進階篇打算把“能用”提升到“用得省事省心”這個層次。
一、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的方式來處理:
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 requestsdef?http_request(self, req): req.add_header("Accept-Encoding",?"gzip, deflate")?return?req ??# decode?defhttp_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 =?urllib2.build_opener(?encoding_support,urllib2.HTTPHandler?)???#直接用opener打開網頁,如果服務器支持gzip/defalte則自動解壓縮?content = opener.open(url).read()二、更方便地多線程
總結一文的確提及了一個簡單的多線程模板,但是那個東東真正應用到程序里面去只會讓程序變得支離破碎,不堪入目。在怎么更方便地進行多線程方面我也動了一番腦筋。先想想怎么進行多線程調用最方便呢?
1、用twisted進行異步I/O抓取
事實上更高效的抓取并非一定要用多線程,也可以使用異步I/O法:直接用twisted的getPage方法,然后分別加上異步I/O結束時的callback和errback方法即可。例如可以這么干:
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類,你可以這么調用
f = Fetcher(threads=10)?#設定下載線程數為10?for?url?in?urls: f.push(url)?#把所有url推入下載隊列?whilef.taskleft():?#若還有未完成下載的線程?content = f.pop()?#從下載完成隊列中取出結果?do_with(content)?# 處理content內容這么個多線程調用簡單明了,那么就這么設計吧,首先要有兩個隊列,用Queue搞定,多線程的基本架構也和“技巧總結”一文類似,push方法和 pop方法都比較好處理,都是直接用Queue的方法,taskleft則是如果有“正在運行的任務”或者”隊列中的任務”則為是,也好辦,于是代碼如 下:
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):?returnself.q_req.qsize()+self.q_ans.qsize()+self.running???def?push(self,req):?self.q_req.put(req)???def?pop(self):?returnself.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 =?''?printwhat?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)三、一些瑣碎的經驗
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?data4、設置超時
import?socket?socket.setdefaulttimeout(10)?#設置10秒后連接超時5、登陸
登陸更加簡化了,首先build_opener中要加入cookie支持,參考“總結”一文;如要登陸VeryCD,給Fetcher新增一個空方法login,并在__init__()中調用,然后繼承Fetcher類并override login方法:
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網站。
四、總結
如此,把上述所有小技巧都糅合起來就和我目前的私藏最終版的Fetcher類相差不遠了,它支持多線程,gzip/deflate壓縮,超時設置,自動重試,設置棧大小,自動登錄等功能;代碼簡單,使用方便,性能也不俗,可謂居家旅行,殺人放火,咳咳,之必備工具。
之所以說和最終版差得不遠,是因為最終版還有一個保留功能“馬甲術”:多代理自動選擇。看起來好像僅僅是一個random.choice的區別,其實包含了代理獲取,代理驗證,代理測速等諸多環節,這就是另一個故事了總結
以上是生活随笔為你收集整理的python crawler(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python crawler(1)
- 下一篇: python spider code