selenium結合tenacity的retry實現驗證碼失敗重試

松勤吳老師發表於2023-02-21

說在前面

  • 驗證碼登入的demo後續可以單獨講解,VIP學員對這部分應該都是掌握的,此處不再贅述
  • 本文假設了一個場景
    • 你透過OCR識別的驗證碼是有一定的錯誤機率的
    • 本文是透過識別後的驗證碼去加一個隨機字元,如果取到的是''空字元則可能成功,否則必然不會成功
  • 所涉及的python庫
    • selenium
    • ddddocr
    • tenacity

上程式碼

  • 細節詳見註釋
from selenium import webdriver
from time import sleep
from tenacity import TryAgain, retry, wait_random


def get_element(locator):
    '''
    這個函式用來判斷是否存在某個元素
    '''
    try:
        from selenium.webdriver.support.wait import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC
        return WebDriverWait(driver, 5, 0.5).until(EC.visibility_of_element_located(locator))
    except:
        return False


driver = webdriver.Chrome()
driver.implicitly_wait(5)
driver.get('http://114.116.2.138:8090/forum.php')  # 這個地址你可以訪問,是我搭建在雲端的一個容器,開源論壇
driver.find_element('css selector', '#ls_username').send_keys('admin')
driver.find_element('css selector', '#ls_password').send_keys('123456')
driver.find_element('css selector', 'button.pn.vm').click()


@retry(wait=wait_random(min=3, max=5)) # 等待一個時間區間,3-5s,注意這個時間的下限建議>hint存在的最大時間
def code_login():
    '''
    這個函式是用來反覆驗證碼登入的
    '''
    # 驗證碼元素
    ele_code = driver.find_element('css selector', '[id^=vseccode_cS]>img')
    import ddddocr
    ocr = ddddocr.DdddOcr()
    code_text = ocr.classification(ele_code.screenshot_as_png)
    # 當失敗的時候尤其要注意: 清空已輸入的內容
    driver.find_element('css selector', 'input[id^=seccodeverify_cS]').clear()
    test_data = ['1','a','','2','b']
    from random import choice
    choice_data = choice(test_data)
    # 輸入識別後的資料+隨機字元
    driver.find_element('css selector', 'input[id^=seccodeverify_cS]').send_keys(code_text + choice_data)
    # 點選登入
    driver.find_element('css selector', "[name='loginsubmit']>strong").click()
    # 注意! 你可以去操作網頁,點選登入如果失敗會彈出提示 "抱歉,驗證碼填寫錯誤"
    hint_locator = 'css selector', '.pc_inner>i'
    if get_element(hint_locator): # 如果出現這個元素,就...
        # 點選下驗證碼,重新整理一下
        driver.find_element('css selector', '[id^=vseccode_cS]>img').click()
        # 丟擲異常,重跑
        raise TryAgain
code_login()

聊聊tenacity

https://tenacity.readthedocs.io/en/latest/api.html

https://github.com/jd/tenacity

  • 這個庫將重試這件事基本做透了

1、 無條件重試

  • 你瘋了
from tenacity import retry

@retry
def retry_without_anything():
    print('retry...')
    raise Exception  # 不加這個可不會重試

retry_without_anything()

2、重試指定次數後停止

from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(3))
def retry_times():
    print(f'retry... times')
    raise Exception


retry_times()

  • 實際執行的效果是這樣
retry... times
retry... times
retry... times
Traceback (most recent call last):
  File "D:\Python39\lib\site-packages\tenacity\__init__.py", line 407, in __call__
    result = fn(*args, **kwargs)
  File "demo_retry.py", line 20, in retry_times
    raise Exception
Exception

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "demo_retry.py", line 23, in <module>
    retry_times()
  File "D:\Python39\lib\site-packages\tenacity\__init__.py", line 324, in wrapped_f
    return self(f, *args, **kw)
  File "D:\Python39\lib\site-packages\tenacity\__init__.py", line 404, in __call__
    do = self.iter(retry_state=retry_state)
  File "D:\Python39\lib\site-packages\tenacity\__init__.py", line 361, in iter
    raise retry_exc from fut.exception()
tenacity.RetryError: RetryError[<Future at 0x16628b45460 state=finished raised Exception>]
  • 別擔心,你可能在最後一次的時候就不丟擲異常了。

3、過一定時間後停止重試

from tenacity import retry, stop_after_delay
import  arrow
from time import sleep
@retry(stop=stop_after_delay(10))
def retry_times():
    print(arrow.now().format('YYYY-MM-DD HH:mm:ss'))
    sleep(1)
    raise Exception
retry_times()
  • 輸出像這樣
2023-02-21 17:32:01
2023-02-21 17:32:02
2023-02-21 17:32:03
2023-02-21 17:32:04
2023-02-21 17:32:05
2023-02-21 17:32:06
2023-02-21 17:32:07
2023-02-21 17:32:08
2023-02-21 17:32:10
2023-02-21 17:32:11
# 最後丟擲異常

4、組合條件

@retry(stop=(stop_after_delay(10) | stop_after_attempt(5)))
def retry_multi_conditions():
    print("10秒後或者5次重試後停止重試")
    raise Exception

5、重試間隔

  • 之前的重試是無縫銜接的

  • 你可以讓重試之間有延遲

    @retry(wait=wait_fixed(2))
    def retry_wait_time1():
        print(arrow.now().format('YYYY-MM-DD HH:mm:ss'))
        print("每次重試前等待2秒")
        raise Exception
    
    retry_wait_time1()
    
  • 當然上面的仍然是個無限重試

  • 你可以組合前面的停止

    @retry(wait=wait_fixed(2),stop=stop_after_attempt(3))
    

  • 重試等待間隔可以設定一個區間(最大最小值)

    from tenacity import retry, stop_after_attempt, wait_random
    import  arrow
    
    
    @retry(wait=wait_random(min=1,max=4),stop=stop_after_attempt(3))
    def retry_wait_time1():
        print(arrow.now().format('YYYY-MM-DD HH:mm:ss'))
        print("每次重試前等待1~4秒區間")
        raise Exception
    
    retry_wait_time1()
    

6、是否重試!!!

  • 這是最重要的了

  • 重試的條件一:引發特定或一般異常的重試

    from tenacity import retry, retry_if_exception_type, stop_after_attempt
    
    @retry(retry=retry_if_exception_type(ZeroDivisionError),
                  stop= stop_after_attempt(5))
    def retry_if_exception():
        print('retry...')
        print(10/0)   # 現在觸發的就是ZeroDivisionError,如果把此處改為 print(a),則遇到的是NameError,那就不會重試
        raise Exception
    
    retry_if_exception()
    

  • 引發 TryAgain 異常隨時顯式重試

  • 比如這樣

    @retry
    def try_if_condition():
        result = 23
        if result == 23:
           raise TryAgain
    
  • 上面的demo中就用到了這個

  • 官網還有很多的例子,相對高階一些,如有興趣可以自行前往或者搜尋之

相關文章