先我們介紹一個 Python 庫,叫做 urllib,利用它我們可以實現 HTTP 請求的發送,而不用去關心 HTTP 協議本身甚至更低層的實現。我們只需要指定請求的 URL、請求頭、請求體等信息即可實現 HTTP 請求的發送,同時 urllib 還可以把服務器返回的響應轉化為 Python 對象,通過該對象我們便可以方便地獲取響應的相關信息了,如響應狀態碼、響應頭、響應體等等。
注意:在 Python 2 中,有 urllib 和 urllib2 兩個庫來實現請求的發送。而在 Python 3 中,已經不存在 urllib2 這個庫了,統一為 urllib,其官方文檔鏈接為:https://docs.python.org/3/library/urllib.html。
首先,我們來了解一下 urllib 庫的使用方法,它是 Python 內置的 HTTP 請求庫,也就是說不需要額外安裝即可使用。它包含如下 4 個模塊。
使用 urllib 的 request 模塊,我們可以方便地實現請求的發送并得到響應。我們先來看下它的具體用法。
urllib.request 模塊提供了最基本的構造 HTTP 請求的方法,利用它可以模擬瀏覽器的一個請求發起過程,同時它還帶有處理授權驗證(Authentication)、重定向(Redirection)、瀏覽器 Cookie 以及其他內容。
下面我們來看一下它的強大之處。這里以 Python 官網為例,我們來把這個網頁抓下來:
import urllib.request
response=urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))
運行結果如圖所示。
圖 運行結果
這里我們只用了兩行代碼,便完成了 Python 官網的抓取,輸出了網頁的源代碼。得到源代碼之后呢?我們想要的鏈接、圖片地址、文本信息不就都可以提取出來了嗎?
接下來,看看它返回的到底是什么。利用 type 方法輸出響應的類型:
import urllib.request
response=urllib.request.urlopen('https://www.python.org')
print(type(response))
輸出結果如下:
<class 'http.client.HTTPResponse'>
可以發現,它是一個 HTTPResposne 類型的對象,主要包含 read、readinto、getheader、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.status)
print(response.getheaders())
print(response.getheader('Server'))
運行結果如下:
200
[('Server', 'nginx'), ('Content-Type', 'text/html; charset=utf-8'), ('X-Frame-Options', 'DENY'), ('Via', '1.1 vegur'), ('Via', '1.1 varnish'), ('Content-Length', '48775'), ('Accept-Ranges', 'bytes'), ('Date', 'Sun, 15 Mar 2020 13:29:01 GMT'), ('Via', '1.1 varnish'), ('Age', '708'), ('Connection', 'close'), ('X-Served-By', 'cache-bwi5120-BWI, cache-tyo19943-TYO'), ('X-Cache', 'HIT, HIT'), ('X-Cache-Hits', '2, 518'), ('X-Timer', 'S1584278942.717942,VS0,VE0'), ('Vary', 'Cookie'), ('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
nginx
可見,前兩個輸出分別輸出了響應的狀態碼和響應的頭信息,最后一個輸出通過調用 getheader 方法并傳遞一個參數 Server 獲取了響應頭中的 Server 值,結果是 nginx,意思是服務器是用 Nginx 搭建的。
利用最基本的 urlopen 方法,可以完成最基本的簡單網頁的 GET 請求抓取。
如果想給鏈接傳遞一些參數,該怎么實現呢?首先看一下 urlopen 方法的 API:
urllib.request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
可以發現,除了第一個參數可以傳遞 URL 之外,我們還可以傳遞其他內容,比如 data(附加數據)、timeout(超時時間)等。
下面我們詳細說明這幾個參數的用法。
data 參數是可選的。如果要添加該參數,需要使用 bytes 方法將參數轉化為字節流編碼格式的內容,即 bytes 類型。另外,如果傳遞了這個參數,則它的請求方式就不再是 GET 方式,而是 POST 方式。
下面用實例來看一下:
import urllib.parse
import urllib.request
data=bytes(urllib.parse.urlencode({'name': 'germey'}), encoding='utf-8')
response=urllib.request.urlopen('https://httpbin.org/post', data=data)
print(response.read().decode('utf-8'))
這里我們傳遞了一個參數 word,值是 hello。它需要被轉碼成 bytes(字節流)類型。其中轉字節流采用了 bytes 方法,該方法的第一個參數需要是 str(字符串)類型,需要用 urllib.parse 模塊里的 urlencode 方法來將參數字典轉化為字符串;第二個參數指定編碼格式,這里指定為 utf-8。
這里請求的站點是 httpbin.org,它可以提供 HTTP 請求測試。本次我們請求的 URL 為 https://httpbin.org/post,這個鏈接可以用來測試 POST 請求,它可以輸出 Request 的一些信息,其中就包含我們傳遞的 data 參數。
運行結果如下:
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germey"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7",
"X-Amzn-Trace-Id": "Root=1-5ed27e43-9eee361fec88b7d3ce9be9db"
},
"json": null,
"origin": "17.220.233.154",
"url": "https://httpbin.org/post"
}
我們傳遞的參數出現在了 form 字段中,這表明是模擬了表單提交的方式,以 POST 方式傳輸數據。
timeout 參數用于設置超時時間,單位為秒,意思就是如果請求超出了設置的這個時間,還沒有得到響應,就會拋出異常。如果不指定該參數,就會使用全局默認時間。它支持 HTTP、HTTPS、FTP 請求。
下面用實例來看一下:
import urllib.request
response=urllib.request.urlopen('https://httpbin.org/get', timeout=0.1)
print(response.read())
運行結果可能如下:
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "/var/py/python/urllibtest.py", line 4, in <module> response=urllib.request.urlopen('https://httpbin.org/get', timeout=0.1)
...
urllib.error.URLError: <urlopen error _ssl.c:1059: The handshake operation timed out>
這里我們設置的超時時間是 1 秒。程序運行 1 秒過后,服務器依然沒有響應,于是拋出了 URLError 異常。該異常屬于 urllib.error 模塊,錯誤原因是超時。
因此,可以通過設置這個超時時間來控制一個網頁如果長時間未響應,就跳過它的抓取。這可以利用 try…except 語句來實現,相關代碼如下:
import socket
import urllib.request
import urllib.error
try:
response=urllib.request.urlopen('https://httpbin.org/get', timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason, socket.timeout):
print('TIME OUT')
這里我們請求了 https://httpbin.org/get 這個測試鏈接,設置的超時時間是 0.1 秒,然后捕獲了 URLError 這個異常,然后判斷異常類型是 socket.timeout,意思就是超時異常。因此,得出它確實是因為超時而報錯,打印輸出了 TIME OUT。
運行結果如下:
TIME OUT
按照常理來說,0.1 秒內基本不可能得到服務器響應,因此輸出了 TIME OUT 的提示。
通過設置 timeout 這個參數來實現超時處理,有時還是很有用的。
除了 data 參數和 timeout 參數外,還有 context 參數,它必須是 ssl.SSLContext 類型,用來指定 SSL 設置。
此外,cafile 和 capath 這兩個參數分別指定 CA 證書和它的路徑,這個在請求 HTTPS 鏈接時會有用。
cadefault 參數現在已經棄用了,其默認值為 False。
前面講解了 urlopen 方法的用法,通過這個最基本的方法,我們可以完成簡單的請求和網頁抓取。若需更加詳細的信息,可以參見官方文檔:https://docs.python.org/3/library/urllib.request.html。
我們知道利用 urlopen 方法可以實現最基本請求的發起,但這幾個簡單的參數并不足以構建一個完整的請求。如果請求中需要加入 Headers 等信息,就可以利用更強大的 Request 類來構建。
首先,我們用實例來感受一下 Request 類的用法:
import urllib.request
request=urllib.request.Request('https://python.org')
response=urllib.request.urlopen(request)
print(response.read().decode('utf-8'))
可以發現,我們依然用 urlopen 方法來發送這個請求,只不過這次該方法的參數不再是 URL,而是一個 Request 類型的對象。通過構造這個數據結構,一方面我們可以將請求獨立成一個對象,另一方面可更加豐富和靈活地配置參數。
下面我們看一下 Request 可以通過怎樣的參數來構造,它的構造方法如下:
class urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
其中,第一個參數 url 用于請求 URL,這是必傳參數,其他都是可選參數。
第二個參數 data 如果要傳,必須傳 bytes(字節流)類型的。如果它是字典,可以先用 urllib.parse 模塊里的 urlencode() 編碼。
第三個參數 headers 是一個字典,它就是請求頭。我們在構造請求時,既可以通過 headers 參數直接構造,也可以通過調用請求實例的 add_header() 方法添加。
添加請求頭最常用的方法就是通過修改 User-Agent 來偽裝瀏覽器。默認的 User-Agent 是 Python-urllib,我們可以通過修改它來偽裝瀏覽器。比如要偽裝火狐瀏覽器,你可以把它設置為:
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
第四個參數 origin_req_host 指的是請求方的 host 名稱或者 IP 地址。
第五個參數 unverifiable 表示這個請求是否是無法驗證的,默認是 False,意思就是說用戶沒有足夠權限來選擇接收這個請求的結果。例如,我們請求一個 HTML 文檔中的圖片,但是我們沒有自動抓取圖像的權限,這時 unverifiable 的值就是 True。
第六個參數 method 是一個字符串,用來指示請求使用的方法,比如 GET、POST 和 PUT 等。
下面我們傳入多個參數來構建請求:
from urllib import request, parse
url='https://httpbin.org/post'
headers={
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
'Host': 'httpbin.org'
}
dict={'name': 'germey'}
data=bytes(parse.urlencode(dict), encoding='utf-8')
req=request.Request(url=url, data=data, headers=headers, method='POST')
response=request.urlopen(req)
print(response.read().decode('utf-8'))
這里我們通過 4 個參數構造了一個請求,其中 url 即請求 URL,headers 中指定了 User-Agent 和 Host,參數 data 用 urlencode 和 bytes 方法轉成字節流。另外,指定了請求方式為 POST。
運行結果如下:
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germey"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)",
"X-Amzn-Trace-Id": "Root=1-5ed27f77-884f503a2aa6760df7679f05"
},
"json": null,
"origin": "17.220.233.154",
"url": "https://httpbin.org/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)')
如此一來,我們就可以更加方便地構造請求,實現請求的發送啦。
在上面的過程中,我們雖然可以構造請求,但是對于一些更高級的操作(比如 Cookies 處理、代理設置等),該怎么辦呢?
接下來,就需要更強大的工具 Handler 登場了。簡而言之,我們可以把它理解為各種處理器,有專門處理登錄驗證的,有處理 Cookie 的,有處理代理設置的。利用它們,我們幾乎可以做到 HTTP 請求中所有的事情。
首先,介紹一下 urllib.request 模塊里的 BaseHandler 類,它是所有其他 Handler 的父類,它提供了最基本的方法,例如 default_open、protocol_request 等。
接下來,就有各種 Handler 子類繼承這個 BaseHandler 類,舉例如下。
另外,還有其他的 Handler 類,這里就不一一列舉了,詳情可以參考官方文檔: https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler。
關于怎么使用它們,現在先不用著急,后面會有實例演示。
另一個比較重要的類就是 OpenerDirector,我們可以稱為 Opener。我們之前用過 urlopen 這個方法,實際上它就是 urllib 為我們提供的一個 Opener。
那么,為什么要引入 Opener 呢?因為需要實現更高級的功能。之前使用的 Request 和 urlopen 相當于類庫為你封裝好了極其常用的請求方法,利用它們可以完成基本的請求,但是現在不一樣了,我們需要實現更高級的功能,所以需要深入一層進行配置,使用更底層的實例來完成操作,所以這里就用到了 Opener。
Opener 可以使用 open 方法,返回的類型和 urlopen 如出一轍。那么,它和 Handler 有什么關系呢?簡而言之,就是利用 Handler 來構建 Opener。
下面用幾個實例來看看它們的用法。
在訪問某些設置了身份認證的網站時,例如 https://ssr3.scrape.center/,我們可能會遇到這樣的認證窗口,如圖 2- 所示:
圖 2- 認證窗口
如果遇到了這種情況,那么這個網站就是啟用了基本身份認證,英文叫作 HTTP Basic Access Authentication,它是一種用來允許網頁瀏覽器或其他客戶端程序在請求時提供用戶名和口令形式的身份憑證的一種登錄驗證方式。
那么,如果要請求這樣的頁面,該怎么辦呢?借助 HTTPBasicAuthHandler 就可以完成,相關代碼如下:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener
from urllib.error import URLError
username='admin'
password='admin'
url='https://ssr3.scrape.center/'
p=HTTPPasswordMgrWithDefaultRealm()
p.add_password(None, url, username, password)
auth_handler=HTTPBasicAuthHandler(p)
opener=build_opener(auth_handler)
try:
result=opener.open(url)
html=result.read().decode('utf-8')
print(html)
except URLError as e:
print(e.reason)
這里首先實例化 HTTPBasicAuthHandler 對象,其參數是 HTTPPasswordMgrWithDefaultRealm 對象,它利用 add_password 方法添加進去用戶名和密碼,這樣就建立了一個處理驗證的 Handler。
接下來,利用這個 Handler 并使用 build_opener 方法構建一個 Opener,這個 Opener 在發送請求時就相當于已經驗證成功了。
接下來,利用 Opener 的 open 方法打開鏈接,就可以完成驗證了。這里獲取到的結果就是驗證后的頁面源碼內容。
在做爬蟲的時候,免不了要使用代理,如果要添加代理,可以這樣做:
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener
proxy_handler=ProxyHandler({
'http': 'http://127.0.0.1:8080',
'https': 'https://127.0.0.1:8080'
})
opener=build_opener(proxy_handler)
try:
response=opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)
這里我們在本地需要先事先搭建一個 HTTP 代理,運行在 8080 端口上。
這里使用了 ProxyHandler,其參數是一個字典,鍵名是協議類型(比如 HTTP 或者 HTTPS 等),鍵值是代理鏈接,可以添加多個代理。
然后,利用這個 Handler 及 build_opener 方法構造一個 Opener,之后發送請求即可。
Cookie 的處理就需要相關的 Handler 了。
我們先用實例來看看怎樣將網站的 Cookie 獲取下來,相關代碼如下:
import http.cookiejar, urllib.request
cookie=http.cookiejar.CookieJar()
handler=urllib.request.HTTPCookieProcessor(cookie)
opener=urllib.request.build_opener(handler)
response=opener.open('https://www.baidu.com')
for item in cookie:
print(item.name + "=" + item.value)
首先,我們必須聲明一個 CookieJar 對象。接下來,就需要利用 HTTPCookieProcessor 來構建一個 Handler,最后利用 build_opener 方法構建出 Opener,執行 open 函數即可。
運行結果如下:
BAIDUID=A09E6C4E38753531B9FB4C60CE9FDFCB:FG=1
BIDUPSID=A09E6C4E387535312F8AA46280C6C502
H_PS_PSSID=31358_1452_31325_21088_31110_31253_31605_31271_31463_30823
PSTM=1590854698
BDSVRTM=10
BD_HOME=1
可以看到,這里輸出了每個 Cookie 條目的名稱和值。
不過既然能輸出,那可不可以輸出成文件格式呢?我們知道 Cookie 實際上也是以文本形式保存的。
答案當然是肯定的,這里通過下面的實例來看看:
import urllib.request, http.cookiejar
filename='cookie.txt'
cookie=http.cookiejar.MozillaCookieJar(filename)
handler=urllib.request.HTTPCookieProcessor(cookie)
opener=urllib.request.build_opener(handler)
response=opener.open('https://www.baidu.com')
cookie.save(ignore_discard=True, ignore_expires=True)
這時 CookieJar 就需要換成 MozillaCookieJar,它在生成文件時會用到,是 CookieJar 的子類,可以用來處理 Cookie 和文件相關的事件,比如讀取和保存 Cookie,可以將 Cookie 保存成 Mozilla 型瀏覽器的 Cookie 格式。
運行之后,可以發現生成了一個 cookie.txt 文件,其內容如下:
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit.
.baidu.com TRUE / FALSE 1622390755 BAIDUID 0B4A68D74B0C0E53E5B82AFD9BF9178F:FG=1
.baidu.com TRUE / FALSE 3738338402 BIDUPSID 0B4A68D74B0C0E53471FA6329280FA58
.baidu.com TRUE / FALSE H_PS_PSSID 31262_1438_31325_21127_31110_31596_31673_31464_30823_26350
.baidu.com TRUE / FALSE 3738338402 PSTM 1590854754
www.baidu.com FALSE / FALSE BDSVRTM 0
www.baidu.com FALSE / FALSE BD_HOME 1
另外,LWPCookieJar 同樣可以讀取和保存 Cookie,但是保存的格式和 MozillaCookieJar 不一樣,它會保存成 libwww-perl(LWP)格式的 Cookie 文件。
要保存成 LWP 格式的 Cookie 文件,可以在聲明時就改為:
cookie=http.cookiejar.LWPCookieJar(filename)
此時生成的內容如下:
#LWP-Cookies-2.0
Set-Cookie3: BAIDUID="1F30EEDA35C7A94320275F991CA5B3A5:FG=1"; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2021-05-30 16:06:39Z"; comment=bd; version=0
Set-Cookie3: BIDUPSID=1F30EEDA35C7A9433C97CF6245CBC383; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0
Set-Cookie3: H_PS_PSSID=31626_1440_21124_31069_31254_31594_30841_31673_31464_31715_30823; path="/"; domain=".baidu.com"; path_spec; domain_dot; discard; version=0
Set-Cookie3: PSTM=1590854799; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0
Set-Cookie3: BDSVRTM=11; path="/"; domain="www.baidu.com"; path_spec; discard; version=0
Set-Cookie3: BD_HOME=1; path="/"; domain="www.baidu.com"; path_spec; discard; version=0
由此看來,生成的格式還是有比較大差異的。
那么,生成了 Cookie 文件后,怎樣從文件中讀取并利用呢?
下面我們以 LWPCookieJar 格式為例來看一下:
import urllib.request, http.cookiejar
cookie=http.cookiejar.LWPCookieJar()
cookie.load('cookie.txt', ignore_discard=True, ignore_expires=True)
handler=urllib.request.HTTPCookieProcessor(cookie)
opener=urllib.request.build_opener(handler)
response=opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
可以看到,這里調用 load 方法來讀取本地的 Cookie 文件,獲取到了 Cookie 的內容。不過前提是我們首先生成了 LWPCookieJar 格式的 Cookie,并保存成文件,然后讀取 Cookie 之后使用同樣的方法構建 Handler 和 Opener 即可完成操作。
運行結果正常的話,會輸出百度網頁的源代碼。
通過上面的方法,我們可以實現絕大多數請求功能的設置了。
這便是 urllib 庫中 request 模塊的基本用法,如果想實現更多的功能,可以參考官方文檔的說明:https://docs.python.org/3/library/urllib.request.html#basehandler-objects。
在前一節中,我們了解了請求的發送過程,但是在網絡不好的情況下,如果出現了異常,該怎么辦呢?這時如果不處理這些異常,程序很可能因報錯而終止運行,所以異常處理還是十分有必要的。
urllib 的 error 模塊定義了由 request 模塊產生的異常。如果出現了問題,request 模塊便會拋出 error 模塊中定義的異常。
URLError 類來自 urllib 庫的 error 模塊,它繼承自 OSError 類,是 error 異常模塊的基類,由 request 模塊產生的異常都可以通過捕獲這個類來處理。
它具有一個屬性 reason,即返回錯誤的原因。
下面用一個實例來看一下:
from urllib import request, error
try:
response=request.urlopen('https://cuiqingcai.com/404')
except error.URLError as e:
print(e.reason)
我們打開一個不存在的頁面,照理來說應該會報錯,但是這時我們捕獲了 URLError 這個異常,運行結果如下:
Not Found
程序沒有直接報錯,而是輸出了如上內容,這樣就可以避免程序異常終止,同時異常得到了有效處理。
它是 URLError 的子類,專門用來處理 HTTP 請求錯誤,比如認證請求失敗等。它有如下 3 個屬性。
下面我們用幾個實例來看看:
from urllib import request, error
try:
response=request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:
print(e.reason, e.code, e.headers, sep='\n')
運行結果如下:
Not Found
404
Server: nginx/1.10.3 (Ubuntu)
Date: Sat, 30 May 2020 16:08:42 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Set-Cookie: PHPSESSID=kp1a1b0o3a0pcf688kt73gc780; path=/
Pragma: no-cache
Vary: Cookie
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Link: <https://cuiqingcai.com/wp-json/>; rel="https://api.w.org/"
依然是同樣的網址,這里捕獲了 HTTPError 異常,輸出了 reason、code 和 headers 屬性。
因為 URLError 是 HTTPError 的父類,所以可以先選擇捕獲子類的錯誤,再去捕獲父類的錯誤,所以上述代碼的更好寫法如下:
from urllib import request, error
try:
response=request.urlopen('https://cuiqingcai.com/404')
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')
這樣就可以做到先捕獲 HTTPError,獲取它的錯誤原因、狀態碼、headers 等信息。如果不是 HTTPError 異常,就會捕獲 URLError 異常,輸出錯誤原因。最后,用 else 來處理正常的邏輯。這是一個較好的異常處理寫法。
有時候,reason 屬性返回的不一定是字符串,也可能是一個對象。再看下面的實例:
import socket
import urllib.request
import urllib.error
try:
response=urllib.request.urlopen('https://www.baidu.com', timeout=0.01)
except urllib.error.URLError as e:
print(type(e.reason))
if isinstance(e.reason, socket.timeout):
print('TIME OUT')
這里我們直接設置超時時間來強制拋出 timeout 異常。
運行結果如下:
<class'socket.timeout'>
TIME OUT
可以發現,reason 屬性的結果是 socket.timeout 類。所以,這里我們可以用 isinstance 方法來判斷它的類型,作出更詳細的異常判斷。
本節中,我們講述了 error 模塊的相關用法,通過合理地捕獲異常可以做出更準確的異常判斷,使程序更加穩健。
前面說過,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。本節中,我們介紹一下該模塊中常用的方法來看一下它的便捷之處。
該方法可以實現 URL 的識別和分段,這里先用一個實例來看一下:
from urllib.parse import urlparse
result=urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(result))
print(result)
這里我們利用 urlparse 方法進行了一個 URL 的解析。首先,輸出了解析結果的類型,然后將結果也輸出出來。
運行結果如下:
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
可以看到,返回結果是一個 ParseResult 類型的對象,它包含 6 個部分,分別是 scheme、netloc、path、params、query 和 fragment。
觀察一下該實例的 URL:
https://www.baidu.com/index.html;user?id=5#comment
可以發現,urlparse 方法將其拆分成了 6 個部分。大體觀察可以發現,解析時有特定的分隔符。比如,:// 前面的就是 scheme,代表協議;第一個 / 符號前面便是 netloc,即域名,后面是 path,即訪問路徑;分號;后面是 params,代表參數;問號 ? 后面是查詢條件 query,一般用作 GET 類型的 URL;井號 # 后面是錨點,用于直接定位頁面內部的下拉位置。
所以,可以得出一個標準的鏈接格式,具體如下:
scheme://netloc/path;params?query#fragment
一個標準的 URL 都會符合這個規則,利用 urlparse 方法可以將它拆分開來。
除了這種最基本的解析方式外,urlparse 方法還有其他配置嗎?接下來,看一下它的 API 用法:
urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)
可以看到,它有 3 個參數。
from urllib.parse import urlparse
result=urlparse('www.baidu.com/index.html;user?id=5#comment', scheme='https')
print(result)
運行結果如下:
ParseResult(scheme='https', netloc='', path='www.baidu.com/index.html', params='user', query='id=5', fragment='comment')
可以發現,我們提供的 URL 沒有包含最前面的 scheme 信息,但是通過默認的 scheme 參數,返回的結果是 https。
假設我們帶上了 scheme:
result=urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme='https')
則結果如下:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
可見,scheme 參數只有在 URL 中不包含 scheme 信息時才生效。如果 URL 中有 scheme 信息,就會返回解析出的 scheme。
下面我們用實例來看一下:
from urllib.parse import urlparse
result=urlparse('https://www.baidu.com/index.html;user?id=5#comment', allow_fragments=False)
print(result)
運行結果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5#comment', fragment='')
假設 URL 中不包含 params 和 query,我們再通過實例看一下:
from urllib.parse import urlparse
result=urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result)
運行結果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html#comment', params='', query='', fragment='')
可以發現,當 URL 中不包含 params 和 query 時,fragment 便會被解析為 path 的一部分。
返回結果 ParseResult 實際上是一個元組,我們既可以用索引順序來獲取,也可以用屬性名獲取。示例如下:
from urllib.parse import urlparse
result=urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result.scheme, result[0], result.netloc, result[1], sep='\n')
這里我們分別用索引和屬性名獲取了 scheme 和 netloc,其運行結果如下:
https
https
www.baidu.com
www.baidu.com
可以發現,二者的結果是一致的,兩種方法都可以成功獲取。
有了 urlparse 方法,相應地就有了它的對立方法 urlunparse。它接收的參數是一個可迭代對象,但是它的長度必須是 6,否則會拋出參數數量不足或者過多的問題。先用一個實例看一下:
from urllib.parse import urlunparse
data=['https', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
print(urlunparse(data))
這里參數 data 用了列表類型。當然,你也可以用其他類型,比如元組或者特定的數據結構。
運行結果如下:
https://www.baidu.com/index.html;user?a=6#comment
這樣我們就成功實現了 URL 的構造。
這個方法和 urlparse 方法非常相似,只不過它不再單獨解析 params 這一部分,只返回 5 個結果。上面例子中的 params 會合并到 path 中。示例如下:
from urllib.parse import urlsplit
result=urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result)
運行結果如下:
SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')
可以發現,返回結果是 SplitResult,它其實也是一個元組類型,既可以用屬性獲取值,也可以用索引來獲取。示例如下:
from urllib.parse import urlsplit
result=urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result.scheme, result[0])
運行結果如下:
https https
與 urlunparse 方法類似,它也是將鏈接各個部分組合成完整鏈接的方法,傳入的參數也是一個可迭代對象,例如列表、元組等,唯一的區別是長度必須為 5。示例如下:
from urllib.parse import urlunsplit
data=['https', 'www.baidu.com', 'index.html', 'a=6', 'comment']
print(urlunsplit(data))
運行結果如下:
https://www.baidu.com/index.html?a=6#comment
有了 urlunparse 和 urlunsplit 方法,我們可以完成鏈接的合并,不過前提是必須要有特定長度的對象,鏈接的每一部分都要清晰分開。
此外,生成鏈接還有另一個方法,那就是 urljoin 方法。我們可以提供一個 base_url(基礎鏈接)作為第一個參數,將新的鏈接作為第二個參數,該方法會分析 base_url 的 scheme、netloc 和 path 這 3 個內容并對新鏈接缺失的部分進行補充,最后返回結果。
下面通過幾個實例看一下:
from urllib.parse import urljoin
print(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2'))
print(urljoin('https://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php'))
print(urljoin('https://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))
運行結果如下:
https://www.baidu.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html?question=2
https://cuiqingcai.com/index.php
https://www.baidu.com?category=2#comment
www.baidu.com?category=2#comment
www.baidu.com?category=2
可以發現,base_url 提供了三項內容 scheme、netloc 和 path。如果這 3 項在新的鏈接里不存在,就予以補充;如果新的鏈接存在,就使用新的鏈接的部分。而 base_url 中的 params、query 和 fragment 是不起作用的。
通過 urljoin 方法,我們可以輕松實現鏈接的解析、拼合與生成。
這里我們再介紹一個常用的方法 —— urlencode,它在構造 GET 請求參數的時候非常有用,示例如下:
from urllib.parse import urlencode
params={
'name': 'germey',
'age': 25
}
base_url='https://www.baidu.com?'
url=base_url + urlencode(params)
print(url)
這里首先聲明一個字典來將參數表示出來,然后調用 urlencode 方法將其序列化為 GET 請求參數。
運行結果如下:
https://www.baidu.com?name=germey&age=25
可以看到,參數成功地由字典類型轉化為 GET 請求參數了。
這個方法非常常用。有時為了更加方便地構造參數,我們會事先用字典來表示。要轉化為 URL 的參數時,只需要調用該方法即可。
有了序列化,必然就有反序列化。如果我們有一串 GET 請求參數,利用 parse_qs 方法,就可以將它轉回字典,示例如下:
from urllib.parse import parse_qs
query='name=germey&age=25'
print(parse_qs(query))
運行結果如下:
{'name': ['germey'], 'age': ['25']}
可以看到,這樣就成功轉回為字典類型了。
另外,還有一個 parse_qsl 方法,它用于將參數轉化為元組組成的列表,示例如下:
from urllib.parse import parse_qsl
query='name=germey&age=25'
print(parse_qsl(query))
運行結果如下:
[('name', 'germey'), ('age', '25')]
可以看到,運行結果是一個列表,而列表中的每一個元素都是一個元組,元組的第一個內容是參數名,第二個內容是參數值。
該方法可以將內容轉化為 URL 編碼的格式。URL 中帶有中文參數時,有時可能會導致亂碼的問題,此時可以用這個方法可以將中文字符轉化為 URL 編碼,示例如下:
from urllib.parse import quote
keyword='壁紙'
url='https://www.baidu.com/s?wd=' + quote(keyword)
print(url)
這里我們聲明了一個中文的搜索文字,然后用 quote 方法對其進行 URL 編碼,最后得到的結果如下:
https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8
有了 quote 方法,當然還有 unquote 方法,它可以進行 URL 解碼,示例如下:
from urllib.parse import unquote
url='https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))
這是上面得到的 URL 編碼后的結果,這里利用 unquote 方法還原,結果如下:
https://www.baidu.com/s?wd=壁紙
可以看到,利用 unquote 方法可以方便地實現解碼。
本節中,我們介紹了 parse 模塊的一些常用 URL 處理方法。有了這些方法,我們可以方便地實現 URL 的解析和構造,建議熟練掌握。
利用 urllib 的 robotparser 模塊,我們可以實現網站 Robots 協議的分析。本節中,我們來簡單了解一下該模塊的用法。
Robots 協議也稱作爬蟲協議、機器人協議,它的全名叫作網絡爬蟲排除標準(Robots Exclusion Protocol),用來告訴爬蟲和搜索引擎哪些頁面可以抓取,哪些不可以抓取。它通常是一個叫作 robots.txt 的文本文件,一般放在網站的根目錄下。
當搜索爬蟲訪問一個站點時,它首先會檢查這個站點根目錄下是否存在 robots.txt 文件,如果存在,搜索爬蟲會根據其中定義的爬取范圍來爬取。如果沒有找到這個文件,搜索爬蟲便會訪問所有可直接訪問的頁面。
下面我們看一個 robots.txt 的樣例:
User-agent: *
Disallow: /
Allow: /public/
這實現了對所有搜索爬蟲只允許爬取 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/
只允許某一個爬蟲訪問的代碼如下:
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
這些是 robots.txt 的一些常見寫法。
大家可能會疑惑,爬蟲名是從哪兒來的?為什么就叫這個名?其實它是有固定名字的了,比如百度的就叫作 BaiduSpider。表 2- 列出了一些常見搜索爬蟲的名稱及對應的網站。
表 一些常見搜索爬蟲的名稱及其對應的網站
爬蟲名稱 | 名稱 | 網站 |
BaiduSpider | 百度 | www.baidu.com |
Googlebot | 谷歌 | www.google.com |
360Spider | 360 搜索 | www.so.com |
YodaoBot | 有道 | www.youdao.com |
ia_archiver | Alexa | www.alexa.cn |
Scooter | altavista | www.altavista.com |
Bingbot | 必應 | www.bing.com |
了解 Robots 協議之后,我們就可以使用 robotparser 模塊來解析 robots.txt 了。該模塊提供了一個類 RobotFileParser,它可以根據某網站的 robots.txt 文件來判斷一個爬蟲是否有權限來爬取這個網頁。
該類用起來非常簡單,只需要在構造方法里傳入 robots.txt 的鏈接即可。首先看一下它的聲明:
urllib.robotparser.RobotFileParser(url='')
當然,也可以在聲明時不傳入,默認為空,最后再使用 set_url 方法設置一下即可。
下面列出了這個類常用的幾個方法。
下面我們用實例來看一下:
from urllib.robotparser import RobotFileParser
rp=RobotFileParser()
rp.set_url('https://www.baidu.com/robots.txt')
rp.read()
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))
這里以百度為例,首先創建 RobotFileParser 對象,然后通過 set_url 方法設置了 robots.txt 的鏈接。當然,不用這個方法的話,可以在聲明時直接用如下方法設置:
rp=RobotFileParser('https://www.baidu.com/robots.txt')
接著利用 can_fetch 方法判斷網頁是否可以被抓取。
運行結果如下:
True
True
False
這里同樣可以使用 parse 方法執行讀取和分析,示例如下:
可以看到這里我們利用 Baiduspider 可以抓取百度等首頁以及 homepage 頁面,但是 Googlebot 就不能抓取 homepage 頁面。
打開百度的 robots.txt 文件看下,可以看到如下的信息:
User-agent: Baiduspider
Disallow: /baidu
Disallow: /s?
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
User-agent: Googlebot
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
由此我們可以看到,Baiduspider 沒有限制 homepage 頁面的抓取,而 Googlebot 則限制了 homepage 頁面的抓取。
這里同樣可以使用 parse 方法執行讀取和分析,示例如下:
from urllib.request import urlopen
from urllib.robotparser import RobotFileParser
rp=RobotFileParser()
rp.parse(urlopen('https://www.baidu.com/robots.txt').read().decode('utf-8').split('\n'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))
運行結果一樣:
True
True
False
本節介紹了 robotparser 模塊的基本用法和實例,利用它,我們可以方便地判斷哪些頁面可以抓取,哪些頁面不可以抓取。
本節內容比較多,我們介紹了 urllib 的 request、error、parse、robotparser 模塊的基本用法。這些是一些基礎模塊,其中有一些模塊的實用性還是很強的,比如我們可以利用 parse 模塊來進行 URL 的各種處理。
本節代碼:https://github.com/Python3WebSpider/UrllibTest。
鍵詞:美團 Python Excel
0.程序是針對美團中的美食部分數據按好評排序采集。
要抓取保存的數據為:
商家名 類型 地理位置 評論人數 均價 最低價格
1.首先編寫網頁數據采集函數,使用request采集網頁源碼,具體實現如下
def getHtml(url):
headers=('User-Agent',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11')
opener=urllib.request.build_opener
opener.addheaders=[headers]
htmldata=opener.open(url).read
htmldata=htmldata.decode('utf-8')
return htmldata
2.根據網頁源碼解析獲取已上線城市的url
class GetCityUrl(HTMLParser):
part=('gaevent','changecity/build')
urldic={}
def handle_starttag(self, tag, attrs):
if tag=='a' and (self.part in attrs):
for att,value in attrs:
if att=='href':
self.urldic.__setitem__(value, value+'/category/meishi/all/rating')
def getUrl(self):
return self.urldic
3.獲取分頁url
class GetPages(HTMLParser):
pagelist=list
temphref=str
flg=0
initurl=str
def setInitUrl(self,url):
self.initurl=url
def handle_starttag(self, tag, attrs):
if tag=='a':
for attr,value in attrs:
if attr=='href' and ('page' in value):
self.temphref=self.initurl + value
if self.temphref not in self.pagelist:
self.pagelist.append(self.temphref)
def getList(self):
return self.pagelist
4.解析網頁源碼 獲取有效信息
class MyHTMLParser(HTMLParser):
tempstr=str
divsum=int
def handle_starttag(self, tag, attrs):
if tag=='div':
for attr,value in attrs:
if attr=='class' and value.find('poi-tile-nodeal')!=-1:
self.tempstr=''
self.divsum=0
def handle_data(self, data):
if(data.isspace==False):
data=data.replace('·', '·')
if data=='¥':
if '¥' not in self.tempstr:
self.tempstr+='無' +'\t'
self.tempstr+=data
elif data=='¥':
if '¥' not in self.tempstr:
self.tempstr+='無' +'\t'
self.tempstr+='¥'
elif data=='人評價':
self.tempstr=self.tempstr[0:-1]+data+'\t'
elif data=='人均 ':
self.tempstr+='人均'
elif data[0]=='起':
self.tempstr=self.tempstr[0:-1]+'起'
else:
self.tempstr+=data+'\t'
def handle_endtag(self, tag):
if tag=='div':
self.divsum+=1
if self.divsum==6:
if (self.tempstr.find('¥'))!=-1:
if (re.split(r'\t', self.tempstr).__len__)==5:
teststr=str
flg=0
for stmp in re.split(r'\t',self.tempstr):
if flg==2:
teststr+='無位置信息'+'\t'
teststr+=stmp+'\t'
flg+=1
self.tempstr=teststr
if (re.split(r'\t', self.tempstr).__len__)==6:
arraystr.append(self.tempstr)
self.divsum=0
self.tempstr=''
5.將信息存放于Excel中
def SaveExcel(listdata):
head=['商家名','類型','地理位置','評論人數','均價','最低價格']
wbk=xlwt.Workbook
sheet1=wbk.add_sheet("sheet1")
ii=0
for testhand in head:
sheet1.write(0,ii,testhand)
ii+=1
i=1
j=0
for stt in listdata:
j=0
lis=re.split(r'\t',stt)
for ls in lis:
sheet1.write(i,j,ls)
j=j+1
i+=1
wbk.save('test.xls')
以下是Excel中的數據:
來自:數據挖掘入門與實戰
↓↓↓【精彩推薦】
1、大咖看過來,數據分析網專欄作家招募!!
2、加入大數據交流QQ群,開啟數據江湖!!
3、有緣人看過來,數據分析網志愿者招募!!
↓↓↓【點擊閱讀原文訪問數據分析網】
鏈接在現代網頁設計中發揮著不可或缺的作用。HTML中,href屬性在anchor標簽中扮演著關鍵角色,使得用戶可以輕松地跳轉到其他頁面或特定部分。在本文中,我們將探討href屬性的基本使用方法以及一些高級技巧,幫助您提升網站用戶體驗。
HTML中,anchor標簽用于創建超鏈接,其基本結構如下:
<a href="鏈接地址">鏈接文本</a>
其中,href屬性指定了跳轉的目標地址,而鏈接文本則是用戶在頁面上看到的可點擊的文本。
絕對路徑和相對路徑是href屬性中常見的兩種取值類型。
外部鏈接指向不同域名的頁面,而內部鏈接則是指向同一域名內的頁面或部分。在創建鏈接時,確保使用正確的協議(如http://或https://)以避免鏈接無法正常跳轉。
通過使用target屬性,您可以控制超鏈接的打開方式。常見的取值有:
rel屬性用于在超鏈接中添加關聯信息,如:
為了提升網站的訪問性和 SEO 排名,請確保鏈接文本清晰、描述性,避免使用過于短的單詞(如 "點擊這里")。此外,使用有意義的 URL 以便搜索引擎更好地理解您的網頁內容。
通過CSS,您可以對超鏈接進行樣式定義,使其與網站設計相一致。例如,更改鏈接的顏色、字體、下劃線等。
掌握href屬性的使用方法和技巧,您可以創建更加易于導航且訪問性較好的網站。在提升用戶體驗的同時,還能夠為搜索引擎提供更多關于網頁內容的信息,從而提高網站的 SEO 排名。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。