上一次學習 Python 網路爬蟲已經是三年前的事了 (2018), 那時測試完內建模組 urllib 之後繼續測試較高階的 requests 套件, 但沒完成就去忙別的事了. 最近在讀 "Python 程式超入門 (旗標, 鎌田正浩, 2016)" 這本書, 書末有簡單談到使用 requests 擷取網頁資訊, 所以又重新進行測試, 結果紀錄如下.
以下的測試參考了下列 Python 網頁擷取相關書籍 :
- Python 網路爬蟲實戰 (松崗, 胡松濤)
- Python 自動化的樂趣 (碁峰, AL Sweigart)
- Python 程式設計實務 (博碩, 何敏煌)
- Python 初學特訓班 (碁峰, 文淵閣工作室)
- 網站擷取 : 使用 Python (碁峰, Ryan Mitchell)
- Python 入門邁向高手之路-王者歸來 (深石, 洪錦魁)
- Python 程式設計入門指南 (碁峰, 蔡明志譯)
- Web Scraping with Python (Packt, Richard Lawson)
- Python Web Scraping Cookbook (Packt, Michael Heydt)
- Automate the Boring Stuff with Python (Starch, AL Sweigart)
- Introduction to Programming Using Python (Pearson, Y. Daniel Liang)
- Python Web Scraping : Fetching data from the web (Packt, Katharine Jarmul)
- Data Wrangling with Python (O'Reilly, Jacqueline Kazil & Katharine Jarmul)
- Web Scraping with Python : Collecting Data from the Modern Web (O'Reilly, Ryan Mitchell)
第三方套件 requests 是比 Python 3 內建的 urllib 更方便好用的爬蟲函式庫, 參考說明文件 :
# http://docs.python-requests.org/en/master/ (英文)
# http://cn.python-requests.org/zh_CN/latest/ (中文)
# https://2.python-requests.org//zh_CN/latest/user/quickstart.html (中文)
# http://docs.python-requests.org/en/master/api/?highlight=codes (API)
一. 安裝 requests 模組 :
使用 requests 前必須先安裝, 指令如下 :
pip3 install -U requests
安裝好 requests 後即可用 import 匯入 :
import requests
二. requests 實作的 6 個 HTTP 方法 :
requests 模組實作了 HTTP/1.1 定義之八種方法中的六種 : GET, POST, PUT, DELETE, HEAD, OPTIONS (TRACE 與 CONNECT 這兩個方法未實作), 其相對應的物件方法如下 :
HTTP 方法 | 物件方法 | 說明 |
GET | requests.get(url [, params]) | 向指定資源提交請求, params 為參數字典 |
POST | requests.post(url [, data]) | 向指定資源提交請求, data 為參數字典 |
PUT | requests.put(url [, data]) | 向指定資源提供最新內容, data 為參數字典 |
DELETE | requests.delete(url) | 請求刪除指定之資源 |
HEAD | requests.head(url) | 請求提供資源之回應標頭 (不含內容) |
OPTIONS | requests.options(url) | 請求伺服器提供資源可用之功能選項 |
呼叫 requests 的這六個方法會傳回一個 Response 物件. 其中 GET 與 POST 是最常用的方法, 主要的差別是 GET 如果有提交參數, 其參數是放在標頭中傳送 (公開), 而 POST 則是放在內容中傳送 (隱密). POST 請求可能會導致新資源之建立或既有資源之刪除, GET 則不適合進行這樣的操作.
OPTIONS 通常是用在要對資源採取具體請求之前先向伺服器查詢該資源可用之功能選項, 以便決定要對此資源進行何種請求. 若 OPTIONS 方法請求之 URL 為星號 *, 表示想要查詢整個伺服器性能 (此為駭客常用方法).
PUT 是向指定之資源更新最新內容 (例如 UPDATE 或 ADD 動作), 其相對應之方法為 DELETE, 此為請求伺服器刪除指定之資源. 需注意的是, DELETE 方法即使回應 200 OK 並非表示伺服器已經依照要求刪除該資源, 而僅僅表示收到該請求而已.
參考 :
# HTTP的請求方法OPTIONS
# HTTP/1.1協議中共定義了八種方法
以下參考 requests 官網教學內容進行測試, 它使用 http://httpbin.org 網站進行測試, 此網站提供了除 CONNECT 與 TRACE 外的六種 HTTP 方法之資源 :
HTTP 方法 | 資源網址 |
GET | http://httpbin.org/get |
POST | http://httpbin.org/post |
PUT | http://httpbin.org/put |
DELETE | http://httpbin.org/delete |
HEAD | http://httpbin.org/head |
OPTIONS | http://httpbin.org/options |
參考 :
# http://docs.python-requests.org/en/master/user/quickstart/#make-a-request
三. Response 物件的屬性與方法 :
呼叫 requests 模組所定義的六種 HTTP 方法會傳回一個 Response 物件, 它包裹了伺服器回應訊息中的資訊, 可透過如下屬性與方法查詢回應內容 :
Response 物件 | 說明 |
url | 要求之資源 URL 位址 |
content | 回應訊息之內容 (bytes) |
text | 回應訊息之內容字串 (str) |
raw | 原始回應訊息串流 (bytes) |
status_code | 回應狀態 (int) |
encoding | 回應訊息編碼, 例如 utf-8, big5, iso-8859-1 |
headers | 回應訊息之標頭 (dict) |
cookies | 回應訊息中的 cookies (dict) |
history | 請求歷史 (list) |
json() | 將回應訊息進行 JSON 解碼後傳回 (dict) |
rasise_for_status() | 檢查是否有例外發生, 有則拋出例外 |
例如對 http://httpbin.org/get 資源提出 GET 要求的話, 測試結果如下 :
>>> import requests
>>> r=requests.get('http://httpbin.org/get')
>>> r.raise_for_status() #200 OK 傳回 None, 否則拋出例外
>>> type(r)
<class 'requests.models.Response'> #傳回值為 Response 物件
>>> r
<Response [200]> #只顯示物件名稱與回應狀態 200 OK
>>> r.status_code
200
>>> type(r.status_code) #status_code 為 int 整數
<class 'int'>
>>> requests.codes
<lookup 'status_codes'>
>>> type(requests.codes)
<class 'requests.structures.LookupDict'>
其中 status_code 屬性為一個整數, 表示 HTTP 回應的狀態, 正常為 200 (OK), 否則表示下載網頁時出現錯誤或異常, raise_for_status() 方法就是用來檢查回應狀態, 通常在下載網頁後緊接著就呼叫此方法, 如果是 200 OK 傳回 None; 否則就會拋出例外並終止程式. 常見的錯誤狀態碼例如 400 (Bad request), 404 (Not found), 500 (Server internal error) 等, 參考 :
# Wiki : HTTP 狀態碼
requests 模組將 HTTP 回應狀態碼定義在 requests.codes 物件內, 此物件為 LookupDict 類別之物件, 以字典形式儲存狀態碼之布林值. 若 requests.codes.ok 之值為 true 表示回應狀態為 200 OK, 可用來判別要求之回應是否正常, 例如 :
if (r.status_code == requests.codes.ok):
print("Response : OK")
當然如果記得狀態碼也可以直接用狀態碼 :
if (r.status_code == 200):
print("Response : OK")
由於 requests.codes 為 LookupDict 類別之物件, 以字典形式儲存狀態碼, 因此 requests.codes.ok 也可以用 requests.codes["ok"] 來存取, 其他狀態碼也是如此, 參考 :
# Does requests.codes.ok include a 304?
例如 :
>>> requests.codes.ok
200
>>> requests.codes['ok']
200
>>> requests.codes.not_modified
304
>>> requests.codes['not_modified']
304
>>> requests.codes.bad_request
400
>>> requests.codes['bad_request']
400
>>> requests.codes.not_found
404
>>> requests.codes['not_found']
404
>>> requests.codes.internal_server_error
500
>>> requests.codes['internal_server_error']
500
狀態屬性之命名原則為全小寫, 而且以底線相連. 例如最常見的錯誤為 404 網頁不存在 (Not found), 其 key 即為 not_found. 關於回應狀態. 由於網路屬於一種 IO, 因此下載網頁時可能會發生錯誤, 例如下載不存在的 網頁時會出現 Not found 錯誤 :
>>> r=requests.get('http://tw.yahoo.com/xyz.htm') #不存在
>>> r.raise_for_status() #檢查回應狀態
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Users\Tony Huang\.thonny\BundledPython36\lib\site-packages\requests\models.py", line 939, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://tw.yahoo.com/xyz.htm
這個密密麻麻的例外訊息會讓使用者納悶, 理想的做法是將 raise_for_status() 等指令放在 try except 結構中攔截可能的例外, 讓程式在被規範的情形下終止, 同時提供使用者較友善之錯誤訊息, 例如 :
import requests
try:
print("Download web page ...")
r=requests.get('http://tw.yahoo.com/xyz.htm')
r.raise_for_status()
except Exception as e:
print(e)
Response 物件最常用的是用來取得回應訊息內容的 text 屬性與 json() 方法. text 屬性以字串形式儲存回應訊息之內容 (即 HTTP 之 BODY 部分); 而 json() 則傳回 dict 型態的 JSON 編碼 回應內容. 例如 :
>>> import requests
>>> r=requests.get('http://httpbin.org/get')
>>> r.text #顯示回應訊息內容 (字串)
'{"args":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Connection":"close","Host":"httpbin.org","User-Agent":"python-requests/2.18.1"},"origin":"114.27.103.62","url":"http://httpbin.org/get"}\n'
>>> r.json() #以 JSON 型態顯示回應訊息內容 (字典)
>>> type(r.text) #屬性 text 型態為字串
<class 'str'>
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.18.1'}, 'origin': '114.27.103.62', 'url': 'http://httpbin.org/get'}
>>> type(r.json())
<class 'dict'>
可見 json() 的傳回值與 text 屬性值內容一樣, 但型態不同, 前者是 dict 後者是 str. 注意, 回應訊息的內容必須是 JSON 格式, 否則呼叫 json() 時將拋出 ValueError 例外, 例如下面這個 PHP 爬蟲測試網頁的回應是單純的 HTML 碼, 並非 JSON 格式之資料, 呼叫 json() 後就會因為解碼失敗而拋出例外 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.text #回應內容為 HTML 網頁, 非 JSON 格式
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> r.json() #回應內容不是 JSON 格式拋出例外
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Users\Tony Huang\.thonny\BundledPython36\lib\site-packages\requests\models.py", line 896, in json
return complexjson.loads(self.text, **kwargs)
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\__init__.py", line 354, in loads
return _default_decoder.decode(s)
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\decoder.py", line 339, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\decoder.py", line 357, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
與 raise_for_status() 一樣, 可能會拋出例外的 json() 應該放在 try ... except 中較好, 例如 :
import requests
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
r=requests.get(url)
try:
r.json() #回應訊息不是 JSON 格式, 會拋出例外
except Exception as e:
print(e)
除了 text 屬性外, content 屬性也可以取得回應訊息之內容, 差別是 content 的類型是 bytes, 而 text 的類型是 str, 例如 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.text
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> type(r.text)
<class 'str'>
>>> r.content
b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> type(r.content)
<class 'bytes'>
另外 raw 屬性也是儲存回應訊息的內容, 不過它儲存的是原始的 HTTP 回應訊息串流 (Stream), 必須呼叫 read() 方法才能將回應訊息讀出來. 注意, 在呼叫 HTTP 方法如 get() 或 post() 時須傳入 stream=True 參數 (預設為 False) 才能取得 raw 屬性, 否則讀取不到資料, 例如 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url, stream=True)
>>> r.raw
<urllib3.response.HTTPResponse object at 0x0393D690>
>>> type(r.raw)
<class 'urllib3.response.HTTPResponse'>
>>> r.raw.read(10) #從原始回應讀取 10 個 bytes, 並從緩衝區刪除
b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'
>>> r.raw.read(20) #從原始回應讀取 20 個 bytes, 並從緩衝區刪除
b'-\x8e\xc1\n\x830\x10D\xcf\x15\xfc\x87\xd5sk\n\xed1x\xa8'
>>> r.raw.read() ##從原始回應讀取剩餘的全部 bytes, 並從緩衝區刪除
b'\n\n\xb6\xf5`)=F\x13k`\x9b@\x12\x11\xff\xbe\x11=\xcd\xb0\xf3\x98Y\x1a\xe5\xcf\xac\xfd4\x05\x94\xed\xbd\x86\xe6u\xab\xab\x0c\xe2\x13!\xefKFH\xde\xe6[pM\xce\xd0\x1a\xa6\xactR+\x86\x84\x14\x8f8\r\x830\xa0\xa3\xfba\xba\xaa`\xdc\xeb\x81:\xe9P\xa4\xa5@\xd4G\x98\xb5A\x1eQ\xb2\x1d=Fv\xce\xdbN\xf3\xc5\xbbL\xab\xafanB\xb6v\xdb\x08\xaa\x01\x16=A\xcf\x14\x18\x0f\x83\x1b\xa5=\x02\xed\x8c\x87\xd7\xc0N}/\xac\x1d&\xc4\x05\xb8\x9e\x15j\xc6\xc5\xc6\xc1 Q$\xeb\xce\xde\xee\x07\xb7\x07\xffgb\xaeY\xeb\x00\x00\x00'
與 text, content 屬性不同的是, raw 是串流資料, 讀取後就會自緩衝器中刪除, 不像 text 與 content 那樣會一直存在.
參考 :
# How to get the raw content of a response in requests with Python?
屬性 url 儲存所要求之資源網址; 而屬性 encoding 則是儲存回應網頁之編碼 (requests 通常會自動判別網頁的編碼, 但如果因編碼錯誤而出現亂碼時, 最好根據網頁中 meta 標籤的編碼手動設定 encoding 屬性值來指定正確之編碼), 例如 Yahoo 股市股利政策網頁編碼為 Big5 :
>>> url="http://tw.stock.yahoo.com/d/s/dividend_2330.html"
>>> r=requests.get(url)
>>> r.url #要求之網頁網址
'https://tw.stock.yahoo.com/d/s/dividend_2330.html'
>>> r.encoding #編碼格式
'Big5'
>>> r.cookies
<RequestsCookieJar[]>
但並非每一個網頁都會在 head 標籤內註明編碼格式, 其 encoding 即為 None, 例如 :
>>> r=requests.get('http://httpbin.org/get')
>>> r.encoding #此網頁無 encoding
>>> r.url
'http://httpbin.org/get'
headers 屬性以字典 (dict) 的形式儲存回應訊息之標頭 (header), 每一個參數可用 key 或呼叫 get() 方法存取, 例如下列網頁的標頭顯示內容型態 (Content-Type) 為 JSON 格式 :
>>> r=requests.get('http://httpbin.org/get')
>>> r.headers
{'Connection': 'keep-alive', 'Server': 'gunicorn/19.8.1', 'Date': 'Fri, 29 Jun 2018 08:51:21 GMT', 'Content-Type': 'application/json', 'Content-Length': '208', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true', 'Via': '1.1 vegur'}
>>> r.headers['Content-Type']
'application/json'
>>> r.headers.get('Content-Type')
'application/json'
而下列網頁標頭顯示內容型態 (Content-Type) 為 HTML 格式 :
>>>
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.headers
{'Date': 'Fri, 29 Jun 2018 08:52:00 GMT', 'Server': 'Apache', 'Accept-Ranges': 'bytes', 'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'Content-Length': '196', 'Keep-Alive': 'timeout=5, max=100', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html'}
四. 將下載的網頁存成檔案 :
用 requests 的 get() 或 post() 所下載的網頁是以 Response 物件的型態儲存, 如果要儲存到本機檔案系統中的話可以使用 Response 物件的 iter_content(chunksize) 方法將回應內容以 byte 為單位讀取後呼叫 write() 寫入本地檔案中 (要用 'wb' 模式), 參考 :
# http://docs.python-requests.org/en/master/api/#requests.Response.iter_content
iter_content(chunksize) 的參數 chunksize 指定每次從回應訊息內容讀取之 bytes 數, 預設為 1 byte, 數字越大占用記憶體越多. 在 "Python 入門邁向高手之路-王者歸來" 這本書的 21-2-7 節有存成檔案的範例, 改寫如下 :
import requests
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
file="hello_world.txt" #欲儲存之檔案名稱
try:
r=requests.get(url)
r.raise_for_status()
print("網頁下載 OK")
with open(file, "wb") as fobj: #以 wb 模式開啟檔案為 fobj 物件
for data in r.iter_content(50): #每次讀取 50 bytes
size=fobj.write(data) #寫入檔案
print(size)
print("儲存網頁檔 %s OK" %file)
except Exception as e:
print(e)
在 Thonny 執行結果如下 :
Python 3.6.4
>>> %Run status.py
網頁下載 OK
50
50
50
50
35
儲存網頁檔 hello_world.txt OK
此程式會將下載的網頁儲存為 hello_world.txt 於程式目前目錄下, 每次讀取 50 bytes, 原網頁 235 bytes 便分成 5 次讀取. 開啟 hello_world.txt 內容如下 :
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
Congratulations! If you can read this, <br>
you successfully downloaded this file.
</body>
</html>
儲存網頁時檔名可以包含路徑, 但要注意, 如果在 Windows 中要寫入指定之目錄例如 D:\Python\test 下的話, 路徑分隔符號需跳脫, 例如 :
file="D:\\Python\\test\\hello_world.txt"
>>> import requests
>>> r=requests.get('http://httpbin.org/get')
>>> r.raise_for_status() #200 OK 傳回 None, 否則拋出例外
>>> type(r)
<class 'requests.models.Response'> #傳回值為 Response 物件
>>> r
<Response [200]> #只顯示物件名稱與回應狀態 200 OK
>>> r.status_code
200
>>> type(r.status_code) #status_code 為 int 整數
<class 'int'>
>>> requests.codes
<lookup 'status_codes'>
>>> type(requests.codes)
<class 'requests.structures.LookupDict'>
其中 status_code 屬性為一個整數, 表示 HTTP 回應的狀態, 正常為 200 (OK), 否則表示下載網頁時出現錯誤或異常, raise_for_status() 方法就是用來檢查回應狀態, 通常在下載網頁後緊接著就呼叫此方法, 如果是 200 OK 傳回 None; 否則就會拋出例外並終止程式. 常見的錯誤狀態碼例如 400 (Bad request), 404 (Not found), 500 (Server internal error) 等, 參考 :
# Wiki : HTTP 狀態碼
requests 模組將 HTTP 回應狀態碼定義在 requests.codes 物件內, 此物件為 LookupDict 類別之物件, 以字典形式儲存狀態碼之布林值. 若 requests.codes.ok 之值為 true 表示回應狀態為 200 OK, 可用來判別要求之回應是否正常, 例如 :
if (r.status_code == requests.codes.ok):
print("Response : OK")
當然如果記得狀態碼也可以直接用狀態碼 :
if (r.status_code == 200):
print("Response : OK")
由於 requests.codes 為 LookupDict 類別之物件, 以字典形式儲存狀態碼, 因此 requests.codes.ok 也可以用 requests.codes["ok"] 來存取, 其他狀態碼也是如此, 參考 :
# Does requests.codes.ok include a 304?
例如 :
>>> requests.codes.ok
200
>>> requests.codes['ok']
200
>>> requests.codes.not_modified
304
>>> requests.codes['not_modified']
304
>>> requests.codes.bad_request
400
>>> requests.codes['bad_request']
400
>>> requests.codes.not_found
404
>>> requests.codes['not_found']
404
>>> requests.codes.internal_server_error
500
>>> requests.codes['internal_server_error']
500
狀態屬性之命名原則為全小寫, 而且以底線相連. 例如最常見的錯誤為 404 網頁不存在 (Not found), 其 key 即為 not_found. 關於回應狀態. 由於網路屬於一種 IO, 因此下載網頁時可能會發生錯誤, 例如下載不存在的 網頁時會出現 Not found 錯誤 :
>>> r=requests.get('http://tw.yahoo.com/xyz.htm') #不存在
>>> r.raise_for_status() #檢查回應狀態
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Users\Tony Huang\.thonny\BundledPython36\lib\site-packages\requests\models.py", line 939, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://tw.yahoo.com/xyz.htm
這個密密麻麻的例外訊息會讓使用者納悶, 理想的做法是將 raise_for_status() 等指令放在 try except 結構中攔截可能的例外, 讓程式在被規範的情形下終止, 同時提供使用者較友善之錯誤訊息, 例如 :
import requests
try:
print("Download web page ...")
r=requests.get('http://tw.yahoo.com/xyz.htm')
r.raise_for_status()
except Exception as e:
print(e)
Response 物件最常用的是用來取得回應訊息內容的 text 屬性與 json() 方法. text 屬性以字串形式儲存回應訊息之內容 (即 HTTP 之 BODY 部分); 而 json() 則傳回 dict 型態的 JSON 編碼 回應內容. 例如 :
>>> import requests
>>> r=requests.get('http://httpbin.org/get')
>>> r.text #顯示回應訊息內容 (字串)
'{"args":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Connection":"close","Host":"httpbin.org","User-Agent":"python-requests/2.18.1"},"origin":"114.27.103.62","url":"http://httpbin.org/get"}\n'
>>> r.json() #以 JSON 型態顯示回應訊息內容 (字典)
>>> type(r.text) #屬性 text 型態為字串
<class 'str'>
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.18.1'}, 'origin': '114.27.103.62', 'url': 'http://httpbin.org/get'}
>>> type(r.json())
<class 'dict'>
可見 json() 的傳回值與 text 屬性值內容一樣, 但型態不同, 前者是 dict 後者是 str. 注意, 回應訊息的內容必須是 JSON 格式, 否則呼叫 json() 時將拋出 ValueError 例外, 例如下面這個 PHP 爬蟲測試網頁的回應是單純的 HTML 碼, 並非 JSON 格式之資料, 呼叫 json() 後就會因為解碼失敗而拋出例外 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.text #回應內容為 HTML 網頁, 非 JSON 格式
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> r.json() #回應內容不是 JSON 格式拋出例外
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Users\Tony Huang\.thonny\BundledPython36\lib\site-packages\requests\models.py", line 896, in json
return complexjson.loads(self.text, **kwargs)
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\__init__.py", line 354, in loads
return _default_decoder.decode(s)
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\decoder.py", line 339, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "C:\Users\Tony Huang\AppData\Local\Programs\Thonny\lib\json\decoder.py", line 357, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
與 raise_for_status() 一樣, 可能會拋出例外的 json() 應該放在 try ... except 中較好, 例如 :
import requests
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
r=requests.get(url)
try:
r.json() #回應訊息不是 JSON 格式, 會拋出例外
except Exception as e:
print(e)
除了 text 屬性外, content 屬性也可以取得回應訊息之內容, 差別是 content 的類型是 bytes, 而 text 的類型是 str, 例如 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.text
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> type(r.text)
<class 'str'>
>>> r.content
b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r\n\r\n<html>\r\n<head>\r\n\t<title>Hello, world!</title>\r\n</head>\r\n\r\n<body>\r\nCongratulations! If you can read this, <br>\r\nyou successfully downloaded this file.\r\n</body>\r\n</html>\r\n'
>>> type(r.content)
<class 'bytes'>
另外 raw 屬性也是儲存回應訊息的內容, 不過它儲存的是原始的 HTTP 回應訊息串流 (Stream), 必須呼叫 read() 方法才能將回應訊息讀出來. 注意, 在呼叫 HTTP 方法如 get() 或 post() 時須傳入 stream=True 參數 (預設為 False) 才能取得 raw 屬性, 否則讀取不到資料, 例如 :
>>> import requests
>>> url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url, stream=True)
>>> r.raw
<urllib3.response.HTTPResponse object at 0x0393D690>
>>> type(r.raw)
<class 'urllib3.response.HTTPResponse'>
>>> r.raw.read(10) #從原始回應讀取 10 個 bytes, 並從緩衝區刪除
b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'
>>> r.raw.read(20) #從原始回應讀取 20 個 bytes, 並從緩衝區刪除
b'-\x8e\xc1\n\x830\x10D\xcf\x15\xfc\x87\xd5sk\n\xed1x\xa8'
>>> r.raw.read() ##從原始回應讀取剩餘的全部 bytes, 並從緩衝區刪除
b'\n\n\xb6\xf5`)=F\x13k`\x9b@\x12\x11\xff\xbe\x11=\xcd\xb0\xf3\x98Y\x1a\xe5\xcf\xac\xfd4\x05\x94\xed\xbd\x86\xe6u\xab\xab\x0c\xe2\x13!\xefKFH\xde\xe6[pM\xce\xd0\x1a\xa6\xactR+\x86\x84\x14\x8f8\r\x830\xa0\xa3\xfba\xba\xaa`\xdc\xeb\x81:\xe9P\xa4\xa5@\xd4G\x98\xb5A\x1eQ\xb2\x1d=Fv\xce\xdbN\xf3\xc5\xbbL\xab\xafanB\xb6v\xdb\x08\xaa\x01\x16=A\xcf\x14\x18\x0f\x83\x1b\xa5=\x02\xed\x8c\x87\xd7\xc0N}/\xac\x1d&\xc4\x05\xb8\x9e\x15j\xc6\xc5\xc6\xc1 Q$\xeb\xce\xde\xee\x07\xb7\x07\xffgb\xaeY\xeb\x00\x00\x00'
與 text, content 屬性不同的是, raw 是串流資料, 讀取後就會自緩衝器中刪除, 不像 text 與 content 那樣會一直存在.
參考 :
# How to get the raw content of a response in requests with Python?
屬性 url 儲存所要求之資源網址; 而屬性 encoding 則是儲存回應網頁之編碼 (requests 通常會自動判別網頁的編碼, 但如果因編碼錯誤而出現亂碼時, 最好根據網頁中 meta 標籤的編碼手動設定 encoding 屬性值來指定正確之編碼), 例如 Yahoo 股市股利政策網頁編碼為 Big5 :
>>> url="http://tw.stock.yahoo.com/d/s/dividend_2330.html"
>>> r=requests.get(url)
>>> r.url #要求之網頁網址
'https://tw.stock.yahoo.com/d/s/dividend_2330.html'
>>> r.encoding #編碼格式
'Big5'
>>> r.cookies
<RequestsCookieJar[]>
但並非每一個網頁都會在 head 標籤內註明編碼格式, 其 encoding 即為 None, 例如 :
>>> r=requests.get('http://httpbin.org/get')
>>> r.encoding #此網頁無 encoding
>>> r.url
'http://httpbin.org/get'
headers 屬性以字典 (dict) 的形式儲存回應訊息之標頭 (header), 每一個參數可用 key 或呼叫 get() 方法存取, 例如下列網頁的標頭顯示內容型態 (Content-Type) 為 JSON 格式 :
>>> r=requests.get('http://httpbin.org/get')
>>> r.headers
{'Connection': 'keep-alive', 'Server': 'gunicorn/19.8.1', 'Date': 'Fri, 29 Jun 2018 08:51:21 GMT', 'Content-Type': 'application/json', 'Content-Length': '208', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true', 'Via': '1.1 vegur'}
>>> r.headers['Content-Type']
'application/json'
>>> r.headers.get('Content-Type')
'application/json'
而下列網頁標頭顯示內容型態 (Content-Type) 為 HTML 格式 :
>>>
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
>>> r=requests.get(url)
>>> r.headers
{'Date': 'Fri, 29 Jun 2018 08:52:00 GMT', 'Server': 'Apache', 'Accept-Ranges': 'bytes', 'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'Content-Length': '196', 'Keep-Alive': 'timeout=5, max=100', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html'}
四. 將下載的網頁存成檔案 :
用 requests 的 get() 或 post() 所下載的網頁是以 Response 物件的型態儲存, 如果要儲存到本機檔案系統中的話可以使用 Response 物件的 iter_content(chunksize) 方法將回應內容以 byte 為單位讀取後呼叫 write() 寫入本地檔案中 (要用 'wb' 模式), 參考 :
# http://docs.python-requests.org/en/master/api/#requests.Response.iter_content
iter_content(chunksize) 的參數 chunksize 指定每次從回應訊息內容讀取之 bytes 數, 預設為 1 byte, 數字越大占用記憶體越多. 在 "Python 入門邁向高手之路-王者歸來" 這本書的 21-2-7 節有存成檔案的範例, 改寫如下 :
import requests
url="http://www.webbotsspidersscreenscrapers.com/hello_world.html"
file="hello_world.txt" #欲儲存之檔案名稱
try:
r=requests.get(url)
r.raise_for_status()
print("網頁下載 OK")
with open(file, "wb") as fobj: #以 wb 模式開啟檔案為 fobj 物件
for data in r.iter_content(50): #每次讀取 50 bytes
size=fobj.write(data) #寫入檔案
print(size)
print("儲存網頁檔 %s OK" %file)
except Exception as e:
print(e)
在 Thonny 執行結果如下 :
Python 3.6.4
>>> %Run status.py
網頁下載 OK
50
50
50
50
35
儲存網頁檔 hello_world.txt OK
此程式會將下載的網頁儲存為 hello_world.txt 於程式目前目錄下, 每次讀取 50 bytes, 原網頁 235 bytes 便分成 5 次讀取. 開啟 hello_world.txt 內容如下 :
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
Congratulations! If you can read this, <br>
you successfully downloaded this file.
</body>
</html>
儲存網頁時檔名可以包含路徑, 但要注意, 如果在 Windows 中要寫入指定之目錄例如 D:\Python\test 下的話, 路徑分隔符號需跳脫, 例如 :
file="D:\\Python\\test\\hello_world.txt"
參考 :
2024-05-10 補充 :
如果要將抓下來的網頁儲存為 html 檔案其實不用這麼麻煩, 直接用 f.write(r.text) 即可 :
>>> with open('test.htm', "w", encoding='utf-8') as f:
f.write(r.text)
print("儲存網頁檔 OK")
此處存檔時順便將編碼格式改成 utf-8 (如果原本是 MS950 這樣做是 OK 的).
五. 偽裝成瀏覽器避免被阻擋的方法 : (2024-04-11 補充)
有些網頁伺服器為了避免爬蟲程式頻繁的擷取流量造成負荷過重, 影響一般瀏覽器用戶的正常使用, 會在伺服器上檢查 HTTP 請求標頭裡面的 User-Agent (使用者代理) 參數, 如果值為瀏覽器版本說明就放行, 否則就拒絕連線. 解決此種阻擋的方法是在呼叫 requests.get() 或 requests.post() 時傳入攜帶一個 User-Agent 的 headers 參數 (其值為一個字典) 即可.
User-Agent 的值視瀏覽器而定, 以 Chrome 瀏覽器為例, 只要在網址列輸入 chrome://version/ 就會顯示目前所使用之 Chrome 的使用者代理 :
可見我目前的 Chrome 使用者代理為 :
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
將此值放入鍵為 User-Agent 的字典中 :
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'}
然後在呼叫 get() 或 post() 傳給 headers 參數即可 :
response=requests.get(url, headers=headers)
如果是 Edge 瀏覽器, 查詢 User-Agent 的方法是在網頁上按滑鼠右鍵, 點選最底下的 "檢查" :
在開啟的視窗中, 按 "網路" > "全部", 然後點選左下框中的任一個項目, 這時 User-Agent 就會顯示在右下角 :
目前的值為 :
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
可見與 Chrome 相同.
其實利用 httpbin.org 網站馬上就能查到瀏覽器的 User-Agent :
不過在寫爬蟲程式時更方便的方法是使用第三方的 fake_useragent 套件, 它可以隨機自動產生各瀏覽器的 User-Agent, 支援 Chrome, Edge, Firefox, 與 Safari 共四種瀏覽器.
安裝指令如下 :
pip install fake_useragent
使用時只要從 fake_useragent 匯入 UserAgent 類別, 然後呼叫其建構式 UserAgent() 即可, 它會傳回一個 FakeUserAgent 物件 :
>>> from fake_useragent import UserAgent
>>> ua=UserAgent()
>>> type(ua)
<class 'fake_useragent.fake.FakeUserAgent'>
用 dir() 檢視物件成員 :
>>> dir(ua)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_filter_useragents', 'browsers', 'chrome', 'data_browsers', 'edge', 'fallback', 'ff', 'firefox', 'getBrowser', 'getChrome', 'getEdge', 'getFirefox', 'getRandom', 'getSafari', 'googlechrome', 'min_percentage', 'min_version', 'os', 'platforms', 'random', 'safari', 'safe_attrs']
我們主要會用到此物件的下列五個屬性來取得瀏覽器之 UserAgent :
- chrome : 取得 Chrome 瀏覽器之 UserAgent
- edge : 取得 Edge 瀏覽器之 UserAgent
- firefox : 取得 Firefox 瀏覽器之 UserAgent
- safari : 取得 Safari 瀏覽器之 UserAgent
- random : 從上面四種瀏覽器 UserAgent 中隨機取一個
例如 :
>>> ua.chrome
'Mozilla/5.0 (Linux; Android 11; moto e20 Build/RONS31.267-94-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.64 Mobile Safari/537.36'
>>> ua.edge
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/119.0.2151.65 Version/17.0 Mobile/15E148 Safari/604.1'
>>> ua.firefox
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0 Config/91.2.2121.13'
>>> ua.safari
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1 OPT/4.3.1'
>>> ua.random
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
>>> ua.random
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/120.0.2210.126 Version/17.0 Mobile/15E148 Safari/604.1'
只要將這五種屬性擇一 (例如 ua.random) 放到 headers 字典中即可 :
headers={'User-Agent': ua.random}
response=requests.get(url, headers=headers)
沒有留言:
張貼留言