0%

[Python] 使用 Selenium 爬蟲下載當前頁面驗證碼圖片

前言

正常的登入機制中有一個環節是顯示一張驗證碼圖片,請使用者輸入圖片中的驗證碼,來達到阻擋機器人 (爬蟲) 登入的效果。但這並不能阻止以爬蟲為樂的我們,我們可以在需要輸入驗證碼的時候,將驗證碼截圖並顯示在跳出對話框,請使用者手動輸入驗證碼。成功登入後,剩下的工作就可交由爬蟲繼續執行。

然而,對驗證碼圖片的連結發出一個資源請求,正常來說會回傳不同於前一次的驗證碼圖片,因此我們不能只抓 img 標籤中的 src 然後重新發出請求來下載該圖片。而是應直接下載當前頁面所顯示的圖片,這篇文章紀錄如何使用 Selenium 在不重新發出請求的情形下,下載當前 img 標籤內的圖片。

環境

  • Python 3.8.5
  • matplotlib 3.4.2
  • selenium 3.140.0

原因說明

直接舉實例來觀察,隨意以 網路郵局 介面中的 驗證碼連結 為例,同樣的連結重新整理後,會吐不一樣的圖片出來

一樣連結產百樣碼一樣連結產百樣碼

若使用 urllibrequests 等套件去下載圖片,顯示給使用者輸入,這時取得的驗證碼圖片,一定不是正確的。要解決這個問題,直接從頁面抓當前的圖片是最佳解。

解決方法

想要解決這個問題還需要使用到 Javascript (js) 來協助。

先透過 js 在 Selenium 中取得 img 的內容,並以 Base64 格式回傳圖片內容到 Python 中,有了這個 Base64 圖片,甚麼都好辦,例如存檔、顯示在 tkinter 視窗或更多其他種應用。

若想要知道還能如何利用透過 Selenium 取得的驗證碼圖片,歡迎參考實際範例 [爬蟲] Selenium+Tkinter 製作具有驗證碼的網站登錄器 !

取得圖片 Base64 格式

使用 js 進行 DOM 操作,每一個步驟詳細說明盡在註解中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 創建一個 canvas 元素
var canvas = document.createElement('canvas');
// 取得 canvas 元素中的 2d 上下文
var context = canvas.getContext('2d');
// 取得驗證碼的 img 標籤,要根據真實情況來變動,
// 每個網頁驗證碼的 css 選擇器應該很高機率不會相同
var img = document.querySelector('div.codes_img img');
// canvas 高度與寬度設定為與驗證碼 img 相同
canvas.height = img.naturalHeight;
canvas.width = img.naturalWidth;
// 在 canvas 2d 上下文中的座標 (0, 0) 處,畫上 img 的內容
context.drawImage(img, 0, 0);
// 轉換此圖片為 Base64 格式
console.log(canvas.toDataURL());

得到的資料會像下面這樣的形式呈現,半形逗號之前為圖片的資訊,例如格式為 image/png 且以 base64 編碼,半形逗號後面的則是編碼後的資料 (data)

1
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI4AAAAw...(ignored)

回傳 Base64 圖片

我們已經會使用 js 來取得 Base64 格式的圖片了,接下來就是要將它回傳到 Python 之中,Selenium 提供一個函數 execute_async_script 讓我們可以在當前頁面運行 js 腳本,並將結果回傳到 Python 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from selenium import webdriver

url = "https://ipost.post.gov.tw/pst/home.html"

driverPath = r'chromedriver.exe'
browser = webdriver.Chrome(executable_path=driverPath)
browser.get(url)

# callback function 會將 canvas.toDataURL() 回傳回來給 captchaBase64
captchaBase64 = browser.execute_async_script("""
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img = document.querySelector('div.codes_img img');
canvas.height = img.naturalHeight;
canvas.width = img.naturalWidth;
context.drawImage(img, 0, 0);

callback = arguments[arguments.length - 1];
callback(canvas.toDataURL());
""")
  • callback:函式的最後一個參數 (arguments[arguments.length - 1]) 為 JavaScript Callback 函式,將要回傳的資料作為參數呼叫此函式,便能將資料回傳到 Python 之中。

若想查看是否真的是驗證碼的圖,可以使用 matplotlib 套件來顯示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import base64
import io
from matplotlib import pyplot as plt
import matplotlib.image as mpimg

# 先將 data Url 前綴 (data:image/png;base64) 去除,再將 base64 資料轉為 bytes
i = base64.b64decode(captchaBase64.split(',')[1])
# 因為 mpimg 讀取的是檔案,但我們不想存檔案於本地端,
# 所以在記憶體中創建一個內容為 i 的 bytes 檔案,
i = io.BytesIO(i)
# 將 i 這個檔案以 PNG 的形式讀出,若不指定 format,imread 預設會以副檔名解析圖片,
# 若無法從副檔名得知,則直接以 PNG 嘗試解析。
# 這邊的情況是沒有副檔名,且有可能讀者遇到非 PNG 的圖片,
# 因此特別展示可以使用 format 來指定圖片形式
i = mpimg.imread(i, format='PNG')
# 顯示驗證碼
plt.imshow(i)
plt.show()

取得與網頁一樣的驗證碼圖片取得與網頁一樣的驗證碼圖片

結語

爬蟲爬的是 html 文件,因此 js、css 多少都要會,這個例子關鍵在於 js 的使用,用以取得 img 標籤 Base64 格式圖片,這是單純 Python 無法達到的功能。

參考文獻

  1. Javascript: how to get image as bytes from a page (without redownloading)

  2. Understanding execute async script in Selenium

  3. Python+Selenium 如何使用execute_async_script的callback

  4. StringIO和BytesIO

  5. matplotlib.pyplot.imread

很高興能在這裡幫助到您,歡迎登入 Liker 為我鼓掌 5 次,或者成為我的讚賞公民,鼓勵我繼續創造優質文章。
以最優質的內容回應您的鼓勵