Skip to content

python 爬虫的重试机制

0 背景

reuquests 是我们爬虫工作中非常常用的一个爬虫框架, 有时, 我们的爬虫引擎在抓取网站的过程, 可能会因为网络不稳定 & 服务器繁忙负载过高 & 任务中断 & 反爬等一些特殊的原因, 导致爬虫异常, 这种情况有时是一种偶然, 那么在这些偶然的情况下, 或许我们重新再去抓取几次, 就能抓到了正确的响应的数据. 因此, 我们需要采取一些措施, 在抓取过程出现异常的时候进行多次重试抓取, 以增加我们抓取请求成功的机会.

1 requests 自带的重试功能 Retry

requests 库内置了一种简单的重试机制,可以使用 Retry 类来设置, 超时自动重试.

import requests
from requests.adapters import Retry, HTTPAdapter

s = requests.session()

retries = Retry(total=3, backoff_factor=0.5, )

s.mount('http://', HTTPAdapter(max_retries=retries))
s.mount('https://', HTTPAdapter(max_retries=retries))

res = s.get('http://127.0.0.1:5000/test_timeout', timeout=(1, 4))
print(res.text)

其中, Retry对象可以如下设置:

retries = Retry(
    total=3,  # 最大重试次数
    backoff_factor=0.5,  # 重试之间的退避因子(等待时间递增因子)
    status_forcelist=[500, 502, 503, 504],  # 需要重试的HTTP状态码列表
)

Retry对象具有许多参数,可以用来配置重试的行为。以下是Retry对象的主要参数及其含义:

  • total: 最大重试次数,默认为3。指定请求的最大重试次数。

  • connect: 一种可以重试的连接错误(如连接超时),默认为None。可以是一个异常类或者异常类的元组,当发生指定的连接错误时,请求将会重试。

  • read: 一种可以重试的读取错误,通常是服务器断开连接导致的。默认为None。可以是一个异常类或者异常类的元组,当发生指定的读取错误时,请求将会重试。

  • redirect: 是否重试对重定向的请求, 默认为False。如果设置为True,那么对于某些特定的重定向(例如,由于网络问题导致的重定向),请求将会重试。

  • status: 一种可以重试的HTTP状态码,可以是一个整数、一个整数元组或者一个True/False值。默认为None。如果设置为一个整数,表示当发生指定状态码时,请求将会重试。如果设置为一个元组,表示当状态码在元组中时,请求将会重试。如果设置为True,表示任何非200系列的状态码都会被视为需要重试的状态码。

  • backoff_factor: 重试之间的退避因子,用于计算重试之间的等待时间。默认为1,即重试之间等待时间不变。如果设置为0.1,那么第一次重试将等待1秒,第二次等待1.1秒,第三次等待1.21秒,以此类推。

  • status_forcelist: 一种强制重试的HTTP状态码列表,默认为None。与status参数不同,这个参数表示任何在列表中的状态码都会被强制视为需要重试的状态码,不论status参数如何设置。

另外, timeout=(1, 4) 中的1和4分别表示连接超时时间读取超时时间:

  • 建立连接的超时时间为1秒:如果在1秒内无法建立与目标服务器的连接,请求会抛出一个连接超时的异常。
  • 读取数据的超时时间为4秒:一旦成功建立连接,服务器有3秒的时间来发送响应。如果在3秒内没有收到完整的响应数据,请求会抛出一个读取超时的异常。

2 第三方包retrying模块

retrying是一个第三方模块, 需要额外安装:

pip install retrying

简要说明以下这个包的一些参数, 该包是通过装饰器对函数进行重试的. 一些有条件的参数, 写在装饰器函数retry()

retry                                            # 不加参数表示无限重试
retry(wait_fixed = 100)                          # 设置重试间隔时长,单位:ms,1000ms = 1s
retry(wait_random_min=100,wait_random_max=200)   # 随机重试间隔,将在100ms - 200ms内
retry(stop_max_attempt_number = 4)               # 最大重试次数,超过后正常抛出异常
retry(stop_max_delay=1)                          # 最大延迟时长,1ms内未满足条件则抛出异常
retry(retry_on_exception = user_define_function)           # 当发生指定异常时会执行函数
retry(retry_on_result=user_define_function)                # 每次都会执行函数,当返回返回True就重试,否则异常退出

我们先模拟一个简单的爬虫, 当然, 重试机制不一定只在爬虫上, 可以应用到很多场景, 爬虫请求往往会收到网络 | 反爬等一些影响, 导致一次请求不会成功, 因此, 重试机制可以有效避免爬虫请求出现异常的情况.

我们先模拟一个简单的爬虫, 并且会出现了错误的爬虫. 待会会用到重试模块, 有条件的重试可以帮助爬虫提升抓取能力.

from retrying import retry
import time
import requests


def spider():
    print('spider engine start work.')
    time.sleep(2)
    raise requests.exceptions.ReadTimeout('超时请求错误')


if __name__ == '__main__':
    spider()

