bigywater.tech
爬虫工程 · 44

sitemap.xml:索引文件、gzip 与 lastmod

sitemap.xml 是爬虫最省力的 URL 来源,但解析它的坑不少:命名空间、两种 gzip、五花八门的 lastmod、几万条 URL 的内存、以及不规范的 XML。这篇用一个本机服务器把这些情形逐一复现。

0 个
不写 XML 命名空间时,findall("sitemap/loc") 找到的 URL 数(写对是 3 个)
38.5 对 4.0 MiB
5 万条 URL 的 sitemap,fromstring 与 iterparse 的内存峰值
x=1y=2
lxml recover=True 把正确转义的 x=1&y=2 读成的样子

命名空间

实测 · 本机执行输出
  根元素标签:{http://www.sitemaps.org/schemas/sitemap/0.9}sitemapindex
  不写命名空间 root.findall("sitemap/loc") → 0 个;写上命名空间 root.findall("{ns}sitemap/{ns}loc") → 3 个
  用 lxml 的 xpath("//*[local-name()='loc']/text()")(不依赖命名空间)→ 3 个

sitemap 的所有元素都在 http://www.sitemaps.org/schemas/sitemap/0.9 命名空间里。findall("sitemap/loc") 找不到任何东西,必须写 {ns}sitemap/{ns}loc,或者用 xpath 的 local-name() 绕过。

两种 gzip

实测 · 本机执行输出
  /sitemap2.xml.gz:Content-Type=application/gzip,Content-Encoding=None;requests 给出的 r.content 开头字节 1f8b(仍是 gzip,需要自己解压)
  /sitemap3.xml:Content-Type=application/xml,Content-Encoding=gzip;requests 给出的 r.content 开头字节 3c3f(已经是明文 XML,requests 解开了 HTTP 层的 gzip)
  对 .xml.gz 自己 gzip.decompress 之后能解析:5 个 URL
  不解压直接 ET.fromstring(r.content):ParseError: not well-formed (invalid token): line 1, column 0

.xml.gz 是文件本身压缩,requests 不会替你解压;Content-Encoding: gzip 是 HTTP 层压缩,requests 自动解开。判断依据是内容的开头字节(1f 8b 表示仍是 gzip),不是文件扩展名。

lastmod

实测 · 本机执行输出
  lastmod '2026-10-11'                      → 2026-10-11 00:00:00+00:00
  lastmod '2026-10-11T08:30:00+08:00'       → 2026-10-11 08:30:00+08:00
  lastmod '2026-10-11T00:30:00Z'            → 2026-10-11 00:30:00+00:00
  lastmod '2026-10-11 08:30'                → 2026-10-11 08:30:00+00:00
  lastmod '11/10/2026'                      → None
  lastmod 'Sun, 11 Oct 2026 00:30:00 GMT'   → None
  lastmod ''                                → None
  3 个子 sitemap 共 15 条 URL;上次抓取时间 2026-10-05:lastmod 更新的 5 条,没有 lastmod 的 5 条(这种条目不能用来判断是否更新,只能当作"需要抓")

datetime.fromisoformat 能解析日期、带时区的时间和 Z 结尾的时间;11/10/2026 或 RFC 2822 日期解析不了。没有 lastmod 的条目(例子里 5/15)不能用来判断是否更新,增量抓取要把它们当作“需要抓”。

大 sitemap

实测 · 本机执行输出
  文件大小 6.8 MiB(未压缩),gzip 后 0.26 MiB
  ElementTree.fromstring:50000 条,312 ms,内存峰值 38.5 MiB
  ElementTree.iterparse(边读边 clear):50000 条,389 ms,内存峰值 4.0 MiB

5 万条 URL 的 6.8 MiB sitemap,fromstring 内存峰值 38.5 MiB,iterparse 边读边 clear 只要 4.0 MiB(多花 77 ms)。协议允许的上限我没有在这里核对(我印象里是 5 万条或 50 MB,以官方文档为准)。

脏数据

实测 · 本机执行输出
  未转义的 & :标准库 ElementTree 报错 ParseError: not well-formed (invalid token): line 1, column 115
  lxml 的 recover=True 能读出 4 条:['http://shop.test/a?x=1=2', 'http://other.test/zzz', '/relative/path', 'http://shop.test/a?x=1y=2']
      http://shop.test/a?x=1=2                → 同主机
      http://other.test/zzz                   → 跨主机(与 sitemap 所在主机不同)
      /relative/path                          → 相对路径,没有主机
      http://shop.test/a?x=1y=2               → 同主机

XML 里 & 没转义是常见错误。标准库直接报错;lxml 的 recover=True 能读,但把 x=1&y=2 读成 x=1=2,甚至把旁边写对了的 x=1&y=2 读成 x=1y=2。另外 sitemap 里可能有跨主机的 URL 和相对路径,要按所在站点校验后再抓。

分步演示

sitemap.xml 解析
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。服务器提供 sitemap 索引、普通 sitemap、.xml.gz、以及带 Content-Encoding: gzip 的 sitemap;解析用标准库 xml.etree.ElementTree 与 lxml 6.1.1;大 sitemap 是合成的 50000 条 URL(6.8 MiB);内存用 tracemalloc 峰值;脏数据是手写的一小段 XML
    • 没有验证:sitemap 协议规定的数量与大小上限(只凭印象);sitemap 里的 image、video、news 扩展命名空间;robots.txt 里的 Sitemap 行与 sitemap 的主机一致性检查;很大的 sitemap 索引(几千个子文件)的并发下载;lxml 恢复模式在其它 libxml2 版本里的表现;defusedxml 等防御 XML 实体炸弹的库(没有安装,也没有测 billion laughs 攻击)
    京ICP备2026067502号-1京公网安备11010502063487号