4. Python 网络请求库:urllib 与 requests 实战

网络请求是数据采集的「手」——你规划得再好,发不出请求、拿不到响应,一切都是空谈。Python 里最常用的两个库就是 urllib(内置)和 requests(第三方)。我个人习惯把 urllib 当作「了解底层」的入门工具,而 requests 才是真正干活的主力。

核心脉络:urllib 让你理解 HTTP 请求的每个零件;requests 让你用三行代码搞定同样的事。两者都绕不开参数处理、响应解析、超时重试这三个坎。

Python 网络请求知识体系 urllib(内置标准库) requests(第三方神器) urllib.request urllib.parse requests.get / post / Session 处理请求参数 处理响应内容 设置超时与重试 URL 编码 Query String JSON 解析 HTML 解析 timeout 参数 重试机制 urllib 适合理解原理,requests 适合生产环境

4.1 urllib 库基础使用

urllib 是 Python 自带的网络请求库,零安装成本。说实话,它的 API 设计有点「啰嗦」,但能让你看清 HTTP 请求的每个细节。

4.1.1 发送 GET 请求

from urllib.request import urlopen

response = urlopen('https://api.example.com/data')
html = response.read().decode('utf-8')
print(response.status)  # 200
print(response.headers)  # 响应头信息

嗯,这里要注意:urlopen 返回的是一个类文件对象,必须手动 .read() 再解码。我在早期项目里经常忘了 .decode(),结果拿到一堆乱码——后来养成了习惯,拿到响应先看 charset。

4.1.2 发送 POST 请求

from urllib.request import Request, urlopen
from urllib.parse import urlencode

data = {'username': 'admin', 'password': '123456'}
req = Request(
    'https://api.example.com/login',
    data=urlencode(data).encode('utf-8'),
    headers={'User-Agent': 'Mozilla/5.0'}
)
resp = urlopen(req)
print(resp.read().decode())

看到没?urllib 的 POST 请求需要三步:构造字典 → urlencode 编码 → .encode() 转字节。说实话,这个流程太繁琐了,所以我后来基本只用 requests。

我的习惯:urllib 我只用来做两件事——① 快速测试一个 URL 是否可达;② 在无法安装第三方库的受限环境(比如某些 Docker 容器)里做简单请求。其他场景一律用 requests。

4.2 requests 库安装与使用

requests 的口号是「HTTP for Humans」——名副其实。安装就一行命令:

pip install requests

我曾经在一个爬虫项目里,用 urllib 写了 50 行代码才搞定带 cookie 的登录流程。换成 requests 后,10 行搞定,而且可读性高得多。

4.2.1 基本请求方法

import requests

# GET 请求
resp = requests.get('https://api.example.com/users')
print(resp.status_code, resp.text)

# POST 请求
resp = requests.post('https://api.example.com/login', data={'user': 'admin'})

# 其他 HTTP 方法
# requests.put(url, data=...)
# requests.delete(url)
# requests.head(url)

你想想看,urllib 需要 4-5 步的操作,requests 一行就搞定了。而且 resp.text 自动帮你解码,不用再操心 .decode('utf-8')。

4.2.2 Session 对象与 Cookie 持久化

这是 requests 最实用的功能之一。当你需要连续请求(比如先登录再获取数据),Session 会自动帮你携带 cookie。

session = requests.Session()
session.post('https://api.example.com/login', data={'user': 'admin', 'pwd': '123'})
# 后续请求自动携带登录态
resp = session.get('https://api.example.com/profile')
print(resp.json())

避坑指南:我曾经在采集一个电商网站时,每次请求都新建一个 requests.get(),结果被服务器识别为不同用户,触发了反爬。换成 Session 后,所有请求共享 cookie,问题立刻解决。

4.3 处理请求参数

不管是 GET 还是 POST,参数处理都是高频操作。我见过太多新手直接把参数拼在 URL 里——遇到特殊字符就崩了。

4.3.1 GET 请求参数

import requests

# 正确方式:用 params 参数
params = {
    'keyword': 'Python 爬虫',
    'page': 1,
    'sort': 'time'
}
resp = requests.get('https://api.example.com/search', params=params)
print(resp.url)  # 自动编码:https://api.example.com/search?keyword=Python+%E7%88%AC%E8%99%AB&page=1&sort=time

看到没?params 会自动处理 URL 编码,中文、空格、特殊符号都不用你操心。我自己以前手拼 URL 时,经常因为忘记 urllib.parse.quote() 而出错。

4.3.2 POST 请求参数

参数类型 参数名 适用场景 示例
表单数据 data 普通表单提交 data={'key': 'value'}
JSON 数据 json RESTful API json={'key': 'value'}
文件上传 files 上传图片/文档 files={'file': open('a.txt', 'rb')}
# 表单数据
resp = requests.post(url, data={'username': 'admin'})

