3、Python网络请求库:urllib基础、requests库入门、处理超时与重试
说到网络请求,这可是数据采集的第一步。我刚开始做爬虫那会儿,用的就是Python自带的urllib。说实话,那玩意儿用起来真不算顺手。后来接触了requests库,嗯,简直打开了新世界的大门。
今天我就把这两个库的核心用法,以及我在项目中踩过的坑,一次性给你讲明白。
3.1 urllib基础:Python自带的请求工具
urllib是Python标准库,不需要额外安装。你想想看,但凡是个Python环境,它都在。但它的API设计嘛……我个人觉得有点反人类。
先看个最简单的GET请求:
from urllib.request import urlopen
response = urlopen('https://api.example.com/data')
html = response.read().decode('utf-8')
print(html[:200]) # 只打印前200个字符看看
是不是感觉还行?但一旦涉及到POST请求、添加请求头,代码就开始变得啰嗦了。
核心要点:urllib的urlopen()返回的是一个类文件对象,需要用read()读取内容,再用decode()解码。这一步很多人会忘,结果拿到一堆乱码。
来个带请求头的例子:
from urllib.request import Request, urlopen
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html'
}
req = Request('https://api.example.com/data', headers=headers)
response = urlopen(req)
data = response.read().decode('utf-8')
看到了吧?先要构造Request对象,再传给urlopen。我个人觉得这个流程太绕了。而且urllib处理cookie、代理什么的,更是麻烦得要命。
我的建议:除非你是在受限环境(比如某些公司内网)只能用标准库,否则我强烈建议你用requests。urllib就当个备胎吧。
3.2 requests库入门:优雅的HTTP请求
requests库的口号是「HTTP for Humans」——为人类设计的HTTP库。我第一次用的时候,心里就一个想法:这才对嘛!
先安装:
pip install requests
然后看个GET请求,对比一下和urllib的区别:
import requests
response = requests.get('https://api.example.com/data')
print(response.text[:200]) # 直接拿到字符串,不用手动解码
是不是清爽多了?response.text自动处理编码,省心。
POST请求也简单:
import requests
data = {'username': 'admin', 'password': '123456'}
response = requests.post('https://api.example.com/login', data=data)
print(response.json()) # 如果返回JSON,直接解析
这里有个小细节——response.json()。我在项目中遇到过很多次,对方接口返回的明明是JSON格式,但有人非要用正则去解析……何必呢?requests直接帮你搞定。
再看看添加请求头和超时设置:
import requests
headers = {
'User-Agent': 'Mozilla/5.0',
'Authorization': 'Bearer your_token_here'
}
response = requests.get(
'https://api.example.com/protected',
headers=headers,
timeout=5 # 5秒超时
)
重要提醒:timeout参数一定要加!不加的话,遇到网络问题你的程序可能会卡住几分钟甚至更久。我见过生产环境因为没设timeout,导致整个爬虫集群雪崩的案例。
3.3 处理超时与重试:让程序更健壮
网络请求嘛,总会遇到各种意外。服务器挂了、网络波动、DNS解析失败……你想想看,如果程序一遇到错误就崩溃,那还怎么搞数据采集?
所以,超时和重试机制是必须的。
3.3.1 超时设置详解
requests的超时有两种:
| 类型 | 说明 | 示例 |
|---|---|---|
| 连接超时 | 建立连接的最大等待时间 | timeout=3.05 |
| 读取超时 | 接收数据的最大等待时间 | timeout=(3.05, 10) |
我个人习惯分开设置:
import requests
# 连接超时3秒,读取超时10秒
timeout = (3.05, 10)
try:
response = requests.get('https://api.example.com/slow', timeout=timeout)
print(response.status_code)
except requests.exceptions.ConnectTimeout:
print('连接超时了,可能是网络问题')
except requests.exceptions.ReadTimeout:
print('读取超时了,服务器响应太慢')
为什么连接超时我写3.05而不是3?嗯,这是个小技巧。很多路由器的ICMP超时设置是3秒,设成3.05可以避免一些边界情况。
3.3.2 重试机制:用tenacity库
我曾经写过一个爬虫,每天要抓取上百万条数据。如果不加重试,大概有5%的请求会因为各种临时故障失败。加了重试之后,失败率降到了0.1%以下。
推荐用tenacity库,专门处理重试逻辑:
pip install tenacity
看个例子:
import requests
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3), # 最多重试3次
wait=wait_exponential(multiplier=1, min=1, max=10) # 指数退避等待
)
def fetch_data(url):
response = requests.get(url, timeout=5)
response.raise_for_status() # 状态码不是200就抛异常
return response.json()
# 使用
try:
data = fetch_data('https://api.example.com/data')
print(data)
except Exception as e:
print(f'所有重试都失败了: {e}')
避坑指南:我曾经犯过一个错误——对所有异常都重试。结果遇到404、403这种客户端错误,重试了3次还是404,白白浪费时间和带宽。正确的做法是:只对网络相关的异常(ConnectionError、Timeout等)进行重试,对HTTP状态码错误(4xx、5xx)要区分处理。
更精细的重试控制:
import requests
from tenacity import retry, stop_after_attempt, retry_if_exception_type
from requests.exceptions import ConnectionError, Timeout
@retry(
stop=stop_after_attempt(3),
retry=retry_if_exception_type((ConnectionError, Timeout)),
reraise=True
)
def safe_request(url):
response = requests.get(url, timeout=5)
if response.status_code == 429: # 请求太频繁
raise Timeout('触发限流,假装超时触发重试')
response.raise_for_status()
return response
3.3.3 手动实现简单重试
如果你不想引入第三方库,自己写个重试也不难:
import time
import requests
from requests.exceptions import ConnectionError, Timeout
def fetch_with_retry(url, max_retries=3, delay=1):
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response
except (ConnectionError, Timeout) as e:
print(f'第{attempt + 1}次尝试失败: {e}')
if attempt < max_retries - 1:
wait_time = delay * (2 ** attempt) # 指数退避
print(f'等待{wait_time}秒后重试...')
time.sleep(wait_time)
else:
print('所有重试都失败了')
raise
# 使用
try:
resp = fetch_with_retry('https://api.example.com/data')
print(resp.text[:100])
except Exception as e:
print(f'最终失败: {e}')
注意:重试时一定要加退避策略。如果失败后立即重试,很可能还是失败,而且会给服务器造成压力。指数退避(1秒、2秒、4秒……)是比较通用的做法。如果是爬虫场景,还可以加上随机抖动,避免所有客户端同时重试。
3.4 实战建议:选哪个库?
说了这么多,总结一下我的个人经验:
- 日常开发:无脑选requests。代码简洁、功能完善、社区活跃。
- 受限环境:用urllib。比如某些公司的内网服务器,不让装第三方包。
- 高并发场景:可以考虑aiohttp(异步版requests),但那是后话了。
- 重试机制:小项目自己写,大项目用tenacity。
最后说一句:不管用哪个库,超时和重试一定要加。这是数据采集的保命符。我见过太多新手,代码写得挺漂亮,一上线就崩,十有八九是没处理网络异常。
好了,这一章就到这里。下一章我们聊聊数据清洗——那才是真正考验耐心的地方。