
Retry异常重试
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
最前面介绍爬虫时讲过爬虫的工作流程共有 7 个步骤,第一步设定抓取目标(种子页面/起始页面)并获取网页,这一步我们可以通过 requests 库完成。现在进入到第二步,当服务器无法访问时,按照指定的重试次数尝试重新下载页面。这步在爬虫的工作流程中特别重要,它其实就是对应爬虫程序中的异常处理模块。
方案探讨
爬虫在爬取网页的过程中会产生大量的网络请求,在请求的过程中出现网络错误是在正常不过的事情了,但如何处理异常错误就值得探讨了:
- 方案一:不进行异常处理,当爬虫在遇到错误后就报错退出了。这对于数据的完整性来说是很不友好的,这很有可有能导致数据漏采,如果没有设置断点续爬,而且采集报错的又是最后一个请求,那么又得重新开始采集,增加了等待时间、浪费了网络资源。
- 方案二:响应异常一直重试,当爬虫在遇到错误后就一直无限次的请求,直到响应正确。这看起来是一种对数据完整性很好的做法,但实则是一种很危险的做法。比如网站正常的一次请求响应耗时500毫秒,刚好人家网站服务器出错了,可能100毫秒就返回了,这样会导致爬虫程序的请求放大五倍,对方服务器承受更大的压力;还比如人家服务器出问题了,访问一直超时,爬虫由于超时错误疯狂发起请求,把人家的长链接全部占住,导致正常访问受阻。因此爬虫采集过程中有大量请求报错时,一定要让程序自动停掉,好多爬虫程序因为没有限定重试次数,导致多线程疯狂请求,变成了网络攻击,这样的后果非常危险!
- 方案三:响应异常指定重试次数,当服务器无法访问时,按照指定的重试次数尝试重新下载页面。这是最推荐的方案,因为它一方面最大限度的保证了数据的完整性,比如第一次请求超时,可能过一会再请求就成功了,所以重试几次可能会消除异常;另一方面也限制了爬虫采集对网站服务器所造成的压力在可承受范围内。
方案实现
循环重试
实现异常指定次数重试最简单的写法就是,try...except...
异常处理加上 for
指定次数循环。代码如下:通过输出可以看到下面的代码完全实现了我们想要的功能,当请求正确的时候就输出一次,当请求发生错误的时候就进行重试,当重试的请求达到指定次数后,退出程序。
import requests
# 错误重试次数
RETRY_TIMES = 3
# 请求函数一
def request_one():
for times in range(RETRY_TIMES):
try:
response = requests.get('https://www.baidu.com/', timeout=0.5)
print(f'request_one:{response}')
break
except Exception as e:
print(f'request_one:{e}')
if times == RETRY_TIMES - 1:
exit()
# 请求函数二
def request_two():
for times in range(RETRY_TIMES):
try:
response = requests.get('http://httpbin.org/get', timeout=0.5)
print(f'request_two:{response}')
break
except Exception as e:
print(f'request_two:{e}')
if times == RETRY_TIMES - 1:
exit()
request_one()
request_two()
'''
输出:
request_one:<Response [200]>
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
'''
装饰器重试
上面的循环重试虽然可以满足我们的需求,但美中不足的是两个函数中有大量的重复代码,代码有很多坏味道,重复是最坏的一种。如何整合重复代码,前面我们所学的“装饰器”就派上用场了。
标准装饰
我们将重复的代码写成一个装饰器,代码如下:通过上面输出可以看到使用装饰器也能完全满足我们的需求,而且还简化了函数内容,提高了代码的可读性。
import requests
# 错误重试次数
RETRY_TIMES = 3
# 错误重试(装饰器)
def error_retry(func_name):
def foo(func):
def bar(*args, **kwargs):
for times in range(RETRY_TIMES):
try:
result = func(*args, **kwargs)
return result
except Exception as e:
print(f'{func_name}: {e}')
if times == RETRY_TIMES - 1:
exit()
return bar
return foo
# 带错误重试装饰器的请求函数一
@error_retry(func_name='request_one')
def request_one():
response = requests.get('https://www.baidu.com/', timeout=0.5)
print(f'request_one:{response}')
# 带错误重试装饰器的请求函数二
@error_retry(func_name='request_two')
def request_two():
response = requests.get('http://httpbin.org/get', timeout=0.5)
print(f'request_two:{response}')
request_one()
request_two()
'''
输出:
request_one:<Response [200]>
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
'''
自定义装饰
装饰器还可以自定义每次请求所携带的参数,比如每次请求都使用随机的请求头:
import requests
from fake_useragent import UserAgent
# 错误重试次数
RETRY_TIMES = 3
# 错误重试(装饰器)
def error_retry(func_name):
def foo(func):
def bar(*args, **kwargs):
for times in range(RETRY_TIMES):
try:
# 每次使用随机请求头
headers = UserAgent().random
# 请求头给到被装饰函数
result = func(headers, *args, **kwargs)
return result
except Exception as e:
print(f'{func_name}: {e}')
if times == RETRY_TIMES - 1:
exit()
return bar
return foo
# 带错误重试装饰器的请求函数一
@error_retry(func_name='request_one')
# headers接收的实参来自装饰器,url接收的实参来自函数调用
def request_one(headers, url):
response = requests.get(url=url, headers=headers, timeout=0.5)
print(f'request_one:{response}')
# 带错误重试装饰器的请求函数二
@error_retry(func_name='request_two')
# headers接收的实参来自装饰器,url接收的实参来自函数调用
def request_two(headers, url):
response = requests.get(url=url, headers=headers, timeout=0.5)
print(f'request_two:{response}')
# 这里headers不用传参,因为装饰器会带上headers参数,但url必须使用关键字传参。
request_one(url='https://www.baidu.com/')
request_two(url='http://httpbin.org/get')
'''
输出:
request_one:<Response [200]>
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
request_two:HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=1)
'''
三方库重试
有的人可能会想,有没有不写装饰器代码进行错误重试办法。刚好 Python 就有这么一个名称为 retrying
的三方库可以实现这个想法。
安装使用
**使用 retrying
是对程序中异常重试的一种优雅的解决方案。**首先我们来安装一下:
pip install retrying
参数详解
三方库 retrying
提供一个装饰器函数 @retry
里面有很多的参数,这些参数,我们可以随意组合使用,并不限定每次只能用一个。
重试次数
stop_max_attempt_number
最大重试次数,被装饰的函数会在运行失败的情况下重新执行,如果没有设置最大重试次数,默认一直报错就一直重试。
from retrying import retry
# 最大重试次数
@retry(stop_max_attempt_number=3)
def do_something_limited():
print("do something several times")
raise Exception("raise exception")
do_something_limited()
'''
输出:
do something several times
do something several times
do something several times
Exception: raise exception
'''
固定间隔时间
wait_fixed
设置每次重试的间隔时间,单位毫秒;
from retrying import retry
# 重试间隔时间
@retry(wait_fixed=3000)
def wait_fixed_time():
print("wait")
raise Exception("raise exception")
wait_fixed_time()
'''
输出:
wait
wait
wait
...
注释:这里没有设置重试次数,因此会一直重试,每次重试的时间间隔为3秒。
'''
随机间隔时间
wait_random_min
设置每次重试的最小间隔时间,单位毫秒;
wait_random_max
设置每次重试的最大间隔时间,单位毫秒;
from retrying import retry
# 设置重试时间的随机范围
@retry(wait_random_min=1000, wait_random_max=3000)
def wait_random_time():
print("wait")
raise Exception("raise exception")
wait_random_time()
'''
输出:
wait
wait
wait
...
注释:这里没有设置重试次数,因此会一直重试,每次重试的时间间隔为随机的1到3秒。
'''
最大重试时间
stop_max_delay
从执行方法开始计时,在重试时间内报错就一直重试,超过重试时间报错就抛出错误,单位毫秒。
import time
from retrying import retry
# 设置最大重试时间(从执行方法开始计算)
@retry(stop_max_delay=3000)
def do_something_in_time():
time.sleep(1)
print("do something in time")
raise Exception("raise exception")
do_something_in_time()
'''
输出:
do something several times
do something several times
do something several times
Exception: raise exception
'''
特定返回重试
retry_on_result
它接收一个函数,设置特定返回值才进行重试;
from retrying import retry
# 通过返回值判断是否重试
def retry_if_result_none(result):
if result == "111":
return True
@retry(retry_on_result=retry_if_result_none)
def might_return_none():
print('retry')
return "111"
might_return_none()
'''
输出:
retry
retry
retry
...
注释:这里我们定义了一个判断返回值的函数,然后将这个函数作为参数传给retry装饰函数。当结果返回是“111”时,就会一直重试执行might_return_none函数。
'''
特定异常重试
retry_on_exception
它接收一个函数,设置特定异常类型才进行重试;
from retrying import retry
# 通过异常类型判断是否重试
def retry_if_io_error(exception):
return isinstance(exception, IOError)
# 设置特定异常类型重试
@retry(retry_on_exception=retry_if_io_error)
def retry_special_error():
print("retry io error")
raise IOError("raise exception")
retry_special_error()
'''
输出:
retry io error
retry io error
retry io error
...
注释:只有出现IOError错误时,retry_if_io_error函数才返回True进行错误重试,而retry_special_error函数一直抛出IOError错误,因此就是一直错误重试。
'''
案例重写
现在我们将最上面的案例进行重写,可以得到如下优雅的代码:
import requests
from retrying import retry
# 错误重试次数
RETRY_TIMES = 3
# 请求函数一(三次错误后才抛出错误)
@retry(stop_max_attempt_number=RETRY_TIMES)
def request_one():
response = requests.get('https://www.baidu.com/', timeout=0.5)
print(f'request_one:{response}')
# 请求函数二(三次错误后才抛出错误)
@retry(stop_max_attempt_number=RETRY_TIMES)
def request_two():
response = requests.get('http://httpbin.org/get', timeout=0.5)
print(f'request_two:{response}')
request_one()
request_two()
'''
输出:
request_one:<Response [200]>
requests.exceptions.ReadTimeout: HTTPConnectionPool(host='httpbin.org', port=80): Read timed out. (read timeout=0.5)
'''