1 重试次数

@retry()
def spider():
    print('spider engine start work.')
    time.sleep(2)
    raise requests.exceptions.ReadTimeout('超时请求错误')

出现异常就会无限重试, 以上的结果当然是无限输出spider engine start work., 在爬虫当中, 这是不可取的.

stop_max_attempt_number

@retry(stop_max_attempt_number=5)
def spider():
    print('spider engine start work.')
    time.sleep(2)
    raise requests.exceptions.ReadTimeout('超时请求错误')

使用stop_max_attempt_number参数设置最大重试次数, 在爬虫引擎中, 有时候因为因为网络或代理IP失效等突然的原因导致请求失败, 设置最大重试次数, 可以减少这种临时的问题.

2 重试时间

stop_max_delay

@retry(stop_max_delay=5000)
def spider():
    print('spider engine start work.')
    time.sleep(10)
    raise requests.exceptions.ReadTimeout('超时请求错误')

这个我其实有点难以理解的, 后面我多去测试了几次: 使用stop_max_delay参数设置最大的重试时间, 这个是一个总的整个的所有的重试运行的时间, 如果出现了异常, 此时就需要重试, 正常会无限运行, 但是通过stop_max_delay当该函数运行超过5000毫秒的时候(上面的例子), 就不再重试了, 这样就可以在异常发生后及时处理, 也可以减少资源占用.

注意, 这个参数值的单位是毫秒.

wait_random_min & wait_random_max

@retry(wait_random_min=1000, wait_random_max=5000)
def spider():
    print(f'{time.time()} spider engine start work.')
    time.sleep(3)
    print(f'{time.time()} spider engine now working.')
    raise requests.exceptions.ReadTimeout('超时请求错误')

使用wait_random_min & wait_random_max参数可以出现异常后, 指定隔随机毫秒再运行, 这不就是为爬虫定制的参数吗!

wait_fixed

@retry(wait_fixed=5000)
def spider():
    print('spider engine start work.')
    time.sleep(2)
    print('spider engine now working.')
    raise requests.exceptions.ReadTimeout('超时请求错误')

使用wait_fixed参数时, 可以让函数出现异常时, 在特定时间后, 再继续运行, 在爬虫当时, 可以避免一些因抓取过快导致的爬虫问题, 这个参数比随机时间规律些.

3 异常的额外处理

stop_func

def get_now():
    return datetime.now().strftime("%H:%M:%S")


def send_msg_to_wechat():
    ...


def error_func(attempts, delay):
    print(f'{attempts=}, {delay=} ')
    if attempts > 3:
        send_msg_to_wechat()
    return attempts > 3


@retry(stop_func=error_func)
def spider():
    print(f'{get_now()} spider engine start work.')
    time.sleep(3)
    print(f'{get_now()} spider engine now working.')
    raise requests.exceptions.ReadTimeout('超时请求错误')

这里, 使用stop_func参数, 可以在当函数出现异常时, 交给其他函数处理. 比如, 当我的爬虫出现错误时而且错误次数出现参数时, 我就将异常情况发送到微信, 并且停止运行, stop_func接收了一个异常处理函数, 在上面的例子我指定了error_func, error_func接受两个参数, attempts表示重试运行的次数, delay表示重试期间运行的时间(毫秒).

3 第三方模块tenacity

由于是第三方模块, 需要安装:

pip install tenacity

无条件重试

from tenacity import retry

@retry
def test_retry():
    print("等待重试,重试无间隔执行...")
    raise Exception

test_retry()

无条件重试,但是在重试之前要等待 2 秒

from tenacity import retry, wait_fixed

@retry(wait=wait_fixed(2))
def test_retry():
    print("ᒵஇ᯿ᦶ...")
    raise Exception

test_retry()

只重试n次

from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(7))
def test_retry():
    print("ᒵஇ᯿ᦶ...")
    raise Exception
test_retry()

何时进行重试

在出现特定错误/异常(比如请求超时)的情况下,再进行重试

from requests import exceptions
from tenacity import retry, retry_if_exception_type
@retry(retry=retry_if_exception_type(exceptions.Timeout))
def test_retry():
    print("ᒵஇ᯿ᦶ...")
    raise exceptions.Timeout
test_retry()

在满足自定义条件时,再进行重试。

如下示例,当 test_retry 函数返回值为 False 时,再进行重试

from tenacity import retry, stop_after_attempt, retry_if_result
def is_false(value):
 return value is False
@retry(stop=stop_after_attempt(3),
 retry=retry_if_result(is_false))
def test_retry():
 return False
test_retry()

当出现异常后,tenacity 会进行重试,若重试后还是失败,默认情况下,往上抛出的异常会变成 RetryError,而不是最根本的原因。 因此可以加一个参数( reraise=True ),使得当重试失败后,往外抛出的异常还是原来的那个。

from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(7), reraise=True)
def test_retry():
 print("ᒵஇ᯿ᦶ...")
 raise Exception
test_retry()