第 38 课 · 实战 · 18 分钟

Python URL 解析

链接进入队列前,先把相对地址补全。

urlparseurljoinparse_qsurlencode

供应商页面上的商品链接全是相对路径,一半还带着跟踪参数。当成普通字符串处理,它们就变成了重复项。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 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← HTML 解析分页与去重 →