# JSON 数据(自动设置 Content-Type: application/json)
resp = requests.post(url, json={'username': 'admin'})

# 文件上传
with open('report.pdf', 'rb') as f:
    resp = requests.post(url, files={'file': f})

注意:千万别把 data 和 json 混用。如果你传了 json=...,requests 会自动设置 Content-Type: application/json;如果传 data=...,默认是 application/x-www-form-urlencoded。搞错了服务器会返回 415 错误。

4.4 处理响应内容

拿到响应后,怎么提取数据?这是数据采集的核心环节。我一般分两种情况处理:JSON 和 HTML。

4.4.1 JSON 响应处理

现在大部分 API 都返回 JSON。requests 内置了 .json() 方法,直接帮你反序列化。

import requests

resp = requests.get('https://api.example.com/users/1')
data = resp.json()  # 返回 dict 或 list
print(data['name'], data['email'])

# 如果响应不是合法 JSON,会抛出 ValueError
# 建议先检查状态码
if resp.status_code == 200:
    try:
        data = resp.json()
    except ValueError:
        print('响应不是合法 JSON')
        print(resp.text[:200])  # 打印前 200 字符用于调试

嗯,这里有个坑:.json() 只能调用一次。因为它是从原始响应流中读取并解析的,调用后流就关闭了。我见过有人写 data1 = resp.json(); data2 = resp.json(),第二个直接报错。

4.4.2 HTML 响应处理

对于 HTML 页面,requests 只负责拿回原始文本,解析需要配合其他库。最常用的是 BeautifulSoup 和 lxml。

import requests
from bs4 import BeautifulSoup

resp = requests.get('https://example.com/news')
soup = BeautifulSoup(resp.text, 'html.parser')

# 提取所有标题
titles = soup.select('h2.news-title')
for title in titles:
    print(title.get_text(strip=True))

# 提取链接
links = soup.select('a[href]')
for link in links:
    print(link['href'], link.get_text())

我的经验:解析 HTML 时,优先用 CSS 选择器(soup.select()),可读性比 find_all() 好很多。如果页面结构复杂,我会先用浏览器开发者工具复制 CSS 选择器,再微调。

4.5 设置超时与重试

网络请求最怕什么?卡死。一个请求挂在那里 5 分钟不返回,整个采集任务就瘫痪了。所以超时和重试是必须配置的。

4.5.1 超时设置

import requests

# 设置总超时 5 秒
resp = requests.get('https://api.example.com', timeout=5)

# 分别设置连接超时和读取超时(推荐)
resp = requests.get('https://api.example.com', timeout=(3, 10))
# 连接超时 3 秒,读取超时 10 秒

我个人强烈建议用元组形式。连接超时设短一点(3-5 秒),读取超时根据数据量设长一点(10-30 秒)。为什么?连接超时说明服务器可能挂了,没必要等;读取超时说明数据在传输中,可以多给点时间。

我曾经踩过的坑:有一次采集一个慢速 API,我设了 timeout=5,结果每次请求都超时。后来发现是读取时间不够——API 返回的数据量很大,5 秒根本传不完。改成 timeout=(5, 30) 后问题解决。

4.5.2 重试机制

requests 本身没有内置重试功能,但可以通过 requests.adapters.HTTPAdapter 配合 urllib3.Retry 实现。

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()

retry_strategy = Retry(
    total=3,                # 总重试次数
    backoff_factor=1,       # 退避因子:等待 1s, 2s, 4s...
    status_forcelist=[500, 502, 503, 504],  # 哪些状态码触发重试
    allowed_methods=['GET', 'POST']  # 哪些方法允许重试
)

adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount('http://', adapter)
session.mount('https://', adapter)

# 使用 session 发起请求,自动带重试逻辑
resp = session.get('https://api.example.com/unstable')

你想想看,如果不加重试,一次网络抖动就可能导致整个采集任务失败。加了重试后,程序会自动在 1 秒、2 秒、4 秒后重试,大大提高了稳定性。

参数 说明 推荐值
total 最大重试次数(包括首次请求) 3-5
backoff_factor 退避因子,重试间隔 = backoff_factor × (2^(n-1)) 1-2
status_forcelist 触发重试的 HTTP 状态码 [500, 502, 503, 504]
allowed_methods 允许重试的 HTTP 方法 ['GET', 'HEAD'](POST 慎用)

核心建议:超时 + 重试是网络请求的「安全带」。我所有生产环境的采集脚本,都会配置 timeout=(3, 10) 和 Retry(total=3, backoff_factor=1)。这两行代码能挡住 90% 的网络异常。

4.6 本章小结

urllib 和 requests 是 Python 数据采集的基石。urllib 让你理解 HTTP 请求的底层机制,requests 让你高效地完成实际工作。处理参数时用 params 和 json 参数,解析响应时区分 JSON 和 HTML 场景,最后一定要加上超时和重试——这三板斧练好了,网络请求这块基本就稳了。