用python寫爬蟲程序,入門很快,要進階從「能用」提高到「用的省心省事」有不少方面須要改進 下面是一些技巧總結。
Gzip/deflate支持
如今的網頁廣泛支持gzip壓縮,這每每能夠解決大量傳輸時間,以VeryCD的主頁爲例,未壓縮版本247k,壓縮了之後45k,爲原來的1/5。這就意味着抓取速度會快5倍。
而後python的urllib/urllib2默認都不支持壓縮,要返回壓縮格式,必須在request的headar裏面寫明’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 """html
# add headers to requests
def http_request(self, req):python
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
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:cookie
return zlib.decompress(data)
encoding_support = ContentEncodingProcessor
opener = urllib2.build_opener( encoding_support, urllib2.HTTPHandler )多線程
content = opener.open(url).read()架構
更方便地多線程python爬蟲
事實上更高效的抓取並不是必定要用多線程,也可使用異步I/O法:直接用twisted的getPage方法,而後分別加上異步I/O結束時的callback和errback方法便可。例如能夠這麼幹:
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):socket
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
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 )
content = opener.open(url).read()
仍是以爲在urllib之類python「本土」的東東里面折騰去來更舒服。試想一下,若是有個Fetcher類,你能夠這麼調用
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則是若是有「正在運行的任務」或者「隊列中的任務」則爲是,也好辦,因而代碼以下:
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)
一些瑣碎的經驗
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方法:
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網站。
如此,把上述全部小技巧都糅合起來就能夠顯著的改善python爬蟲,它支持多線程,gzip/deflate壓縮,超時設置,自動重試,設置棧大小,自動登錄等功能;代碼簡單,使用方便 性能也不俗。