供应商页面上的商品链接全是相对路径,一半还带着跟踪参数。当成普通字符串处理,它们就变成了重复项。urljoin 知道什么,是字符串拼接不知道的?
抓取到的页面里充满相对链接、跟踪参数和片段。把它们当普通字符串处理,会产生重复地址和错误请求。urllib.parse 提供一组标准工具,用来拆分、拼接和重建 URL。 你还会为去重规范化 URL,并拒绝不安全的协议或主机。
把 URL 拆成组成部分
urlparse 分离协议、网络位置、路径、查询字符串和片段。片段是给浏览器用的,不会发送给服务器。分别读取各部分,比在第一个斜杠处切字符串更安全。
from urllib.parse import urlparse
url = urlparse("https://example.com/docs/page.html?q=python#intro")
print(url.scheme)
print(url.netloc)
print(url.path)
print(url.query)一个对象,五个部分
基于当前页面补全相对链接
urljoin 用页面 URL 补全相对引用。它能处理 ../、根相对路径和只有查询参数的链接。先去重再补全,会把 /docs 和 https://example.com/docs 当成两个不同地址。
from urllib.parse import urljoin
base = "https://example.com/docs/page.html"
print(urljoin(base, "../guide.html"))
print(urljoin(base, "/about"))
print(urljoin(base, "?page=2"))一个基准,三种链接
读取查询参数
parse_qs 把查询字符串转换成“键到列表”的字典,因为同一个键可以出现多次。需要保留顺序时使用 parse_qsl。值仍然是字符串,页码在参与计算前要显式转换。
from urllib.parse import parse_qs
params = parse_qs("q=python&page=2&tag=a&tag=b")
print(params["page"])
print(params["tag"])重复的键会变成列表
安全地构造查询字符串
urlencode 会替你转义空格、& 和非 ASCII 文本。可以传入映射或键值对序列。不要把原始用户输入直接拼进 URL;一个多余的 & 就可能改变请求含义。
from urllib.parse import urlencode
query = urlencode({"q": "a b", "page": 2})
print(query)编码,而不是拼接
先去重前规范化
同一个页面可能有大小写不同的协议、默认端口、片段或不同顺序的查询参数。比较前把协议和主机转小写、去掉默认端口、排序查询参数并丢掉片段;路径本身仍然区分大小写。
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
def normalize(url):
parts = urlsplit(url)
scheme = parts.scheme.lower()
host = (parts.hostname or "").lower()
default_port = 443 if scheme == "https" else 80
port = parts.port
netloc = host if not port or port == default_port else f"{host}:{port}"
query = urlencode(sorted(parse_qsl(parts.query, keep_blank_values=True)))
return urlunsplit((scheme, netloc, parts.path or "/", query, ""))
for raw in [
"HTTPS://Example.com:443/docs?b=2&a=1#top",
"https://example.com/docs?a=1&b=2",
]:
print(normalize(raw))两种写法,一个键
拒绝不安全的协议和意外主机
urljoin 会跟随链接里的任何协议,包括 javascript: 和 data:。解析结果,只允许 http 或 https,并检查主机是否在预期范围内。相对链接拼接后也可能变成外部 URL,所以解析前后都要检查。
from urllib.parse import urljoin, urlsplit
base = "https://example.com/docs/page"
for link in ["/next", "javascript:alert(1)", "https://evil.example/x", "//example.com/other"]:
target = urljoin(base, link)
parts = urlsplit(target)
safe = parts.scheme in {"http", "https"} and parts.hostname == "example.com"
print(link, safe, target)先解析,再做允许列表
本课要点
- urlparse 分别提供 scheme、netloc、path、query 和 fragment。
- urljoin 用链接所在页面的地址补全相对链接。
- parse_qs 返回列表,因为查询键可能重复。
- urlencode 在值进入 URL 前进行转义。
- 比较抓取队列中的地址前,先去掉片段。
- 比较前先规范化;urljoin 之后检查协议和主机。
常见错误
- 用字符串拼接来补全相对链接。
- 假设查询参数只会出现一次。
- 比较两个页面地址时保留 # 后面的片段。
- 直接比较原始 URL 字符串,结果放过了 javascript: 链接。
基于一个基准 URL 补全三个相对链接,并按首次出现顺序打印唯一的绝对地址。
动手试试
改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。