站点地图分片:一万篇文章的sitemap切法

一、五万篇文章,一个 sitemap 文件装不下

我站上的文章数过了一万五之后,Google Search Console 的抓取统计就开始不对劲了。提交的 sitemap 显示「已发现 15000 个网址」,但「已编入索引」的数字卡在八千左右不动,而且那个 sitemap 文件的下载状态经常显示「暂时无法获取」。

一开始我以为是内容质量问题,去优化标题和正文,效果很小。后来我用 curl 量了一下 sitemap 的大小:9.4MB,未压缩。那个瞬间我明白了问题在哪——单个 URL 集合文件太大,爬虫每次来都要重新下载整份,而连接一断就得从头再来。对爬虫来说,这个文件是个沉重的负担,不是一份索引。

这篇文章写 sitemap 分片的完整做法:什么时候该分、怎么分、索引文件怎么写、lastmod 怎么维护、以及那十几个「看着正确其实无效」的细节。如果你也遇到过「提交了但收录不涨」,这篇大概率能对上你的问题。

二、先看清 sitemap 的硬限制

这些数字是协议规范里写死的,不是建议:

单个 sitemap 文件最多 50,000 个 URL,未压缩的字节数最多 50MB。两个限制哪个先到都算超。所以「我有四万个网址,可以放一个文件」这句话是错的——四万个网址如果每篇正文带摘要,很容易超过 50MB。

一个 sitemap 索引文件最多指向 50,000 个 sitemap 文件,所以理论上限是 25 亿个 URL,对个人站完全够用。

索引文件里不能混放普通 URL。索引文件只允许 <sitemap> 条目,随便塞一个 <url> 进去,整份索引会被判为格式错误,然后你提交上去的那个文件在 GSC 里就会一直「待处理」。

还有一个容易忽略的:同一个 sitemap 里所有 URL 必须同源。协议要求 sitemap 中列出的所有网址都属于同一个主机(含协议),不能 www 和裸域混着写,http 和 https 更不能混。我的站最初就踩了这个——老文章里留着一批 http:// 的链接,混在 https 的清单里,那一整个 sitemap 的解析在 GSC 里就是「有 2 个错误」。

判断要不要分片,我的经验阈值是三千个 URL 就该考虑,而不是等到五万。原因不是协议限制,而是抓取效率:一份几百 KB 的 sitemap,爬虫一次请求就能拿完,解析快、断点成本低;一份 9MB 的文件,下载要十几秒,中间任何网络抖动都会导致整次抓取作废,而爬虫的重试是有预算的。分片换来的抓取稳定性,远比省下的几个文件描述符值钱。

三、分片的三种切法,以及为什么最后选了「按月」

分片方案本质上是「按什么维度把 URL 分成组」。我试过三种:

方案一,按数量切:每 5000 篇一个文件,sitemap-1.xml、sitemap-2.xml……最直接,但有个致命问题:新文章会改变所有文件的内容。第 15001 篇文章加入后,它落在第三个文件里,但那之后的每一个文件都要重排——因为你按 id 顺序切,插入会推动边界。结果是每次发新文章,所有分片文件的 lastmod 都变了,爬虫会把它们全部重新下载一遍。这等于没分。

方案二,按分类切:每个分类一个 sitemap。听起来合理,实际也不好——分类会重命名、会合并、一个新分类会从零开始,而且各分类的量极不均衡(我的「技术教程」有八千篇,「站长经验」只有三百篇),还是需要二次切分。

方案三,按月切:sitemap-post-2026-10.xml 装 2026 年 10 月发布的文章。这是我现在用的,优点非常明显:一个文件的内容在其对应的月份结束后就冻结了。十月份的文章全部发完,sitemap-post-2026-10.xml 从此不再变化,它的 lastmod 也就永远不变,爬虫下载过一次就不需要再来。只有当月那个文件是「活的」。

按月切的另一个好处是优先级天然清晰。索引文件里我可以把最近三个月排在最前面,让爬虫优先抓新内容;老月份的排在后面,一年才动一次。这和搜索引擎「新内容优先」的抓取策略天然对齐。

