python selenium 爬蟲文字登入驗證碼處理

 前言:

我不喜歡說爬蟲這個詞, 講"自動化測試"不是帥多了嗎(笑
雖然爬蟲案子錢都不多, 但不用動甚麼腦子, 做起來很愉也很快, 是個不錯的選擇

正文:

首先, 目標網頁是https://csms.mohw.gov.tw/lcms/
我們用到的package有 :
  • selenium
  • opencv-python
  • numpy
  • ddddocr
先初始化一些需要用到的東西

import ddddocr
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

TARGET_URL = r"https://csms.mohw.gov.tw/lcms/"  # 目標URL
ocr = ddddocr.DdddOcr()                         # 初始化 ddddocr

# 這裡用RemoteDriver是因為剛好有 請選擇用自己有的driver 例如 webdriver.chrome(...)
driver = webdriver.Remote(command_executor="http://127.0.0.1:4444",
                          desired_capabilities=DesiredCapabilities.CHROME)

接著打開目標頁面看看長怎樣


嗚阿 有個pop up modal
想辦法把他關掉
於是我們先載入網頁

from selenium.webdriver.support.ui import WebDriverWait

driver.get(TARGET_URL)  # 載入網頁
wait = WebDriverWait(driver, 10)  # 設定implicit wait 為10秒

接下來

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# 有個礙事的modal 直接用javascript幹掉他
modal: WebElement = wait.until(
    EC.visibility_of_element_located((By.ID, "myModal")))
driver.execute_script("arguments[0].remove();", modal)

WebDriverWait.until(method: any)方法可以等待直到method回傳True為止
如果沒有找到該元素(超出implicit wait的時間) 則會拋出
selenium expected_conditions 有提供一些class 方便使用
像是 visibility_of_element_located(locator: any) 可以檢查該元素是否渲染完成並可見
更多的class可以在selenium的文件中找到
然後locator通常是tuple (By: str, param: str)
用 By 的方式來定位元素 param, By可以有這些值 : 

class By(object):
    """
    Set of supported locator strategies.
    """

    ID = "id"
    XPATH = "xpath"
    LINK_TEXT = "link text"
    PARTIAL_LINK_TEXT = "partial link text"
    NAME = "name"
    TAG_NAME = "tag name"
    CLASS_NAME = "class name"
    CSS_SELECTOR = "css selector"

找到討人厭的modal之後 我們用javascript把元素直接刪除
要在webdriver執行javascript可以使用 
driver.execute_script(script: any, *args: any)
script 是要執行的javascript 如果你有已經定位的元素想要在javascript中使用
將他放到args參數中 就可以在javascript中用 arguments[i] 參照
例如 
driver.execute_script("arguments[0]; arguments[1];", input1, input2)
arguments[0] 會對應到 input1, arguments[1] 會對應到 input2 依此類推
然後執行完就會變這樣


耶~ modal不見了
可以看到的我們的目標驗證碼了
接著我們要辨識驗證碼 大概會有以下的步驟
  1. 找到圖片元素並取得圖像
  2. 圖像處理
  3. ocr辨識圖像 回傳結果
考慮到不一定每次辨識都是正確的
我們寫個function 方便重新辨識

from PIL import Image
import cv2
import time
import numpy as np
import io

# 因為驗證碼固定長度為4個字元 為了提高準確度
# 辨識結果的字串長度不為4時 重新取的新的驗證
# 碼 這邊寫成function方便呼叫

def captchaCrack(driver: webdriver) -> str:
    wait = WebDriverWait(driver, 10)
    # 找驗證碼圖片
    captchaDiv: WebElement = wait.until(
        EC.visibility_of_element_located((By.ID, "captchaDiv")))
    captchaImg: WebElement = captchaDiv.find_element_by_tag_name("img")

    # 截圖存成PIL的Image
    img = Image.open(io.BytesIO(captchaImg.screenshot_as_png))
    # 灰階處理
    gray = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2GRAY)
    _, threshold = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
    # 雜訊處理
    noise = np.random.normal(0, 15, threshold.shape)
