【爬蟲大世界】
學習爬蟲,最初的操作便是模擬瀏覽器向服務器發出請求。至於怎么做,不必感到無從下手,Python提供了功能齊全的類庫來幫助我們完成這一操作
最基礎的HTTP庫有urllib、httplib2、request、treq等
【3.1使用urllib】
在Python2中,有urllib和urllib2兩個庫來實現請求的發送;而在Python3中,已經不存在urllib2了,統一為urllib,其官方文檔為:https://docs.python.org/3/library/urllib.html
urllib庫是Python內置的HTTP請求庫,它包含4個模塊:
△request:它是最基本的HTTP請求模塊,用來模擬發送請求。只需給庫方法傳入URL以及額外的參數,就可以模擬[在瀏覽器輸入網址,按下回車]這一過程
△error:異常處理模塊,如果出現請求錯誤,我們可以捕捉這些異常,然后進行重試或其他操作,保證程序不會意外終止
parse /pɑ:rs/ v.從語法上描述或分析(詞句等)
△parse:一個工具模塊,提供了許多URL處理方法,比如拆分、解析、合並等
△robotparser:主要用來識別網站的robots.txt文件,然后判斷哪些網站可以爬,哪些不可以
【3.1.1發送請求】
1、urlopen( )
urllib.request模塊提供了最基本的構造HTTP請求的方法,利用它可以模擬瀏覽器的一個請求發起過程。以抓取Python官網為例:
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(response.read().decode('utf-8'))
運行這兩行代碼,可以得到:
這里輸出了網頁的源代碼
△如果在上述代碼中不添加decode( )方法,源代碼中的一些轉義字符將無法被轉義,比如上圖中的空行是通過'\n'實現的,如果刪去了decode( )方法,將會直接顯示\n,而不是空行
如果刪去decode( )方法,除了不轉義之外,還有就是返回的數據類型為bytes,而不是str。由於返回的數據過多,需要將終端窗口擴充才能看到完整的數據,也就是在原有的str數據添加:b' ',用以表示bytes類型的數據
【bytes(字節流)類型數據】
事實上,一切數據在傳輸時都是bytes類型,原因在於為了更好地傳輸非英文系的數據(諸如漢字、韓文等),需要根據Unicode將這些字進行encode(編碼),比如漢字'中',它對應的bytes類型數據為b'\xe4\xb8\xad',進行編碼后,漢字'中'就能夠進行數據傳輸了
為了統一,原本不需要進行編碼的英文系數據也進行了編碼,比如字母'a',它對應的bytes類型就直接是b'a'。這種編碼只是形式上的,'a'與b'a'並沒有一目了然的區別
我們基本上不需要關心bytes類型數據的編碼和解碼,因為這一切都是計算機自動完成的,普通用戶不需要了解這么深,但對於學計算機的人必須了解,因為往往在寫代碼時進行數據操作,需要在bytes的編碼和解碼之間來回轉換
原本服務器傳回的HTML代碼的解碼工作由瀏覽器自動完成,但這里我們通過Python類庫模擬瀏覽器向服務器發送請求,並沒有瀏覽器,因此但凡是之前瀏覽器的工作,都需要我們自行完成
下面來看看它返回的到底是什么:
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(type(response))
利用type( )方法輸出響應的類型,得到如下結果:
<class 'http.client.HTTPResponse'>
可以發現,它是一個HTTPResponse類型的對象,再通過help(response),可以得知HTTPResponse類型的對象主要包含read( )、readinto( )、getheader(name)、getheaders( )、fileno( )等方法,以及msg、version、status、reason、debuglevel、closed等屬性
將這個對象賦值為response變量后,就可以調用這些方法和屬性了
read( )方法可以得到返回的網頁內容,我們可以試試調用其他的方法和屬性:比如status屬性可以得到返回結果的狀態碼,最常見的200代表請求成功、404代表網頁未找到
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(response.stauts) print(response.getheaders()) print(response.getheader('Server'))
得到輸出:
nginx(engine X) n.一款輕量級的Web服務器/反向代理服務器/電子郵件代理服務器
調用status屬性直接輸出了響應狀態碼200;然后調用getheaders( )方法,輸出了響應的頭信息,顯示為一個元組列表(a sequence of two-element tuples);最后通過getheader( )方法並傳遞一個'Server'參數獲取響應頭中的值【為什么是元組列表而不是字典?】
利用最基本的urlopen( )方法,可以完成最基本的簡單網頁的GET請求抓取
【API(Application Programming Interface),應用程序編程接口】
API是一些預先定義的函數,目的是“提供應用程序與開發人員基於某軟件或硬件得以訪問一組例程的能力,而又無需訪問源碼,或理解內部工作機制的細節”
如果我們要給鏈接傳遞一些參數,該如何實現?首先看一下urlopen( )函數的API:
urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
直接在Python官網中搜索'urllib.request.urlopen'即可查看該函數的API,可以發現,除了第一個參數可以傳遞URL外,我們還可以傳遞其他內容,比如data(附加數據)、timeout(超時時間)
●data參數
data參數是可選的,對於urlopen( )函數而言,data參數必須是字節流編碼格式的內容,即bytes類型,因此我們使用Python的bytes( )方法進行轉化
另外,如果傳遞了這個參數,則它的請求就不再是GET方式,而是POST方式
import urllib.request import urllib.parse data = bytes(urllib.parse.urlencode({'word' : 'hello'}), encoding = 'utf-8') response = urllib.request.urlopen('http://httpbin.org/post', data = data) print(response.read().encode('utf-8'))
首先我們假設要傳遞{'word' : 'hello'}這個數據,先是通過parse模塊中的urlencode( )方法將字典轉化為字符串;然后由於data參數類型必須是bytes,再用bytes( )函數,且通過第二個參數encoding指定編碼格式,將原本為str類型數據的data轉換為bytes類型數據
【urlencode( )】
留意一下,常見的URL中如果包含參數,它們出現在URL中的形式不會是鍵值對,而是類似,即通過'&'字符連接兩個鍵值對,而鍵值對的鍵與值之間直接用'='連接,整體是一個字符串
urlencode( )方法就是幫助我們把鍵值對轉化為上述這種形式的:
這里請求的站點是httpbin.org,它提供HTTP請求測試;這里請求的URL為http://httpbin.org/post,這個鏈接可以用來測試POST請求,輸出請求的一些信息,其中包含我們傳遞的data參數:
我們傳遞的參數出現在了form字段中,這表明是模擬了表單提交的方式,以POST方式傳輸數據
●timeout參數
timeout參數用於設置超時時間,單位為秒。當請求超出了設置的這個時間,還沒有得到響應,就會拋出異常。如果不指定該參數,就會使用全局默認時間。
它支持HTTP、HTTPS、FTP請求
import urllib.request response = urllib.request.urlopen('http://httpbin.org/get', timeout = 0.1) print(reponse.read().decode('utf-8'))
輸出結果為:
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File ''xxx.py'' , line 3 , in <module>
reponse = urllib.request.urlopen('http://httpbin.org/get' , timeout = 1)
...
urllib.error.URLError: <urlopen error timed out>
這里我們設置超時時間是0.1秒。程序過0.1秒后,服務器依然沒有響應,就會拋出URLError異常。該異常屬於urllib.error模塊,錯誤原因是超時
因此,可以通過設置這個超時時間來控制一個網頁如果長時間未響應,就跳過它的抓取。可以利用try except語句來實現:
socket (原)n.孔,插座 (計)n.套接字
網絡上兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個socket
import socket import urllib.request import urllib.error try: response = urllib.request.urlopen('http://httpbin.org/get', timeout = 0.1) except urllib.error.URLError as e: if isinstance(e.reason, socket.timeout): print('Time Out!')
我們請求了http://httpbin.org/get測試鏈接,設置超時時間為0.1秒,然后捕獲了URLError異常
一開始導入的socket模塊暫時難以理解透徹,只需知道,socket模塊中有timeout屬性,如果要判斷一個異常的發生是否真的是由於timeout,那么就需要通過isinstance( )函數來判斷
對於所有的異常,都有一個reason屬性,用以顯示該異常的具體情況
就上述代碼而言:
可以看到,其顯示異常的具體情況為'socket.timeout'
按照常理來說,0.1秒內基本不可能得到服務器的響應,因此上述代碼中成功輸出了'Time Out!'
●其他參數
我們回顧urlopen( )函數的其他API:
urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
除了data和timeout參數,還有其他參數,下面來簡要概述:
context n.語境,上下文,背景,環境
△context參數:用來指定SSL設置,它必須是ssl.SSLContext類型
△cafile、capath參數:分別指定CA證書和它的路徑,應用於請求HTTPS鏈接
△cadefault參數:現已棄用,默認值為False
綜上,便是urlopen( )方法的用法,詳見https://docs.python.org/3/library/urllib.request.html
2、Request
我們可以利用urlopen( )函數實現最基本請求的發起,但查看urlopen( )函數的API,似乎這幾個參數並不足以構建一個完整的請求
如果請求中需要加入Headers等信息,就可以利用更強大的Request類來構建
import urllib.request request = urllib.request.Request('https://python.org') response = urllib.request.urlopen(request) print(reponse.read().decode('utf-8'))
以上述代碼為例,我們依然使用urlopen( )來發送這個請求,只是這次的參數不再是URL,而是一個Request類型的對象
通過構造這個數據結構,一方面可以將請求獨立成一個對象,另一方面實現靈活且豐富地配置參數
查看Request的構造方法:【為什么官網上沒有相關資料?】
urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
Request存在的意義便是在發送請求時附加上一些信息,而僅靠urlopen( )則不能完成
下面來分析一下各個參數:
□ url:為用於請求的URL,是必傳參數,其它都是可選參數
□ data:該參數與urlopen( )函數中的data參數一樣,限定為bytes類型數據;且如果是字典,需用urllib.parse模塊中的urlencode( )函數編碼
□ headers:該參數為一個字典,它就是請求頭
通過該函數,可以修改原本請求頭中的信息,比如User-Agent,默認的User-Agent是Python-urllib,如果要偽裝成火狐瀏覽器,可以把它設置為:
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
從而實現爬蟲偽裝
我們可以在構建請求時通過headers參數直接構造,也可以通過請求實例的add_header( )方法
□ origin_req_host:請求方的host名稱或IP地址
verify /ˈverɪfaɪ/ v.核實,證明,判定
□ unverifiable:默認為False,表示這個請求是否是無法驗證的,意思是用戶沒有足夠的權限來選擇接收這個結果的請求
“例如,我們請求一個HTML文檔中的圖片,但是我們沒有自動抓取圖像的權限,此時unverifiable的值就是True”
□ method:該參數為一個字符串,用來指示請求使用的方法,如'GET'、'POST'和'PUT'等
from urllib import request , parse url = 'http://httpbin.org/post' headers = { 'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' , 'Host' : 'httpbin.org' } dict = {'name' : 'Examine2'} data = bytes(parse.urlencode(dict), encoding = 'utf-8') req = request.Request(url = url, headers = headers, data = data, method = 'POST') response = request.urlopen(req) print(response.read().decode('utf-8'))
我們嘗試傳入多個參數來構建一個請求,url指定請求URL,headers中指定User-Agent和Host,參數data用urlencode( )和bytes( )轉換成字節流,指定請求方式為POST
執行上面示例代碼,可得到輸出:
可以看到,除卻data中的數據成功發送外,我們還設置了headers和method
之前有提及,headers修改請求頭中的信息“可以通過請求實例的add_header( )方法”,即:
req = request.Request(url = url, data = data, method = 'POST') req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
注意add_header( )方法接收兩個字符串
3、高級用法
在上面的過程中,我們雖然可以構造請求,但對於一些更高級的操作(如Cookies處理、代理設置等)還是無法實現
為此,我們需要更強大的工具【Handler】
Handler可以理解為各種處理器,有專門處理登錄驗證的,有處理Cookies的,有處理代理設置的...
首先介紹一下urllib.request模塊中的BaseHandler類,它是所有其他Handler的父類,它提供了最基本的方法,如default_open( )、protocol_request( )等
接下來就有各種Handler子類繼承這個BaseHandler類,如:
□ HTTPDefaultErrorHandler:用於處理HTTP響應錯誤,錯誤都會拋出HTTPError類型的異常
□ HTTPRedirectHandler:用於處理重定向
□ HTTPCookieProcessor:用於處理Cookies
□ ProxyHandler:用於設置代理,默認代理為空
Mgr → manager
□ HTTPPasswordMgr:用於管理密碼,它維護了用戶名和密碼的表
Auth → authentication /ɔ:ˌθentɪ'keɪʃn/ n.認證
authentic /ɔ:ˈθentɪk/ adj.真的,可信的,認證了的
□ HTTPBasicAuthHandler:用於管理認證(鏈接打開時可能需要認證)
其余Handler請查閱:https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
另外一個比較重要的類是【OpenerDirector】,簡稱為【Opener】;我們之前用過的urlopen( )方法就是urllib為我們提供的一個Opener
為什么要引入Opener?
之前使用的Request和urlopen( )相當於類庫為你封裝好了極其常用的請求方法,利用它們可以完成最基本的請求,但為了實現更高級的功能,我們需要深入一層進行配置,使用更底層的實例來完成操作
Opener可以使用open( )方法,返回的類型和urlopen( )如出一轍(記住urlopen( )也是Opener)
Opener與Handler的關系是:利用Handler來構建Opener
下面來看看幾個實例:(可暫時不過於深究,待需要時回來復習)
●驗證
有時打開某些網頁,會出現以下對話框:
倘若要請求這樣的頁面,就需要借助HTTPBasicAuthHandler
realm /rɛlm]/ n.王國,領域
from urllib.request import HTTPBasciAuthHandler, HTTPPasswordMgrWithDefaultRealm, build_opener from urllib.error import URLError url = 'http://localhost:5000/'#←此為書上示范網址,未知原因無法打開 username = 'username' password = 'password' p = HTTPPasswordMgrWithDefaultRealm() p.add_password(None, url, username, password) auth_handler = HTTPBasicAuthHandler(p) opener = build_opener(auth_handler) try: result = opener.open(url) print(result.read().decode('utf-8')) except URLError as e: print(e.reason)
這里實例化了HTTPBasicAuthHandler對象,其參數是HTTPPasswordMgrWithDefaultRealm對象,它利用add_password( )添加用戶名和密碼,這樣就建立了一個處理驗證的Handler
然后利用這個Handler通過build_opener( )構建一個Opener,使用Opener的open( )方法打開鏈接,調用open( )就相當於調用了urlopen( ),剩下步驟相似
integrate /ˈɪntɪgreɪt/ v.使一體化,合並 adj.整體的、完整的
IDE → Integrated Development Environment 集成開發環境,是用於程序開發環境的應用程序,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面等工具
實踐過程中始終出現Error:[WinError 10061] 由於目標計算機積極拒絕,無法連接。
嘗試了許多方法,如:http://tieba.baidu.com/p/5995082291?pid=123475984092&cid=0#123475984092
其給出的方案是將服務器代碼和客戶端代碼分別置於兩個終端命令窗口執行;嘗試了將范例中的代碼搬運,依瓢畫葫蘆成功打開http://localhost:5000/網址,但依舊爬取失敗,原因未知
●代理
做爬蟲時,免不了要使用代理,如果要添加代理,可以這樣做:
from urllib.error import URLError from urllib.request import ProxyHandler, build_opener proxy_handler = ProxyHandler({ 'http' : 'http://127.0.0.1:9743' , 'https' : 'https://127.0.0.1:9743' }) opener = build_opener(proxy_handler) try: result = opener.open('https://www.baidu.com') print(result.read().decode('utf-8')) except URLError as e: print(e.reason)
此次爬取同樣出現Error:[WinError 10061] 由於目標計算機積極拒絕,無法連接。
“這里我們搭建了一個本地代理,它運行在9743端口上” 【?】
這里使用的ProxyHandler,其參數是一個字典,鍵名是協議類型(如HTTP或HTTPS等),鍵值是代理鏈接,可以添加多個代理
然后利用這個Handler加build_opener( )方法構建Opener,發送請求即可
●Cookies
jar /dʒɑr/ n.罐子、缸、杯 v.猛然震動,不一致
Mozilla /məuzilə/ Mosaic+Godzilla中文名稱摩斯拉,Mozilla FireFox(火狐瀏覽器)的生產廠商
Cookies的處理就需要相關的Handler了
首先看看如何將網站的Cookies獲取下來:
import http.cookiejar , urllib.request cookie = http.cookiejar.CookieJar() handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') for item in cookie: print(item.name +'='+ item.value)
首先我們創建了一個CookieJar對象實例,然后利用HTTPCookieProcessor來構建一個Handler,最后構建出Opener,執行函數open( )即可
執行open( )函數后,目標網址上的Cookies就被獲取,並存儲到變量cookie中,結果輸出:
這里可以看到輸出了每條Cookie的名稱和值
此外,獲取的Cookies存儲在變量cookie中,並非是單純地以字典的形式儲存,而是CookieJar類型,我們在上述代碼后面添加print(cookie),可以看到:
因此不能簡單地通過for key , value in cookie.items( ):來輸出
不過,既然能輸出,那就有可能輸出成文件格式。Cookies實際上也是以文本形式保存的。
firename = 'cookies.txt' cookie = http.cookiejar.MozillaCookieJar(filename) handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') cookie.save(ignore_discard = True, ignore_expires = True)
這時的CookieJar就需要換成MozillaCookieJar了
【CookieJar子類】
CookieJar本身是一種類型對象,用於
①管理HTTP Cookie值
②存儲HTTP請求生成的Cookie
③向傳出的HTTP請求添加Cookie對象
【FileCookieJar(filename , delayload = None , policy = None)】:
從CookieJar派生而來,用來創建FileCookieJar實例,檢索Cookie信息並將Cookie存儲到文件中
filename是存儲cookie的文件名;delayload為True時,支持延時訪問文件,即只有在需要時才讀取文件或在文件中儲存數據
【MozillaCookieJar(filename , delayload = None , policy = None)】:
從FileCookieJar派生而來,創建與Mozilla瀏覽器cookie.txt兼容的FileCookieJar實例
【LWPCookieJar(filename , delayload = None , policy = None)】:
從FileCookieJar派生而來,創建與libwww-perl標准的Set-Cookie3格式兼容的FileCookieJar實例
大多數情況下,我們只用CookieJar;如果需要和本地文件交互,就用MozillaCookieJar或LWPCookieJar
上述代碼將CookieJar替換成MozillaCookieJar,並調用了save( )函數:save(ignore_discard = True , ignore_expires = True),成功將Cookies保存成Mozilla型瀏覽器的Cookies格式
discard /dɪsˈkɑ:d/ v.丟棄,解雇,出牌 n.被拋棄的人,丟出的牌
【Mozilla型瀏覽器】
之外我們介紹Request類型對象時,介紹到了headers,而使用Request對象作為urlopen( )參數而不是單純的URL的根本原因是,Request能在請求時發送一些附加信息,如headers
我們有說到“代理”,headers中的User-Agent是方便我們偽裝成瀏覽器的,默認為Python-urllib;我們之前偽裝成火狐瀏覽器,就是將User-Agent修改為
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
我們查閱一下其他瀏覽器的User-Agent,發現:
搜狗瀏覽器 1.x
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)
360瀏覽器
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)
世界之窗(The World) 3.x
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)
IE 9.0
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0
你會發現,幾乎所有的瀏覽器的User-Agent都帶有'Mozilla'字符串,這也是為什么MozillaCookieJar會成為常用的Cookie存儲格式
至於為什么都會帶有'Mozilla'字符串,詳細請參閱:https://blog.csdn.net/puppylpg/article/details/47319401
總結下來就是,
最初的Mozilla瀏覽器功能卓越,擁有Mozilla Frame(框架);當其他瀏覽器被研發出來時,不想放棄Mozilla Frame,通通就在User-Agent上聲明自己支持Mozilla(事實上是偽裝成Mozilla)
而當時偽裝成Mozilla的其他瀏覽器又擁有各自的優點,當后續瀏覽器被研發,為了兼容其他瀏覽器的優點,也總是在User-Agent上偽裝成其他瀏覽器,追根溯源,導致絕大部分瀏覽器都在User-Agent上標注為'Mozilla'。所謂User-Agent,也變得混亂不堪,失去了原本的意義
盡管現在Mozilla瀏覽器已經被淘汰(繼承的是Mozilla FireFox瀏覽器),但就是這個已經不存在的瀏覽器,“在形式上”存活於其他各大瀏覽器的User-Agent中
這就是Mozilla型瀏覽器
題外話到此結束
上述代碼將CookieJar修改為MozillaCookieJar,運行代碼,會生成一個cookie.txt文件,內容如下:
這也就是Mozilla型瀏覽器儲存Cookies的格式
生成文件是通過在創建MozillaCookieJar實例時傳遞文件名、調用save( )函數實現的,除此之外,步驟與普通的CookieJar一樣;事實上,如果這時調用for item in cookie: print(item.name +'='+ item.value),仍可輸出原來的結果
這表明MozillaCookieJar生成的cookie實例在后續操作中產生的變化與原來的CookieJar是一樣(除了實例類型從CookieJar變成MozillaCookieJar),只是在保存成文本時修改了格式,與Mozilla瀏覽器兼容
另外,上述在介紹CookieJar子類時提及到了LWPCookieJar
LWP是Library for WWW access Perl的縮寫,Perl是一門編程語言,LWP是訪問Web服務器的Perl包,利用這個包,可以很方便地在Perl腳本里面訪問外部Web服務器上的資源
因此將Cookie保存成libwww-perl(LWP)格式的Cookies文件有好處
要生成LWP格式的Cookies文件,只需修改:
cookie = http.CookieJar.LWPCookieJar(filename)
可以得到一個新的cookie.txt文件:
生成了Cookies文件可以對其進行讀取並利用(以LWPCookieJar格式為例,MozillaCookieJar格式的同樣適用),當請求的網站需要Cookies時:
import http.cookiejar , urllib.reqeust cookie = http.cookiejar.LWPCookieJar() cookie.load('cookie.txt') handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') print(response.read().decode('utf-8'))
這里,在已經生成了LWPCookieJar格式的Cookies並保存成文件的前提下,通過調用load( )方法來讀取本地的Cookies文件,獲取到Cookies的內容
通過同樣的方法構建Handler和Opener后,即可輸出百度網頁的源碼
【3.1.2處理異常】
前一節我們了解了請求的發送過程,但某些時候會出現各種異常,倘若不處理這些異常,程序很可能因報錯而終止運行。因此我們很有必要學會異常處理
urllib的error模塊定義了由request模塊產生的異常;如果出現了問題,request模塊就會拋出error模塊中定義的異常
1、URLError
“URLError類來自urllib庫的error模塊,它繼承自OSError,是error異常模塊的基類,由request模塊產生的異常都可以通過捕獲這個類來處理”
它具有一個reason屬性,即返回錯誤的原因
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.URLError as e: print(e.reason)
我們嘗試打開一個不存在的頁面,按理說應該會報錯,但這時我們捕獲了URLError這個異常,成功輸出了'Not Found'字樣
所謂不存在的頁面,是服務器成功響應(狀態碼200)后,但服務器檢測到該域名下並不存在的網頁;而在地址欄上隨便打上不存在的URL(諸如baaidu.com),屬於根本沒有作出響應;兩種情況不同
同樣的還有https://www.52pojie.cn/thread-666632-1-1.htmlnn
程序輸出了e.reason而沒有直接報錯,就避免了程序異常終止,同時有效處理異常
2、HTTPError
HTTPError是URLError的子類,專門用來處理HTTP請求錯誤,比如認證請求失敗等
它有3個屬性:
□ code:返回HTTP狀態碼,404網頁不存在、500服務器內部錯誤...
□ reason:繼承自父類URLError,同樣返回錯誤的原因
□ headers:返回請求頭
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.HTTPError as e: print(e.reason, e.code, e.headers, sep = '\n')
sep → separator,默認為' ',替換print( )函數中的逗號
輸出為:
依舊是同樣的網址,這里捕獲了HTTPError,並輸出了reason、code和headers屬性
因為URLError是HTTPError的父類,所以一般推薦先選擇捕獲子類的錯誤,再去捕獲父類的錯誤:
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.HTTPError as e: print(e.reason, e.code, e.headers, sep = '\n') except error.URLError as e: print(e.reason) else: print('Request Successfully!')
有時候,reason返回的是諸如'Not Found'的字符串,但有時候返回的也可能是一個對象
我們在學習urlopen( )的timeout參數時,有代碼行if isinstance(e.reason , socket.timeout),如果這時輸入代碼print(type(e.reason)),結果會輸出<class 'socket.timeout'>
【3.1.3解析鏈接】
前面有提及urllib的parse模塊,它定義了處理URL的標准接口,實現URL各部分的抽取、合並以及鏈接轉換等
它支持如下協議的URL處理:file、ftp、gopher、hdl、http、https、imap、mailto、mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、sip、sips、snews、svn、svn+ssh、telnet和wais
①、urlparse( )
該方法可以實現URL的識別和分段:
from urllib import parse result = urllib.parse.urlparse('http://www.baidu.com/index.html;user?id=5#comment') print(type(result), result)
利用urlparse( )對一個URL進行解析,輸出解析結果的類型以及解析結果:
可以看到,返回的結果是一個ParseResult類型的對象,它包含6個部分,分別是scheme、netloc、path、params、query和fragment
scheme /ski:m/ v./n.計划,圖謀 n.體系 位於'://'之前,代表協議
netloc → network locality 代表域名,也即服務器位置,位於'/'后面、';'或'?'前面的全部
path 訪問路徑,也即網頁文件在服務器中的位置
params → parameters n.形參(復) 位於';'后面,是可選參數
query /ˈkwɪəri/ v./n.詢問,疑問 n.問題,問號 位於'?'后面的查詢條件,一般用作GET類型的URL,用'&'連接鍵值對
fragment /ˈfrægmənt/ v.破碎、破裂 n.碎片,片段,分段,未完成的部分 位於'#'后面,是錨點,用於直接定位頁面內部的下拉位置
所以可以得出一個標准的鏈接格式:
scheme://netloc/path;params?query#fragment
一個標准的URL都會符合這個規則,利用urlparse( )將其拆分出來
除卻最基本的解析方式外,urlparse( )還擁有其他配置。查閱它的API用法:
urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)
□ urlstring:為必填項,即待解析的URL
□ scheme:它是默認的協議。倘若這個URL沒有帶協議信息,會將這個作為默認的協議
from urllib.parse import urlparse result = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme = 'https') print(result)
可以看到,URL沒有包含最前面的scheme信息,但返回了指定默認的scheme參數
【URL不攜帶scheme屬性時,原本netloc的值為什么搬運至path處?】
假如我們帶上scheme:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme = 'https') print(result)
可見,scheme參數只有在URL中不包含scheme信息時才會生效
□ allow_fragments:即是否允許fragment,默認為True。當它被設置為False時,原fragment部分會被忽略,被解析為path、params或者query的一部分,fragment部分為空
from urllib.parse import urlparse reslut = urlparse('http://www.baidu.com/index.html;user?id=5#comment', allow_fragments = False) print(result)
如果URL中不包含params和query:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments = False) print(result)
當URL中不包含params和query,fragment會被解析為path的一部分
實際上返回的ParseResult是一個元組,我們可以用索引來獲取,也可以用屬性名來獲取:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments = False) print(result.scheme, result[0], result.netloc, result[1], sep = '\n')
得到輸出:
通過索引和屬性名來獲取,其結果是一致的
②、urlunparse( )
有了urlparse( ),就有它的對立方法urlunparse( )
urlunprase( )接受的參數是一個可迭代對象(諸如list、tuple等),但它的長度必須是6,否則會拋出參數不足或過多的問題
from urllib.parse import urlunparse data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment'] print(urlunparse(data))
這樣我們就成功實現了URL的構造
③、urlsplit( )
這個方法與urlparse( )非常相似,只不過它不再單獨解析params部分,而是將params合並到path中,只返回5個結果:
from urllib.parse import urlsplit result = urlsplit('http://www.baidu.com/index.html;user?id=5#comment') print(result)
返回的SplitResult類型對象同樣也是一個元組,可以通過之前提到的兩種方式來獲取
④、urlunsplit( )
傳入的參數必須是長度為5的可迭代對象,params部分提前合並到path中,詳細參考urlunparse( )
⑤、urljoin( )
對於如何生成鏈接,我們可以使用urlunparse( )和urlunsplit( ),但前提是必須有特定長度的對象,且鏈接的每一部分都要清晰分開
生成鏈接還有另外一個方法,那就是urljoin( ),在學習urljoin( )之前,我們先回顧join( )用法:
【join( )】(sep).join(seq) → (separator).join(sequence)
join( )不能像print( )一樣單獨調用,它需要一個'sep',即分隔符(separator);而join( )接受一個序列
▷'分'.join(['1' , '2' , '3']) ==> '1分2分3'
▷''.join(['1' , '2' , '3']) ==> '123'
可以看到,join( )用於將序列中的元素按指定的連接符合並成一個新字符串;當sep為空時,就是純粹地將序列中的元素連接成一個新字符串
既然是合並字符串,那么原有的序列中的元素也必須是字符串:
▷'分'.join([1 , 2 , 3])
這樣會拋出異常TypeError: sequence item 0: excepted str instance, int found
urljoin( )的API為urljoin(base, url, allow_fragment=True),它接收兩個參數,base_url(基礎鏈接)為第一個參數,新的URL作為第二個參數;該方法會分析base_url的scheme、netloc和path這三個內容,並對新鏈接缺失的部分進行補充
通過實例可以看出,urljoin( )將base_url解析,剝離出scheme、netloc和path三項內容(如果有),檢查url參數是否在這三項內容上有殘缺,如果有,將會用剝離出的scheme、netloc或path來修補;如果url參數本身就含有scheme、netloc或path,將保持自身的內容
base_url中的params、query和fragment將不起作用
⑥、urlencode( )
該方法在之前已經有提前學習,簡而言之,其作用就是將字典轉換為符合URL的字符串
有時為了更加方便地構造參數,我們會事先將參數用字典來表示,然后調用urlencode( )來構造URL
from urllib.parse import urlencode params = {'name' : 'Examine2', 'age' : '19'} base_url = 'http://www.baidu.com?' url = base_url + urlencode(params) print(url)
結果輸出:
參數就成功地由字典轉化為GET請求參數了
⑦、parse_qs( )
qs是query string的縮寫,特指URL中的query參數,而一般在URL中,query參數表現為:
事實上parse_qs( )是反序列化,旨在將GET請求參數轉化回字典,與urlencode( )相反,你甚至可以把parse_qs( )看作是urldecode( )
from urllib.parse import parse_qs query = 'name=Examine2&age=19' print(parse_qs(query))
代表query參數的字符串成功轉化為字典,而由於字典的值有時候並非只有一個元素,因此parse_qs( )返回的字典中,值全部為含有str類型數據的list
⑧、parse_qsl( )
qsl是query string list的縮寫,該方法與parse_qs( )十分相似,只是返回的結果不再是dict,而是由tuple組成的list
from urllib.parse import parse_qsl query = 'name=Examine2&age=19' print(parse_qsl(query))
鑒於list中每個元素都是tuple,故可以通過parse_qsl(query)[0][1]的方式來讀取數據
⑨、quote( )
quote /kwəʊt/ v.引用、引述,報價
如果傳遞給URL中文參數,有時可能導致亂碼,quote( )就是將中文字符轉化為URL編碼的
from ullib.parse import quote keyword = '真實' url = 'https://www.baidu.com/s?wd=' + quote(keyword) print(url)
可以看到,中文字符'真實'被編碼成了'%E7%9C%9F%E5%AE%9E',而這也其實是搜索關鍵字在網頁中存在的真正形式:在百度搜索欄中輸入'真實',盡管URL中顯示出中文字符:
但打開網頁源文件,會發現這時變成了
quote( )默認的編碼方式為UTF-8
⑩、unquote( )
quote( )的對立方法,能夠對URL中隱藏的中文字符進行解碼
from urllib.parse import unquote print(unquote('https:///www.baidu.com/s?wd=%E7%9C%9F%E5%AE%9E'))
【3.1.4分析Robots協議】
1、Robots協議
exclude /ɪkˈsklu:d/ v.排除,排斥,驅除
Robots協議(爬蟲協議、機器人協議),全名為網絡爬蟲排除標准(Robots Exclusion Protocol),用來告訴爬蟲和搜索引擎哪些頁面可以爬取、哪些不可以
它通常是一個叫作robots.txt的文本文件,一般放在網站的根目錄下
當搜索爬蟲訪問一個站點時,它首先會檢查這個站點根目錄下是否存在robots.txt文件,如果存在,搜索爬蟲會根據其中定義的爬取范圍來爬取;如果沒有找到這個文件,搜索爬蟲便會訪問所有可直接訪問的頁面
下面來看一個robots.txt的樣例:
User-agent: *
Disallow: /
Allow: /public/
上述robots.txt限制了所有爬蟲只可以爬取public目錄的功能
將上述內容保存成robots.txt文件,放在網站的根目錄下,和網站的入口文件(比如index.php、index.html和index.jsp等)放在一起
上面的User-agent描述了搜索爬蟲的名稱,這里設置為*表示該協議對任何爬取爬蟲都有效
比如,設置為User-agent: Baiduspider代表我們設置的規則對百度爬蟲是有效的;如果有多條User-agent記錄,則會有多個爬取爬蟲受到限制
Disallow指定了不可以抓取的頁面,以上例子設置為/則代表不允許爬取任何頁面
Allow通常與Disallow一起使用,一般不會單獨使用,它用來排除某些限制。現在設置為/public/,則表示所有頁面不允許抓取,但可以抓取public目錄
下面來看幾個例子:
①禁止所有爬蟲訪問任何目錄:
User-agent: *
Disallow: /
②允許所有爬蟲訪問任何目錄:
User-agent: *
Disallow:
(或者直接把robots.txt文件留空)
③禁止所有爬蟲訪問網站某些目錄:
User-agent: *
Disallow: /private/
Disallow: /tmp/
tmp → temporary .tmp文件是臨時文件,很多都沒有什么價值
④只允許某個爬蟲訪問:
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
2、爬蟲名稱
事實上,各個網站的爬蟲都有了固定的名字,比如:
爬蟲名稱 |
名稱 |
網站 |
BaiduSpider |
百度 |
|
Googlebot |
谷歌 |
|
360Spider |
360搜索 |
|
YodaoBot |
有道 |
|
ia_archiver |
Alexa |
|
Scooter |
altavista |
更多的可以在網上進行搜索
3、robotparser
了解了Robots協議后,我們可以使用robotparser模塊來解析robots.txt了
robotparser模塊只提供了一個類RobotFileParser,它可以根據某網站的robots.txt文件來判斷一個爬取爬蟲是否有權限來爬取這個網頁
該類的API為:urllib.robotparser.RobotFileParser(url=' '),構造RobotFileParser的實例只需傳入robots.txt的鏈接即可;當然,可以在聲明時不傳入,使其按默認的空URL創建一個實例,然后調用實例的set_url( )方法
這個類的幾個常用方法:
□ set_url( ):用來設置robots.txt文件的鏈接。如果在創建RobotFileParser對象時就已經傳入了鏈接,則不需要再使用這個方法設置了
□ read( ):讀取robots.txt文件並進行分析。注意這個方法執行了一個讀取和分析的操作,如果不調用這個方法,接下來的判斷都為False,所以一定要記得調用這個方法。該方法不會返回任何內容,但執行了讀取操作
□ parse( ):用來解析robots.txt文件,傳入的參數是robots.txt某些行的內容,它會按照robots.txt的語法規則來分析這些內容。該方法允許直接分析已有的robots.txt文件,可以看作是read( )的替補
fetch /fetʃ/ v.取來、拿來,售得 n.詭計,風浪區
□ can_fetch( ):該方法傳入兩個參數,第一個是User-agent(可以是代表所有爬蟲的'*'),第二個是要抓取的URL(該URL必須存在於構造時傳入的URL的子目錄下)。它返回True或False,代表該搜索引擎是否可以爬取這個URL
□ mtime( ):'m'的含義未知,我的理解是'modified time',將返回上次抓取和分析robots.txt的時間。這個方法對於長時間分析和抓取的爬蟲是很有必要的,你可能需要定期檢查來抓取最新的robots.txt
□ modified( ):將當前時間設置為上次抓取和分析robots.txt的時間【作用?】
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url('http://www.jianshu.com/robots.txt') rp.read() print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d')) print(rp.can_fetch('*', 'http://www.jianshu.com/search?q=python&page=1&type=collections'))
上述代碼中,我們首先創建了一個RobotFileParser對象,通過set_url( )方法設置了robots.txt的鏈接,接着利用can_fetch( )方法判斷了網頁是否可以被爬取:
上面的代碼是通過set_url( )和read( )來獲取即將要分析的robots.txt,其實也等價於:
rp.parse(urlopen('http://www.jianshu.com/robots.txt').read( ).decode('utf-8').split('\n'))
以上便是robotparser模塊的基本用法和實例,利用它判斷出哪些頁面時可以爬取、哪些不可以
''''''''''''''