bigywater.tech
爬虫工程 · 37

相对链接解析:urljoin 与 base 标签

从页面里提取出的链接大多是相对的,爬虫要把它们变成绝对 URL 才能继续抓。看起来 urljoin 一行就行,可是浏览器遵循的 WHATWG 标准和 Python 的 RFC 风格有细节差别, 标签和重定向还会改变基准。这篇用 Node 的 URL 做对照逐条看。

25 / 36
urljoin 与 WHATWG URL 结果相同的用例数(不同的 11 个)
file://
经 urljoin 仍原样保留的 scheme——要自己过滤,只放行 http 和 https
r.url
重定向之后解析相对链接应该用的 base(请求时的 URL 会得到错误的路径)

常规情形一致

实测 · 本机执行输出
  结果相同的 25 个,不同的 11 个

./、../、绝对路径、协议相对 //、只有 ? 或 #、目录结尾的斜杠、点段等 25 种用例,urljoin 与 WHATWG 的结果一致。

不一致的细节

实测 · 本机执行输出
  空字符串:相对链接 ''
        Python:http://a.test/dir/sub/page.html?x=1#top
        Node  :http://a.test/dir/sub/page.html?x=1
  前后有空格:相对链接 '  x.html  '
        Python:http://a.test/dir/sub/x.html  
        Node  :http://a.test/dir/sub/x.html
  反斜杠:相对链接 '..\\x.html'
        Python:http://a.test/dir/sub/..\x.html
        Node  :http://a.test/dir/x.html
  反斜杠开头的绝对路径:相对链接 '\\x.html'
        Python:http://a.test/dir/sub/\x.html
        Node  :http://a.test/x.html
  不同 scheme 的 scheme:foo:相对链接 'https:foo'(base http://a.test/dir/)
        Python:https:foo
        Node  :https://foo/
  三个斜杠 http:///x:相对链接 'http:///x'
        Python:http://a.test/x
        Node  :http://x/
  大写 scheme:相对链接 'HTTP://C.test/Z'
        Python:http://C.test/Z
        Node  :http://c.test/Z
  端口:相对链接 'http://a.test:80/x'
        Python:http://a.test:80/x
        Node  :http://a.test/x
  中文路径:相对链接 '/路径/页面.html'
        Python:http://a.test/路径/页面.html
        Node  :http://a.test/%E8%B7%AF%E5%BE%84/%E9%A1%B5%E9%9D%A2.html
  中文域名绝对链接:相对链接 'http://中文.test/x'
        Python:http://中文.test/x
        Node  :http://xn--fiq228c.test/x
  查询串里有空格:相对链接 '/s?q=a b'
        Python:http://a.test/s?q=a b
        Node  :http://a.test/s?q=a%20b

11 个不同的结果集中在:空字符串是否保留 fragment、前后空格、反斜杠、scheme 后没有斜杠的 https:foo、主机大小写、默认端口、中文路径/域名/空格的编码。浏览器会修正这些写法,urljoin 不会,所以同一个链接,你的爬虫和浏览器访问的 URL 字符串可能不同。去重前最好统一一遍(上一篇)。

别的 scheme

实测 · 本机执行输出
  urljoin(base, 'javascript:void(0)') = 'javascript:void(0)';scheme = 'javascript'
  urljoin(base, 'mailto:a@b.test') = 'mailto:a@b.test';scheme = 'mailto'
  urljoin(base, 'data:text/plain,hi') = 'data:text/plain,hi';scheme = 'data'
  urljoin(base, 'tel:+123') = 'tel:+123';scheme = 'tel'
  urljoin(base, 'ftp://f.test/x') = 'ftp://f.test/x';scheme = 'ftp'
  urljoin(base, 'file:///etc/passwd') = 'file:///etc/passwd';scheme = 'file'

urljoin 对 javascript:、mailto:、data:、tel:、ftp:、file: 一律原样返回。把它们交给下载器之前必须检查 scheme。

与重定向

实测 · 本机执行输出
  页面 URL = http://a.test/dir/page.html
  没有 <base>:['http://a.test/dir/x.html', 'http://a.test/root.html', 'http://a.test/up.html']
  <base href="http://cdn.test/assets/">:['http://cdn.test/assets/x.html', 'http://cdn.test/root.html', 'http://cdn.test/up.html']
  同样的页面,make_links_absolute(url, resolve_base_href=False)(文档字符串说"忽略 <base>"):['http://cdn.test/assets/x.html', 'http://cdn.test/root.html', 'http://cdn.test/up.html']
  → 实测 <base> 仍然生效。lxml 6.1.1 源码里的原因:
      make_links_absolute:if resolve_base_href:
      make_links_absolute:self.rewrite_links(link_repl)
      rewrite_links:def rewrite_links(self, link_repl_func, resolve_base_href=True,
      rewrite_links:elif resolve_base_href:
      rewrite_links:self.resolve_base_href()
  <base href="/other/"> (相对的 base):['http://a.test/other/x.html', 'http://a.test/root.html', 'http://a.test/up.html']
  两个 <base>:['http://two.test/x']
  请求 /old 被重定向到 /new/dir/page.html;相对链接 next.html:用请求时的 URL 当 base → <站点>/next.html;用 r.url(最终 URL)当 base → <站点>/new/dir/next.html

页面里的 <base href> 会改变所有相对链接的基准;两个 <base> 时 lxml 取最后一个。lxml 的 make_links_absolute(resolve_base_href=False) 在 6.1.1 里并没有忽略 <base>(源码里 rewrite_links 的默认参数让它仍然生效)。被重定向的页面,基准应该是最终 URL(r.url)而不是请求时的 URL。

分步演示

相对链接解析
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。Python 的 urllib.parse.urljoin(3.14.8)对照 Node v26.10.0 的 new URL(rel, base).href;lxml 6.1.1 的 make_links_absolute;重定向实验用本机服务器;lxml 的行为通过读 inspect.getsource 得到的源码确认;“两个 取最后一个”只是 lxml 的实测
    • 没有验证:真实浏览器(Chrome)对这些用例的结果(Node 的 URL 是 WHATWG 的实现,浏览器应当一致,但我没有在 Chrome 里逐条测);HTML 标准对多个 的规定; 与 xml:base;相对链接里的 IPv6、userinfo;html 属性里的 HTML 实体(&)解码在不同解析器里的差异
    京ICP备2026067502号-1京公网安备11010502063487号