【爬蟲大世界】php
學習爬蟲,最初的操做即是模擬瀏覽器向服務器發出請求。至於怎麼作,沒必要感到無從下手,Python提供了功能齊全的類庫來幫助咱們完成這一操做html
最基礎的HTTP庫有urllib、httplib2、request、treq等python
【3.1使用urllib】nginx
在Python2中,有urllib和urllib2兩個庫來實現請求的發送;而在Python3中,已經不存在urllib2了,統一爲urllib,其官方文檔爲:https://docs.python.org/3/library/urllib.html
編程
urllib庫是Python內置的HTTP請求庫,它包含4個模塊:瀏覽器
△request:它是最基本的HTTP請求模塊,用來模擬發送請求。只需給庫方法傳入URL以及額外的參數,就能夠模擬[在瀏覽器輸入網址,按下回車]這一過程服務器
△error:異常處理模塊,若是出現請求錯誤,咱們能夠捕捉這些異常,而後進行重試或其餘操做,保證程序不會意外終止cookie
parse /pɑ:rs/ v.從語法上描述或分析(詞句等)網絡
△parse:一個工具模塊,提供了許多URL處理方法,好比拆分、解析、合併等數據結構
△robotparser:主要用來識別網站的robots.txt文件,而後判斷哪些網站能夠爬,哪些不能夠
【3.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
二、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( )方法接收兩個字符串
三、高級用法
在上面的過程當中,咱們雖然能夠構造請求,但對於一些更高級的操做(如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模塊中定義的異常
一、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而沒有直接報錯,就避免了程序異常終止,同時有效處理異常
二、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協議】
一、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: /
二、爬蟲名稱
事實上,各個網站的爬蟲都有了固定的名字,好比:
爬蟲名稱 |
名稱 |
網站 |
BaiduSpider |
百度 |
|
Googlebot |
谷歌 |
|
360Spider |
360搜索 |
|
YodaoBot |
有道 |
|
ia_archiver |
Alexa |
|
Scooter |
altavista |
更多的能夠在網上進行搜索
三、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模塊的基本用法和實例,利用它判斷出哪些頁面時能夠爬取、哪些不能夠
''''''''''''''