如果某个月的文章超过 5000 篇(我没有遇到过,但你要考虑),就在月份后面加序号:sitemap-post-2026-10-1.xml、-2。别用「第一章/第二章」这种会变化的切法,序号往后追加就行。

独立页面的处理:标签页、分类归档页、作者页这类,量可能很大(我的 tag 页有两千多个),而且每天都在变。这些我单独放一组文件,不按月切,而是按「只保留有实质内容的」过滤后每 5000 个一个文件,命名 sitemap-tax-1.xml。并且这一组允许 lastmod 每天变,因为它们确实每天都变。

四、单条记录该写哪几个字段

先看一条标准的 <url>:

<url>
  <loc>https://www.example.com/1234.html</loc>
  <lastmod>2026-10-18T09:12:33+08:00</lastmod>
  <changefreq>weekly</changefreq>
  <priority>0.8</priority>
</url>

四个字段里,只有 loc 是必须的,其余三个都是可选的提示,而其中两个基本没用:

changefreq 和 priority 已被 Google 明确忽略。官方文档写得清楚,这两个值不再被用于抓取决策。我早年在它们上面花过不少心思:给首页 1.0、给文章 0.8、给标签页 0.3。回头看全是无用功,不如把精力放在 lastmod 准不准上。现在我的生成器干脆不输出这两个字段,文件小了将近三分之一。

lastmod 是唯一真正有意义、也唯一真正容易被写错的字段。它的作用是告诉爬虫「这个页面自上次以来变了没有」,如果写得不准,爬虫要么反复来抓没变的内容(浪费预算),要么以为页面没变而不来(新内容不收录)。

三个常见错误:

第一,每次生成 sitemap 都把 lastmod 写成当前时间。这是最普遍的错误,几乎是所有「自动生成 sitemap」脚本的默认行为。结果就是所有 URL 的 lastmod 永远等于今天,爬虫第一次会信,第二次发现「这个站所有页面每天都变」,之后就彻底忽略这个字段了——你等于亲手废掉了自己唯一的有效信号。

第二,lastmod 用文章的发布时间,而不是修改时间。我早期就是这样,后来发现一批老文章被我修正过错别字,实际上内容变了,但 lastmod 还是三年前,爬虫不会重新抓,Google 索引里一直是错字版本。正确做法是维护一个「内容最后修改时间」字段,在编辑保存时更新它,且只在正文真的发生变化时更新——后台改个浏览量、调个推荐位不该触发。

第三,格式不合法。lastmod 必须是 W3C Datetime 格式,最完整的是 2026-10-18T09:12:33+08:00。只写日期 2026-10-18 是允许的,但精度降低意味着同一天内的修改无法被识别。写成本地时间又没带时区(2026-10-18 09:12:33)是无效的,空格不能代替 T。这一条我的站点就踩过:PHP 的 date('Y-m-d H:i:s') 直接输出,整份 sitemap 的 lastmod 全部无效,而 GSC 只是提示「有一个警告」,不会明确告诉你是格式问题。

顺带说一个实现细节:如果站点时区是 UTC+8,用 date('c') 输出的是带 +08:00 的正确格式,可以直接用。但如果站点配置里时区设置错了(很多虚拟主机默认 UTC 而内容按北京时间写),lastmod 会整体偏八小时,出现「未来时间」。搜索引擎对未来的 lastmod 是直接忽略的,这又是一个静默失效。

五、索引文件与提交策略

索引文件的格式比想象中简单,只有一层:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://www.example.com/sitemap-post-2026-10.xml</loc>
    <lastmod>2026-10-27T01:00:00+08:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://www.example.com/sitemap-post-2026-09.xml</loc>
    <lastmod>2026-10-01T00:05:00+08:00</lastmod>
  </sitemap>
</sitemapindex>

几个要点:

索引文件的 lastmod 应该等于该分片内最新的 lastmod,而不是生成索引的时间。这样才能起到「这个分片变了没有」的作用。如果按月切,非当月的分片 lastmod 就是固定的。

绝对不要用中文文件名或者带空格的路径。虽然协议允许 UTF-8,但实测中不少抓取工具在遇到非 ASCII 路径时会出错,而错误反馈在 GSC 里只显示「无法获取」。纯 ASCII、短横线分隔,是最安全的。

