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。

最后说一句:不管用哪个库,超时和重试一定要加。这是数据采集的保命符。我见过太多新手,代码写得挺漂亮,一上线就崩,十有八九是没处理网络异常。

好了,这一章就到这里。下一章我们聊聊数据清洗——那才是真正考验耐心的地方。