python爬蟲的一些技巧

用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

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:cookie

return zlib.decompress(data)

而後就簡單了,

encoding_support = ContentEncodingProcessor
opener = urllib2.build_opener( encoding_support, urllib2.HTTPHandler )多線程

直接用opener打開網頁,若是服務器支持gzip/defalte則自動解壓縮

content = opener.open(url).read()架構

更方便地多線程python爬蟲

  1. 用twisted進行異步I/O抓取

事實上更高效的抓取並不是必定要用多線程,也可使用異步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

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. 設計一個簡單的多線程抓取類

仍是以爲在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壓縮,超時設置,自動重試,設置棧大小,自動登錄等功能;代碼簡單,使用方便 性能也不俗。

相關文章
相關標籤/搜索