noise = np.clip(threshold +noise, 0, 255).astype('uint8')
    guassian = cv2.blur(noise, (3, 3))
    # 最後存回 PIL Image
    img = Image.fromarray(guassian)
    # 回傳辨識結果
    return ocr.classification(img)

res = captchaCrack(driver)

# 如果長度不為4 點擊重新整理再辨識一次
while(len(res) != 4):
    refresh: WebElement = wait.until(
        EC.element_to_be_clickable((By.XPATH, "//button[text()='重新產生']")))
    refresh.click()
    time.sleep(1)
    res = captchaCrack(driver)


圖像處理的方法很多我就不多說明了 Google 資料很多 OUOb
這裡把圖像轉成灰階 & 高斯模糊 降低雜訊
再用ddddocr 的classification(img) -> str 函數取得辨識的結果
img 可以是 PIL Image | bytes | base64 string | pathlib.PurePath

再把辨識結果輸入到input裡面就完成了

# 接著找驗證碼的input
captcha_input: WebElement = wait.until(
    EC.visibility_of_element_located((By.ID, "captcha")))

# 將結果輸入到input
captcha_input.send_keys(res)


執行結果




最後附上完整程式碼

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from PIL import Image
import cv2
import ddddocr
import time
import numpy as np
import io

TARGET_URL = r"https://csms.mohw.gov.tw/lcms/"  # 目標URL
ocr = ddddocr.DdddOcr()                         # 初始化 ddddocr

# 這裡用RemoteDriver是因為剛好有 請選擇用自己有的driver 例如 webdriver.chrome(...)
driver = webdriver.Remote(command_executor="http://127.0.0.1:4444",
                          desired_capabilities=DesiredCapabilities.CHROME)
driver.get(TARGET_URL)  # 載入網頁
wait = WebDriverWait(driver, 10)  # 設定implicit wait 為10秒

# 有個礙事的modal 直接用javascript幹掉他
modal: WebElement = wait.until(
    EC.visibility_of_element_located((By.ID, "myModal")))
driver.execute_script("arguments[0].remove();", modal)

# 因為驗證碼固定長度為4個字元 為了提高準確度
# 辨識結果的字串長度不為4時 重新取的新的驗證
# 碼 這邊寫成function方便呼叫


def captchaCrack(driver: webdriver) -> str:
    wait = WebDriverWait(driver, 10)
    # 找驗證碼圖片
    captchaDiv: WebElement = wait.until(
        EC.visibility_of_element_located((By.ID, "captchaDiv")))
    captchaImg: WebElement = captchaDiv.find_element_by_tag_name("img")

    # 截圖存成PIL的Image
    img = Image.open(io.BytesIO(captchaImg.screenshot_as_png))
    # 灰階處理
    gray = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2GRAY)
    _, threshold = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
    # 雜訊處理
    noise = np.random.normal(0, 15, threshold.shape)
    noise = np.clip(threshold +noise, 0, 255).astype('uint8')
    guassian = cv2.blur(noise, (3, 3))
    # 最後存回 PIL Image
    img = Image.fromarray(guassian)
    # 回傳辨識結果
    return ocr.classification(img)


res = captchaCrack(driver)
# 如果長度不為4 點擊重新整理再辨識一次
while(len(res) != 4):
    refresh: WebElement = wait.until(
        EC.element_to_be_clickable((By.XPATH, "//button[text()='重新產生']")))
    refresh.click()
    time.sleep(1)
    res = captchaCrack(driver)

# 接著找驗證碼的input
captcha_input: WebElement = wait.until(
    EC.visibility_of_element_located((By.ID, "captcha")))

# 將結果輸入到input
captcha_input.send_keys(res)

留言

這個網誌中的熱門文章

[android]QR code掃描