提交给 GSC 的应该是索引文件,不是各个分片。提交索引就够,GSC 会自动发现它指向的所有分片。「已发现的网址数」会变成所有分片的总和。有人两边都提交,导致同样的 URL 被报告两次,反而干扰了「已编入索引」的统计口径。

robots.txt 里只写索引文件一行:

Sitemap: https://www.example.com/sitemap.xml

不要写多行分片地址,一是会随月份增长无限膨胀,二是没有任何额外收益。

已删除或已下线的月份分片要从索引里移除,并且返回 404 而不是 200 空文件。返回 200 的空 sitemap 会让爬虫认为「这个分片现在没东西了」,但它仍然会定期回来检查;返回 404 则会被彻底遗忘。两种都可以接受,但不要返回 500 或者重定向到首页——那会让爬虫把你整站的 sitemap 当成不健康的。

六、动态生成还是静态生成

我的站用的是 PHP 按需生成,也就是 /sitemap-post-2026-10.xml 这个地址被请求时才拼出 XML。这条路能走,但有几个硬性要求:

第一,一定加缓存。抓取高峰期(比如你刚在 GSC 点了重新提交),爬虫可能几秒内请求多次。每次让 PHP 去查询一万五千行并拼 XML,CPU 立刻打满还拖慢整站。我在 Nginx 层面对 sitemap 路径做了 proxy_cache,缓存一小时,并给当月的分片单独设一个较短的缓存时间(比如十分钟)。这样内容更新最多滞后十分钟,代价几乎为零。

第二,用流式输出而不是一次性拼字符串。把一万五千条全部建成一个字符串,再 echo 出来,内存峰值会到几十 MB。改成 while 循环边查边 echo,内存恒定在几百 KB。配合 SELECT 里的 LIMIT 5000 分段(按 id 游标翻页,不要用 OFFSET,翻到后面会全表扫),大月份也不会有问题。

第三,别让生成过程被搜索引擎的超时打断。如果生成一份文件要二十秒,爬虫大概率等不到就断开,然后记录为「无法获取」。加缓存之后首次生成仍然慢,我的做法是让 cron 每小时预热一次:对每个活跃分片发一个本地请求,把缓存填上,这样爬虫来的时候永远是缓存命中。

另一种是静态生成:cron 每天把文件写到磁盘,由 Nginx 直接 serve。这个方案更快、更稳,缺点是「新文章发布后要等 cron 才出现在 sitemap 里」。可以折中——发布钩子里直接触发那个月的分片文件重新生成,因为只有当月的那一个文件会变,成本极低。我用的是这个思路:当月分片走 PHP 动态但短缓存,历史月份走静态文件,一年只写一次。

如果站点规模再大一个量级(十万篇以上),就该考虑 gzip 存储了。sitemap 允许用 .gz 压缩后提供,只要文件名以 .gz 结尾、内容是 gzip 的 XML,爬虫会认。一份 9MB 的文件压完只有 800KB,用 gzip -9 提前压好放磁盘,对带宽和抓取速度都是明显改善。索引文件里指向的名字也要相应改成 sitemap-post-2026-10.xml.gz。

七、上线后的验证与观察

分片上线后不能只看「提交成功」就完事,要按下面的顺序逐项验证:

第一步,直接 curl 拿原始内容。不要用浏览器看——浏览器会把 XML 渲染成树,掩盖编码问题和格式错误:

curl -sI "https://www.example.com/sitemap.xml"
curl -s "https://www.example.com/sitemap.xml" | head -20

看三件事:HTTP 200、Content-Type 是 application/xml 或 text/xml、开头就是 XML 声明而没有 BOM 和空白。**BOM 是个隐蔽的杀手**:某些编辑器保存文件时会加 UTF-8 BOM,XML 解析器遇到 BOM 会直接报错,而浏览器显示起来完全正常。

第二步,验证 XML 格式合法性。用 xmllint 一行就能查出所有结构问题(未闭合标签、非法字符、命名空间缺失):

curl -s "https://www.example.com/sitemap-post-2026-10.xml" | xmllint --noout -

没有输出就是合法。报错会指出行号,比在 GSC 里等两天看「有 1 个错误」高效得多。

