2024年5月22日 星期三

Python 學習筆記 : 網頁擷取 (六) : 處理表單與模擬登入

在前面的爬蟲測試中只要用 requests.get() 發出 HTTP GET 請求即可取得目標資料, 但有些目標網頁 (例如高鐵班次時刻表) 使用者必須先在網頁表單中輸入或選擇後提交給伺服器, 後端程式才會提供目標資料. 另外有些網站 (例如會員制網站) 必須填寫帳密登入成功後才能進一步瀏覽或查詢內部資料, 這兩種類型網站都無法依靠 requests.get() 取得, 必須使用 requests.post() 才行. 

本系列之前的筆記參考 : 


以下是我讀 "網頁擷取-使用 Python" 第九章後所做的整理與測試. 


一. 基本的表單 : 

在 HTML 網頁中, 超連結 a 元素基本上是用來讓使用者送出 GET 請求的 (雖然也可以觸發 Javascript 來送出 POST 請求), 而表單 form 元素則主要是用來送出 POST 請求 (當然也可以送出 GET 請求), requests 套件中的 post() 函式能向伺服器送出 POST 請求, 且能攜帶 cookies, headers 等參數, 可模擬使用者填寫表單的動作. 

測試表單的 POST 請求需要後端程式配合, 所幸我在 "網頁擷取-使用 Python" 這本書中找到作者所提供的基本表單測試網頁 : 





檢視原始碼可知此網頁是由兩個 input-text 輸入框元素與一個提交按鈕元素 submit 構成的表單, 當填好輸入欄位按 "Submit" 鈕就會向伺服器的後端程式 processing.php 提交 POST 請求, 表單中的這兩個欄位 firstname 與 lastname 就會被放在請求參數 :

<h2>Tell me your name!</h2>
<form method="post" action="processing.php">
First name: <input type="text" name="firstname"><br>
Last name: <input type="text" name="lastname"><br>
<input type="submit" value="Submit" id="submit">
</form>

在 Chrome 按 F12 開啟開發者頁面後, 在此表單網頁輸入姓名提交, 後端程式 processing.php 會將收到的欄位資料回應給客戶端, 結果如下 : 



這時在開發者頁面的 "Network/All" 頁籤, 點左方的 processing.php 請求, 再點右邊的 "Headers" 即可看到這個 POST 請求的標頭 :




點 "Headers" 右邊的 "Payload" 就可看到表單資料 (form data) 傳出的兩個參數 firstname 與 lastname :




以上是手動操作的結果, 接下來我們使用 requests.post() 模擬表單的 POST 請求動作, 匯入 requests 套件後, 將要傳遞的變數放在字典中, 然後在呼叫 requests.post() 函式向 processing.php 提交 POST 請求時將變數字典傳給 data 參數 :

>>> import requests  
>>> url='https://pythonscraping.com/pages/files/processing.php'   
>>> data={'firstname': 'Donald', 'lastname': 'Trump'}   
>>> res=requests.post(url, data=data)   # post 傳遞變數的參數名稱為 data
>>> print(res.text)   
Hello there, Donald Trump!   

可見得到與手動操作瀏覽器提交表單同樣的結果. 

注意, GET 請求除了直接在 URL 上用 ? 與 & 攜帶變數, 還可以在呼叫 requests.get() 時透過 params 參數來傳遞, params 也是一個傳遞變數組成的字典, 但此處要強調的是 get() 使用的關鍵字參數名稱是 params, 而 post() 則是使用 data, 不要搞混了. 


二. 上傳欄位表單 : 

網頁表單內的欄位型態不只是文字欄位與按鈕, 還可能有單選圓鈕, 可複選的核取方塊, 文字區域, 與下拉式選單等, 但不管表單包含多少元素, 使用 requests.post() 去模擬 POST 請求前只要知道這些被提交的欄位名稱 (name 屬性) 與其所傳遞之變數值是哪種類型即可, 這些資訊可以透過檢視網頁原始碼與瀏覽器的開發者工具得知. 關於表單元素參考 :


"網頁擷取-使用 Python" 這本書的官方網站提供了一個檔案上傳網頁 :


檢視原始碼如下 :

<h2>Upload a file!</h2>
<form action="processing2.php" method="post" enctype="multipart/form-data">
Submit a jpg, png, or gif: <input type="file" name="uploadFile"><br>
<input type="submit" value="Upload File">
</form>

可見表單內就是一個型態為 file, 名稱為 uploadFile 的 input 元素, 它其實是一種固定標示為 '選擇檔案' 的按鈕, 按下時可從本機選取檔案 :



選取檔案後檔名會顯示在按鈕後面 :




