供应商的报价页只差一个礼貌的请求,可上一个实习生因为猛刷被封了。请求需要名字、超时和理由。解析之前,你该先检查什么?
爬虫从请求开始,而不是从正则开始。请求说明你要访问哪个 URL、以什么身份访问,以及客户端愿意等待多久。本课先构造请求对象,再解释解析之前必须处理的失败情况。 你还会编码查询参数、检查 HTTPError,并限制重试。
请求不只有 URL
urllib.request.Request 保存 URL 和可选请求头。描述清楚的 User-Agent 告诉服务器是谁在访问。构造请求不会真的发送;在普通 Python 环境里,urlopen 才执行网络调用。
from urllib.request import Request
request = Request(
"https://example.com/docs",
headers={"User-Agent": "PyPracticeBot/1.0"},
)
print(request.full_url)
print(request.get_header("User-agent"))先构造,再发送
为慢服务器和故障做准备
网络会失败,页面会迁移。超时限制等待时间,状态码告诉你响应是否可用。2xx 视为成功,3xx 需要处理重定向,4xx 或 5xx 需要判断,而不是盲目重试。
status = 404
if 200 <= status < 300:
result = "ok"
elif 300 <= status < 400:
result = "redirect"
else:
result = "error"
print(result)按状态码分支
把抓取和解析分开
把解析写成接收 HTML 文本的函数。这样可以用保存的网页样本测试,不需要联网。这个边界也能清楚划分限速和缓存应放在哪里。
from html.parser import HTMLParser
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag == "a":
href = dict(attrs).get("href")
if href:
self.links.append(href)
def parse_links(html):
parser = Links()
parser.feed(html)
return parser.links
saved = '<a href="/docs">Docs</a><a href="/about">About</a>'
print(parse_links(saved))可以离线测试的解析器
先编码查询参数,再构造请求
urlencode 会转义空格、& 和非 ASCII 文本。用编码后的片段拼 URL,不要直接拼接原始用户输入。清楚的 User-Agent 和 Accept 头也能让请求更容易识别和调试。
from urllib.parse import urlencode
from urllib.request import Request
query = urlencode({"q": "green tea", "page": 2})
url = "https://example.com/search?" + query
request = Request(url, headers={"User-Agent": "PyPracticeGift/1.0"})
print(request.full_url)
print(request.get_header("User-agent"))由编码片段组成的 URL
把 HTTPError 当成响应来读
urllib 遇到 4xx/5xx 会抛出 HTTPError,但异常里仍有状态码、响应头和可读响应体。错误页里有有用信息时可以捕获并读取;不要捕获所有网络异常然后假装请求成功。
from io import BytesIO
from urllib.error import HTTPError
error = HTTPError(
"https://example.com/missing",
404,
"Not Found",
{"Content-Type": "text/plain"},
BytesIO(b"no such page"),
)
print(error.code, error.reason)
print(error.read().decode("utf-8"))失败响应体也是数据
限制重试并尊重 Retry-After
只重试可能暂时的错误,例如 429 和 503。Retry-After 可能是秒数,也可能是 HTTP 日期;要给它设上限,别让服务器让你的任务睡几个小时。重试次数也必须封顶。
def retry_delay(status, retry_after, cap=300):
if status not in {429, 503}:
return None
try:
seconds = int(retry_after)
except (TypeError, ValueError):
return 60
return max(1, min(seconds, cap))
for status, header in [(200, None), (429, "120"), (503, "soon")]:
print(status, retry_delay(status, header))有上限的等待,不是无限睡眠
本课要点
- Request 对象携带 URL 和请求头,包括描述清楚的 User-Agent。
- 设置超时,避免卡住的连接让程序一直等待。
- 认真处理状态码,不要假设每个响应都是 HTML。
- 用保存的 HTML 样本测试解析器,而不是直接打真实网站。
- 通过缓存和限速减少不必要的请求。
- 查询参数要编码,HTTPError 要主动读,Retry-After 和重试次数都要封顶。
常见错误
- 在循环里发请求,却没有延迟、超时或停止条件。
- 伪装成浏览器来绕过访问控制或站点条款。
- 还没确认请求成功,就开始解析响应。
- 连永久性的 404 也重试,给已经吃力的服务器继续加负载。
为一个文档页构造带机器人 User-Agent 的 Request,在不发送的情况下打印 URL 和请求头。
动手试试
改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。