第三步,抽样检查 URL 是否都是 200。sitemap 里列出 404 的页面是很常见的错误——文章删了但生成器还从缓存表里取。抽十条用 xargs 批量 HEAD 一下就知道了:

curl -s "https://www.example.com/sitemap-post-2026-10.xml" \
  | grep -oE '<loc>[^<]+' | sed 's/<loc>//' | head -20 \
  | xargs -I{} curl -s -o /dev/null -w '%{http_code} {}\n' {}

输出里出现非 200 的,要么修页面,要么从 sitemap 里剔除。这一步的价值很高:sitemap 里混进大量 404,会让你整个站的可信度下降。

第四步,在 GSC 里重新提交并看趋势。提交后不要每天点「重新提交」,那没有任何加速作用。观察两个数字就够了:「已发现的网页数」应该接近你实际的文章总数,「已被 Google 编入索引的网页数」应该在一到三周内稳步接近它。如果发现数远大于收录数且长期不动,那才是内容或结构问题,不是 sitemap 的问题——这时候该去看那些未被收录的页面的原因(重复内容、canonical 冲突、内链太少)。

另外看一眼「抓取统计信息」里的平均响应时间。分片后如果发现爬虫的抓取量反而下降,先检查是不是服务器变慢了,而不是 sitemap 写错了。

八、几个静默失效的细节

下面这几条,任何一条都会让你的 sitemap 白做,而报错信息通常含糊到看不出原因:

第一,域名不一致。sitemap 里写 https://www.example.com/,但站点实际访问是 https://example.com/(不带 www),或者反过来。这会导致 GSC 认为你提交的是「另一个站点的 sitemap」。检查方法是:sitemap 里的每一条 URL,点开应该和一个不完全重定向到自身。curl -sI 看有没有 301,一条都不能有。

第二,URL 里的特殊字符没转义。& 在 XML 里必须写成 &amp;,否则整份文件格式错误。带中文的 URL 要么做 percent-encoding,要么确保文件是 UTF-8 并且声明一致——两者混用会出现「页面能打开但 sitemap 报错」的诡异情况。我最稳妥的做法是站点 URL 一律用纯 ASCII slug,从根上避免这个问题。

第三,XML 声明与编码不匹配。声明写 encoding="UTF-8" 但内容实际是 GBK 输出(老 PHP 项目很常见),浏览器宽容,解析器不宽容。用 file -i 确认一下文件编码,比猜快。

第四,CDN 返回了 HTML 错误页。这是最坑的一种:源站正常,但 CDN 在缓存回源失败时返回了一个 200 的「维护中」HTML 页面,GSC 拿到的是一个 HTML 而不是 XML,报「无法解析」。用 curl -s ... | head -5 看到 <!DOCTYPE html> 就知道是这个原因。解决方法是给 sitemap 路径设置「不缓存错误响应」。

第五,分片文件名被规范成 404 却不自知。索引里写的是 sitemap-post-2026-10.xml,实际这个月的文章还没开始写,文件不存在。这种情况应该让索引里暂时不放这个分片,等当月有文章时再生成——而不是生成一个空文件。空 sitemap 是合法的但它没有任何意义,还会让 GSC 报告「已发现 0 个网址」的警告。

九、这套方案的实际效果

从单文件 9.4MB 改成按月分片之后,我这边观察到的变化是:GSC 里「无法获取」的错误消失了;发现数从九千涨到接近全量;收录数在三周内从八千涨到一万三。抓取量的曲线变得更平稳,不再出现「某天突然抓了几万次然后三天不动」的锯齿。

我无法证明这些变化百分之百来自分片——同期我也修了 canonical 和内链。但至少有一点是确定的:爬虫愿意来,是因为来一趟的成本变低了。一份 9MB 的文件和一份 200KB 的文件,对爬虫来说是完全不同的两个东西,前者是负担,后者是清单。

最后提一句:分片是基础设施,不是收录的万能药。如果页面的正文质量差、内链结构混乱、重复内容严重,再漂亮的分片也不会让它们被收录。分片解决的是「告诉爬虫有什么」,收录解决的是「让爬虫觉得值得收」,这两件事不能混为一谈。

Last modification:October 27th, 2026 at 01:26 pm

Leave a Comment