按下 Upload File 按鈕會將檔案上傳給後端伺服器上的 processing2.php 處理, 該程式回應如下 :




用 Chrome 開發者工具觀察請求的 payload 與回應訊息如下 :





使用 requests 套件來模擬檔案上傳動作需以二進位方式開啟圖檔, 將其以所傳遞之變數名稱 (此處為 input 元素名稱 uploadFile) 為屬性組成字典傳給 requests.post() 的 files 參數 : 

>>> import requests  
>>> url='https://pythonscraping.com/pages/files/processing.php'   
>>> f=open('種矮仙丹-1.jpg', 'rb')          # 以二進位方式開啟圖檔
>>> files={'uploadFile': f}                        # 製作 files 字典
>>> res=requests.post(url, files=files)     # 提交表單
>>> print(res.text)     
uploads/種矮仙丹-1.jpg
The file 種矮仙丹-1.jpg has been uploaded.
>>> f.close()                                                # 關閉檔案

結果與人工操作是一樣的. 


三. 登入表單 : 

這種網站後端擁有會員管理系統, 使用者以註冊好的帳密在登入表單提出 POST 請求登入成功後才能進一步取得網站內容, 所以通常需要至少兩次 POST 請求才能取得目標網頁. 由於 HTTP 的請求回應是無狀態的, 為了能辨別使用者是否已登入, 伺服器會在前端儲存一個 cookie (伺服器產生的 token) 或在後端用 Session 物件保存登入狀態. 

"網頁擷取-使用 Python" 這本書的官方網站提供了測試登入表單的網頁 :





檢視原始碼如下 :

<h2>Log In Here!</h2>
Warning: Your browser must be able to use cookies in order to view our site!
<form method="post" action="welcome.php">
Username (use anything!): <input type="text" name="username"><br>
Password (try "password"): <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>

可見此提交此表單時會向後端伺服器的 welcome.php 傳遞 username (帳號) 與 password (密碼) 這兩個變數, 帳號可隨便填, 但密碼必須輸入 password 才能成功登入 (可見 welcome.php 只是簡單地比對密碼是否符合而已). 

按 Login 鈕提交表單經 welcome.php 檢查密碼正確回應如下網頁 :




點下方的超連結即可進入網站 : 





登入時開發者頁面的請求 payload 與回應如下 :





在開發者工具視窗的 Application 頁籤可以找到伺服器傳回之 cookies 資訊 : 




如果要用 requests 套件模擬表單登入, 必須進行兩次 POST 請求, 第一次是用 data 參數傳遞 username 與 password 變數向 welcome.php 程式提出請求, 若登入成功伺服器會回應 cookie 給瀏覽器儲存, 我們可以用回應物件的 cookies 子物件的 get_dict() 方法取得放在 HTTP 回應中的 cookies 字典 :

>>> import requests  
>>> url='https://pythonscraping.com/pages/cookies/welcome.php' 
>>> data={'username': 'Tony', 'password': 'password'}     
>>> res=requests.post(url, data=data)   
>>> print(f'cookies: {res.cookies.get_dict()}')   
cookies: {'loggedin': '1', 'username': 'Tony'}   

然後向 profile.php 發出 POST 請求並將此 cookies 字典傳給 cookies 參數即可 :

>>> url='https://pythonscraping.com/pages/cookies/profile.php'      
>>> res=requests.post(url, cookies=res.cookies  # 提出第二個 POST 請求 (回傳 cookies)
>>> res.text   
"Hey Tony! Looks like you're still logged into the site!"

這樣便成功取得目標網頁了. 

除了在登入成功後的後續請求中傳回 cookies 讓伺服器知道你是已登入之使用者外, 還可以在伺服端儲存 Session 變數來達到相同目的, 事實上使用 Session 比較好, 因為瀏覽器可以關閉 cookies 功能. 

>>> import requests  
>>> session=requests.Session()     # 建立 Session 物件
>>> url='https://pythonscraping.com/pages/cookies/welcome.php' 
>>> data={'username': 'Tony', 'password': 'password'}     
>>> res=session.post(url, data=data)      # 用 Session 物件提出 POST 請求
>>> print(f'cookies: {res.cookies.get_dict()}')     # 使用 Session 也會傳回 cookies
cookies: {'loggedin': '1', 'username': 'Tony'}
>>> url='https://pythonscraping.com/pages/cookies/profile.php'   
>>> res=session.post(url)      # 用 Session 物件提出第二個 POST 請求
>>> res.text   
"Hey Tony! Looks like you're still logged into the site!"

結果與上面使用 post(url, coookies) 相同.

沒有留言 :