作者:範傳輝
如需轉載請聯絡大資料(ID:hzdashuju)
01 網路爬蟲概述
接下來從網路爬蟲的概念、用處與價值和結構等三個方面,讓大家對網路爬蟲有一個基本的瞭解。
1. 網路爬蟲及其應用
隨著網路的迅速發展,全球資訊網成為大量資訊的載體,如何有效地提取並利用這些資訊成為一個巨大的挑戰,網路爬蟲應運而生。網路爬蟲(又被稱為網頁蜘蛛、網路機器人),是一種按照一定的規則,自動地抓取全球資訊網資訊的程式或者指令碼。下麵透過圖3-1展示一下網路爬蟲在網際網路中起到的作用:
▲圖3-1 網路爬蟲
網路爬蟲按照系統結構和實現技術,大致可以分為以下幾種型別:通用網路爬蟲、聚焦網路爬蟲、增量式網路爬蟲、深層網路爬蟲。實際的網路爬蟲系統通常是幾種爬蟲技術相結合實現的。
搜尋引擎(Search Engine),例如傳統的通用搜索引擎baidu、Yahoo和Google等,是一種大型複雜的網路爬蟲,屬於通用性網路爬蟲的範疇。但是通用性搜尋引擎存在著一定的侷限性:
-
不同領域、不同背景的使用者往往具有不同的檢索目的和需求,通用搜索引擎所傳回的結果包含大量使用者不關心的網頁。
-
通用搜索引擎的標的是盡可能大的網路改寫率,有限的搜尋引擎伺服器資源與無限的網路資料資源之間的矛盾將進一步加深。
-
全球資訊網資料形式的豐富和網路技術的不斷發展,圖片、資料庫、音訊、影片多媒體等不同資料大量出現,通用搜索引擎往往對這些資訊含量密集且具有一定結構的資料無能為力,不能很好地發現和獲取。
-
通用搜索引擎大多提供基於關鍵字的檢索,難以支援根據語意資訊提出的查詢。
為瞭解決上述問題,定向抓取相關網頁資源的聚焦爬蟲應運而生。
聚焦爬蟲是一個自動下載網頁的程式,它根據既定的抓取標的,有選擇地訪問全球資訊網上的網頁與相關的連結,獲取所需要的資訊。與通用爬蟲不同,聚焦爬蟲並不追求大的改寫,而將標的定為抓取與某一特定主題內容相關的網頁,為面向主題的使用者查詢準備資料資源。
說完了聚焦爬蟲,接下來再說一下增量式網路爬蟲。增量式網路爬蟲是指對已下載網頁採取增量式更新和只爬行新產生的或者已經發生變化網頁的爬蟲,它能夠在一定程度上保證所爬行的頁面是盡可能新的頁面。
和週期性爬行和掃清頁面的網路爬蟲相比,增量式爬蟲只會在需要的時候爬行新產生或發生更新的頁面,並不重新下載沒有發生變化的頁面,可有效減少資料下載量,及時更新已爬行的網頁,減小時間和空間上的耗費,但是增加了爬行演演算法的複雜度和實現難度。
例如:想獲取趕集網的招聘資訊,以前爬取過的資料沒有必要重覆爬取,只需要獲取更新的招聘資料,這時候就要用到增量式爬蟲。
最後說一下深層網路爬蟲。Web頁面按存在方式可以分為表層網頁和深層網頁。表層網頁是指傳統搜尋引擎可以索引的頁面,以超連結可以到達的靜態網頁為主構成的Web頁面。深層網路是那些大部分內容不能透過靜態連結獲取的、隱藏在搜尋表單後的,只有使用者提交一些關鍵詞才能獲得的Web頁面。
例如使用者登入或者註冊才能訪問的頁面。可以想象這樣一個場景:爬取貼吧或者論壇中的資料,必須在使用者登入後,有許可權的情況下才能獲取完整的資料。
2. 網路爬蟲結構
下麵用一個通用的網路爬蟲結構來說明網路爬蟲的基本工作流程,如圖3-4所示。
▲圖3-4 網路爬蟲結構
網路爬蟲的基本工作流程如下:
-
首先選取一部分精心挑選的種子URL。
-
將這些URL放入待抓取URL佇列。
-
從待抓取URL佇列中讀取待抓取佇列的URL,解析DNS,並且得到主機的IP,並將URL對應的網頁下載下來,儲存進已下載網頁庫中。此外,將這些URL放進已抓取URL佇列。
-
分析已抓取URL佇列中的URL,從已下載的網頁資料中分析出其他URL,並和已抓取的URL進行比較去重,最後將去重過的URL放入待抓取URL佇列,從而進入下一個迴圈。
02 HTTP請求的Python實現
透過上面的網路爬蟲結構,我們可以看到讀取URL、下載網頁是每一個爬蟲必備而且關鍵的功能,這就需要和HTTP請求打交道。接下來講解Python中實現HTTP請求的三種方式:urllib2/urllib、httplib/urllib以及Requests。
1. urllib2/urllib實現
urllib2和urllib是Python中的兩個內建模組,要實現HTTP功能,實現方式是以urllib2為主,urllib為輔。
1.1 首先實現一個完整的請求與響應模型
urllib2提供一個基礎函式urlopen,透過向指定的URL發出請求來獲取資料。最簡單的形式是:
import urllib2
response=urllib2.urlopen('http://www.zhihu.com')
html=response.read()
print html
其實可以將上面對http://www.zhihu.com的請求響應分為兩步,一步是請求,一步是響應,形式如下:
import urllib2
# 請求
request=urllib2.Request('http://www.zhihu.com')
# 響應
response = urllib2.urlopen(request)
html=response.read()
print html
上面這兩種形式都是GET請求,接下來演示一下POST請求,其實大同小異,只是增加了請求資料,這時候用到了urllib。示例如下:
import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
postdata = {'username' : 'qiye',
'password' : 'qiye_pass'}
# info 需要被編碼為urllib2能理解的格式,這裡用到的是urllib
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
html = response.read()
但是有時會出現這種情況:即使POST請求的資料是對的,但是伺服器拒絕你的訪問。這是為什麼呢?問題出在請求中的頭資訊,伺服器會檢驗請求頭,來判斷是否是來自瀏覽器的訪問,這也是反爬蟲的常用手段。
1.2 請求頭essay-headers處理
將上面的例子改寫一下,加上請求頭資訊,設定一下請求頭中的User-Agent域和Referer域資訊。
import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
'password' : 'qiye_pass'}
# 將user_agent,referer寫入頭資訊
essay-headers={'User-Agent':user_agent,'Referer':referer}
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data,essay-headers)
response = urllib2.urlopen(req)
html = response.read()
也可以這樣寫,使用add_essay-header來新增請求頭資訊,修改如下:
import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
'password' : 'qiye_pass'}
data = urllib.urlencode(postdata)
req = urllib2.Request(url)
# 將user_agent,referer寫入頭資訊
req.add_essay-header('User-Agent',user_agent)
req.add_essay-header('Referer',referer)
req.add_data(data)
response = urllib2.urlopen(req)
html = response.read()
對有些essay-header要特別留意,伺服器會針對這些essay-header做檢查,例如:
-
User-Agent:有些伺服器或Proxy會透過該值來判斷是否是瀏覽器發出的請求。
-
Content-Type:在使用REST介面時,伺服器會檢查該值,用來確定HTTP Body中的內容該怎樣解析。在使用伺服器提供的RESTful或SOAP服務時,Content-Type設定錯誤會導致伺服器拒絕服務。常見的取值有:application/xml(在XML RPC,如RESTful/SOAP呼叫時使用)、application/json(在JSON RPC呼叫時使用)、application/x-www-form-urlencoded(瀏覽器提交Web表單時使用)。
-
Referer:伺服器有時候會檢查防盜鏈。
1.3 Cookie處理
urllib2對Cookie的處理也是自動的,使用CookieJar函式進行Cookie的管理。如果需要得到某個Cookie項的值,可以這麼做:
import urllib2
import cookielib
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
response = opener.open('http://www.zhihu.com')
for item in cookie:
print item.name+':'+item.value
但是有時候會遇到這種情況,我們不想讓urllib2自動處理,我們想自己新增Cookie的內容,可以透過設定請求頭中的Cookie域來做:
import urllib2
opener = urllib2.build_opener()
opener.addessay-headers.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) )
req = urllib2.Request( "http://www.zhihu.com/" )
response = opener.open(req)
print response.essay-headers
retdata = response.read()
1.4 Timeout設定超時
在Python2.6之前的版本,urllib2的API並沒有暴露Timeout的設定,要設定Timeout值,只能更改Socket的全域性Timeout值。示例如下:
import urllib2
import socket
socket.setdefaulttimeout(10) # 10 秒鐘後超時
urllib2.socket.setdefaulttimeout(10) # 另一種方式
在Python2.6及新的版本中,urlopen函式提供了對Timeout的設定,示例如下:
import urllib2
request=urllib2.Request('http://www.zhihu.com')
response = urllib2.urlopen(request,timeout=2)
html=response.read()
print html
1.5 獲取HTTP響應碼
對於200 OK來說,只要使用urlopen傳回的response物件的getcode()方法就可以得到HTTP的傳回碼。但對其他傳回碼來說,urlopen會丟擲異常。這時候,就要檢查異常物件的code屬性了,示例如下:
import urllib2
try:
response = urllib2.urlopen('http://www.google.com')
print response
except urllib2.HTTPError as e:
if hasattr(e, 'code'):
print 'Error code:',e.code
1.6 重定向
urllib2預設情況下會針對HTTP 3XX傳回碼自動進行重定向動作。要檢測是否發生了重定向動作,只要檢查一下Response的URL和Request的URL是否一致就可以了,示例如下:
import urllib2
response = urllib2.urlopen('http://www.zhihu.cn')
isRedirected = response.geturl() == 'http://www.zhihu.cn'
如果不想自動重定向,可以自定義HTTPRedirectHandler類,示例如下:
import urllib2
class RedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_301(self, req, fp, code, msg, essay-headers):
pass
def http_error_302(self, req, fp, code, msg, essay-headers):
result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code,
msg, essay-headers)
result.status = code
result.newurl = result.geturl()
return result
opener = urllib2.build_opener(RedirectHandler)
opener.open('http://www.zhihu.cn')
1.7 Proxy的設定
在做爬蟲開發中,必不可少地會用到代理。urllib2預設會使用環境變數http_proxy來設定HTTP Proxy。但是我們一般不採用這種方式,而是使用ProxyHandler在程式中動態設定代理,示例程式碼如下:
import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener([proxy,])
urllib2.install_opener(opener)
response = urllib2.urlopen('http://www.zhihu.com/')
print response.read()
這裡要註意的一個細節,使用urllib2.install_opener()會設定urllib2的全域性opener,之後所有的HTTP訪問都會使用這個代理。這樣使用會很方便,但不能做更細粒度的控制,比如想在程式中使用兩個不同的Proxy設定,這種場景在爬蟲中很常見。比較好的做法是不使用install_opener去更改全域性的設定,而只是直接呼叫opener的open方法代替全域性的urlopen方法,修改如下:
import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener(proxy,)
response = opener.open("http://www.zhihu.com/")
print response.read()
2. httplib/urllib實現
httplib模組是一個底層基礎模組,可以看到建立HTTP請求的每一步,但是實現的功能比較少,正常情況下比較少用到。在Python爬蟲開發中基本上用不到,所以在此只是進行一下知識普及。下麵介紹一下常用的物件和函式:
-
建立HTTPConnection物件:
class httplib.HTTPConnection(host[, port[, strict[, timeout[, source_address]]]])。
-
傳送請求:
HTTPConnection.request(method, url[, body[, essay-headers]])。
-
獲得響應:
HTTPConnection.getresponse()。
-
讀取響應資訊:
HTTPResponse.read([amt])。
-
獲得指定頭資訊:
HTTPResponse.getessay-header(name[, default])。
-
獲得響應頭(essay-header, value)元組的串列:
HTTPResponse.getessay-headers()。
-
獲得底層socket檔案描述符:
HTTPResponse.fileno()。
-
獲得頭內容:
HTTPResponse.msg。
-
獲得頭http版本:
HTTPResponse.version。
-
獲得傳回狀態碼:
HTTPResponse.status。
-
獲得傳回說明:
HTTPResponse.reason。
接下來演示一下GET請求和POST請求的傳送,首先是GET請求的示例,如下所示:
import httplib
conn =None
try:
conn = httplib.HTTPConnection("www.zhihu.com")
conn.request("GET", "/")
response = conn.getresponse()
print response.status, response.reason
print '-' * 40
essay-headers = response.getessay-headers()
for h in essay-headers:
print h
print '-' * 40
print response.msg
except Exception,e:
print e
finally:
if conn:
conn.close()
POST請求的示例如下:
import httplib, urllib
conn = None
try:
params = urllib.urlencode({'name': 'qiye', 'age': 22})
essay-headers = {"Content-type": "application/x-www-form-urlencoded"
, "Accept": "text/plain"}
conn = httplib.HTTPConnection("www.zhihu.com", 80, timeout=3)
conn.request("POST", "/login", params, essay-headers)
response = conn.getresponse()
print response.getessay-headers() # 獲取頭資訊
print response.status
print response.read()
except Exception, e:
print e
finally:
if conn:
conn.close()
3. 更人性化的Requests
Python中Requests實現HTTP請求的方式,是本人極力推薦的,也是在Python爬蟲開發中最為常用的方式。Requests實現HTTP請求非常簡單,操作更加人性化。
Requests庫是第三方模組,需要額外進行安裝。Requests是一個開源庫,原始碼位於:
GitHub: https://github.com/kennethreitz/requests
希望大家多多支援作者。
使用Requests庫需要先進行安裝,一般有兩種安裝方式:
-
使用pip進行安裝,安裝命令為:pip install requests,不過可能不是最新版。
-
直接到GitHub上下載Requests的原始碼,下載連結為:
https://github.com/kennethreitz/requests/releases
將原始碼壓縮包進行解壓,然後進入解壓後的檔案夾,執行setup.py檔案即可。
如何驗證Requests模組安裝是否成功呢?在Python的shell中輸入import requests,如果不報錯,則是安裝成功。如圖3-5所示。
▲圖3-5 驗證Requests安裝
3.1 首先還是實現一個完整的請求與響應模型
以GET請求為例,最簡單的形式如下:
import requests
r = requests.get('http://www.baidu.com')
print r.content
大家可以看到比urllib2實現方式的程式碼量少。接下來演示一下POST請求,同樣是非常簡短,更加具有Python風格。示例如下:
import requests
postdata={'key':'value'}
r = requests.post('http://www.xxxxxx.com/login',data=postdata)
print r.content
HTTP中的其他請求方式也可以用Requests來實現,示例如下:
r = requests.put('http://www.xxxxxx.com/put', data = {'key':'value'})
r = requests.delete('http://www.xxxxxx.com/delete')
r = requests.head('http://www.xxxxxx.com/get')
r = requests.options('http://www.xxxxxx.com/get')
接著講解一下稍微複雜的方式,大家肯定見過類似這樣的URL:
http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex;=1
就是在網址後面緊跟著“?”,“?”後面還有引數。那麼這樣的GET請求該如何傳送呢?肯定有人會說,直接將完整的URL帶入即可,不過Requests還提供了其他方式,示例如下:
import requests
payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
print r.url
透過列印結果,我們看到最終的URL變成了:
http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex;=1
3.2 響應與編碼
還是從程式碼入手,示例如下:
import requests
r = requests.get('http://www.baidu.com')
print 'content-->'+r.content
print 'text-->'+r.text
print 'encoding-->'+r.encoding
r.encoding='utf-8'
print 'new text-->'+r.text
其中r.content傳回的是位元組形式,r.text傳回的是文字形式,r.encoding傳回的是根據HTTP頭猜測的網頁編碼格式。
輸出結果中:“text–>”之後的內容在控制檯看到的是亂碼,“encoding–>”之後的內容是ISO-8859-1(實際上的編碼格式是UTF-8),由於Requests猜測編碼錯誤,導致解析文字出現了亂碼。Requests提供瞭解決方案,可以自行設定編碼格式,r.encoding=’utf-8’設定成UTF-8之後,“new text–>”的內容就不會出現亂碼。
但是這種手動的方式略顯笨拙,下麵提供一種更加簡便的方式:chardet,這是一個非常優秀的字串/檔案編碼檢測模組。安裝方式如下:
pip install chardet
安裝完成後,使用chardet.detect()傳回字典,其中confidence是檢測精確度,encoding是編碼形式。示例如下:
import requests
r = requests.get('http://www.baidu.com')
print chardet.detect(r.content)
r.encoding = chardet.detect(r.content)['encoding']
print r.text
直接將chardet探測到的編碼,賦給r.encoding實現解碼,r.text輸出就不會有亂碼了。
除了上面那種直接獲取全部響應的方式,還有一種流樣式,示例如下:
import requests
r = requests.get('http://www.baidu.com',stream=True)
print r.raw.read(10)
設定stream=True標誌位,使響應以位元組流方式進行讀取,r.raw.read函式指定讀取的位元組數。
3.3 請求頭essay-headers處理
Requests對essay-headers的處理和urllib2非常相似,在Requests的get函式中新增essay-headers引數即可。示例如下:
import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
essay-headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',essay-headers=essay-headers)
print r.content
3.4 響應碼code和響應頭essay-headers處理
獲取響應碼是使用Requests中的status_code欄位,獲取響應頭使用Requests中的essay-headers欄位。示例如下:
import requests
r = requests.get('http://www.baidu.com')
if r.status_code == requests.codes.ok:
print r.status_code# 響應碼
print r.essay-headers# 響應頭
print r.essay-headers.get('content-type')# 推薦使用這種獲取方式,獲取其中的某個欄位
print r.essay-headers['content-type']# 不推薦使用這種獲取方式
else:
r.raise_for_status()
上述程式中,r.essay-headers包含所有的響應頭資訊,可以透過get函式獲取其中的某一個欄位,也可以透過字典取用的方式獲取字典值,但是不推薦,因為如果欄位中沒有這個欄位,第二種方式會丟擲異常,第一種方式會傳回None。
r.raise_for_status()是用來主動地產生一個異常,當響應碼是4XX或5XX時,raise_for_status()函式會丟擲異常,而響應碼為200時,raise_for_status()函式傳回None。
3.5 Cookie處理
如果響應中包含Cookie的值,可以如下方式獲取Cookie欄位的值,示例如下:
import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
essay-headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',essay-headers=essay-headers)
# 遍歷出所有的cookie欄位的值
for cookie in r.cookies.keys():
print cookie+':'+r.cookies.get(cookie)
如果想自定義Cookie值發送出去,可以使用以下方式,示例如下:
import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
essay-headers={'User-Agent':user_agent}
cookies = dict(name='qiye',age='10')
r = requests.get('http://www.baidu.com',essay-headers=essay-headers,cookies=cookies)
print r.text
還有一種更加高階,且能自動處理Cookie的方式,有時候我們不需要關心Cookie值是多少,只是希望每次訪問的時候,程式自動把Cookie的值帶上,像瀏覽器一樣。Requests提供了一個session的概念,在連續訪問網頁,處理登入跳轉時特別方便,不需要關註具體細節。使用方法示例如下:
import Requests
oginUrl = 'http://www.xxxxxxx.com/login'
s = requests.Session()
#首先訪問登入介面,作為遊客,伺服器會先分配一個cookie
r = s.get(loginUrl,allow_redirects=True)
datas={'name':'qiye','passwd':'qiye'}
#向登入連結傳送post請求,驗證成功,遊客許可權轉為會員許可權
r = s.post(loginUrl, data=datas,allow_redirects= True)
print r.text
上面的這段程式,其實是正式做Python開發中遇到的問題,如果沒有第一步訪問登入的頁面,而是直接向登入連結傳送Post請求,系統會把你當做非法使用者,因為訪問登入介面時會分配一個Cookie,需要將這個Cookie在傳送Post請求時帶上,這種使用Session函式處理Cookie的方式之後會很常用。
3.6 重定向與歷史資訊
處理重定向只是需要設定一下allow_redirects欄位即可,例如:
r=requests.get(‘http://www.baidu.com’,allow_redirects=True)
將allow_redirects設定為True,則是允許重定向;設定為False,則是禁止重定向。如果是允許重定向,可以透過r.history欄位檢視歷史資訊,即訪問成功之前的所有請求跳轉資訊。示例如下:
import requests
r = requests.get('http://github.com')
print r.url
print r.status_code
print r.history
列印結果如下:
https://github.com/
200
(301]>,)
上面的示例程式碼顯示的效果是訪問GitHub網址時,會將所有的HTTP請求全部重定向為HTTPS。
3.7 超時設定
超時選項是透過引數timeout來進行設定的,示例如下:
requests.get('http://github.com', timeout=2)
3.8 代理設定
使用代理Proxy,你可以為任意請求方法透過設定proxies引數來配置單個請求:
import requests
proxies = {
"http": "http://0.10.1.10:3128",
"https": "http://10.10.1.10:1080",
}
requests.get("http://example.org", proxies=proxies)
也可以透過環境變數HTTP_PROXY和HTTPS_PROXY?來配置代理,但是在爬蟲開發中不常用。你的代理需要使用HTTP Basic Auth,可以使用http://user:password@host/語法:
proxies = {
"http": "http://user:pass@10.10.1.10:3128/",
}
03 小結
本文主要講解了網路爬蟲的結構和應用,以及Python實現HTTP請求的幾種方法。希望大家對本文中的網路爬蟲工作流程和Requests實現HTTP請求的方式重點吸收消化。
關於作者:範傳輝,資深網蟲,Python開發者,參與開發了多項網路應用,在實際開發中積累了豐富的實戰經驗,並善於總結,貢獻了多篇技術文章廣受好評。研究興趣是網路安全、爬蟲技術、資料分析、驅動開發等技術。
本文摘編自《Python爬蟲開發與專案實戰》,經出版方授權釋出。
延伸閱讀《Python爬蟲開發與專案實戰》
點選上圖瞭解及購買
轉載請聯絡微信:DoctorData
推薦語:零基礎學習爬蟲技術,從Python和Web前端基礎開始講起,由淺入深,包含大量案例,實用性強。
朋友會在“發現-看一看”看到你“在看”的內容