4. Python 网络请求库:urllib 与 requests 实战
网络请求是数据采集的「手」——你规划得再好,发不出请求、拿不到响应,一切都是空谈。Python 里最常用的两个库就是 urllib(内置)和 requests(第三方)。我个人习惯把 urllib 当作「了解底层」的入门工具,而 requests 才是真正干活的主力。
核心脉络:urllib 让你理解 HTTP 请求的每个零件;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 场景,最后一定要加上超时和重试——这三板斧练好了,网络请求这块基本就稳了。