写一个零依赖的 Markdown 失效链接检查器:锚点、代码块和 HEAD 请求的三个细节

举报
yd_249973085 发表于 2026/11/09 10:13:17 2026/11/09
【摘要】 摘要:文档越写越多,文件改名、标题改字、外链过期都会让链接悄悄失效。我写了一个只用 Python 标准库的小工具 mdlinkcheck 来扫这些问题。本文复盘实现中三个容易踩坑的细节:标题锚点怎么算、怎么避免把代码块里的“链接”当真、在线检查为什么不能只发 HEAD。文中的输出都是我在本机跑代码得到的,没有接入任何云服务。 一、背景写技术文档时最常见的小事故有三类:文件改名了,旁边文档里的...

摘要:文档越写越多,文件改名、标题改字、外链过期都会让链接悄悄失效。我写了一个只用 Python 标准库的小工具 mdlinkcheck 来扫这些问题。本文复盘实现中三个容易踩坑的细节:标题锚点怎么算、怎么避免把代码块里的“链接”当真、在线检查为什么不能只发 HEAD。文中的输出都是我在本机跑代码得到的,没有接入任何云服务。

一、背景

写技术文档时最常见的小事故有三类:

  1. 文件改名了,旁边文档里的相对链接没跟着改;
  2. 标题改了几个字,指向它的 #锚点 就失效了;
  3. 外链过几个月 404。

这些问题肉眼很难发现,放进 CI 里自动查最合适。mdlinkcheck 的目标很克制:纯标准库、几百行代码、本地和 CI 都能用,退出码 0 表示没问题、1 表示有失效链接、2 表示参数错误。代码分成解析(parser.py)、检查(checker.py)、在线请求(online.py)三层。

二、细节一:锚点要自己“复刻”渲染规则

检查 [配置](guide.md#配置) 时,要知道目标文件里有哪些锚点,而锚点是渲染器根据标题生成的。我按 GitHub 的规则实现 slugify:转小写,空格变连字符,保留各语言的字母、数字和下划线,其他标点和符号丢弃。实际跑出来的结果是:

标题 锚点
快速开始 快速开始
Hello, World! hello-world
API & SDK 说明 api--sdk-说明
🚀 Features -features

第三行很容易让人意外:& 被丢弃后,两侧的空格各变成一个连字符,所以出现了双连字符。第四行同理,emoji 被丢掉,前面的空格留下了一个开头的连字符。如果工具把这些“修正”得好看一点,反而会和真实渲染结果对不上。

重复标题也要处理:同名标题第二个起要加 -1、-2 后缀。我用一个计数字典实现,对三个都叫“重复”的标题,得到的锚点集合是 重复、重复-1、重复-2。另外,HTML 里手写的 <a id="x"> 或 name="x" 也要算作锚点,所以解析时对每一行都用正则额外扫一遍。

需要说明的是,这是我按规则做的“够用”实现,不是完整的 CommonMark 解析器,一些极端写法没有覆盖。

三、细节二:先把代码块“抹掉”,再找链接

文档里经常有示例:

```
[x](no.md)
```

示例里的链接不是真链接,检查它只会产生误报。我的做法是在提取链接之前,先把围栏代码块(``` 或 ~~~)和行内代码替换成空行或空格,并且保持行数不变,这样后面报出来的行号仍然对得上原文件。

我用一段混合文本验证:三个重复标题、一个代码块里的 [x](no.md)、一段行内代码里的 [y](no2.md)、一个正常链接 [ok](a.md),以及一个没有定义的引用 [引用][nope]。解析结果是:只提取到 a.md(第 8 行),未定义的引用 nope 被单独报出(第 9 行),代码里的两个“链接”都被忽略了。

有个小技巧值得记下:围栏的关闭条件要同时满足“字符相同”和“长度不小于开头”,否则用四个反引号包住三个反引号示例的写法就会提前结束。

四、细节三:在线检查不能只发 HEAD

外链检查最直觉的写法是发 HEAD,省流量。但现实里有两种相反的问题:有的服务器不支持 HEAD(返回 405 等),有的服务器 HEAD 和 GET 行为不一致。

我的策略是:先 HEAD;如果出错或返回 400、403、404、405、429、5xx 这类“HEAD 上常被错误返回”的状态码,就回退成 GET 再确认;遇到网络错误、429、5xx 时按指数退避重试,默认重试 2 次。

为了验证,我在本机起了一个小 HTTP 服务模拟五种情况,实测请求序列如下:

路径 模拟的行为 实际请求 判定
/ok 一切正常 HEAD 通过(200)
/nohead HEAD 返回 405,GET 正常 HEAD、GET 通过(200)
/flaky 前两次 GET 返回 503,之后正常 HEAD、GET、HEAD、GET 重试后通过
/gone 一直 404 HEAD、GET 失败(404)
/headlies HEAD 返回 200,GET 返回 404 HEAD 通过(200)

最后一行是这个策略的已知盲区:如果服务器的 HEAD 谎报 200,工具只发了 HEAD 就信了,漏掉了真实的 404。这种服务器不多,但确实存在。要堵这个洞只能每个链接都发 GET,代价是流量和耗时翻倍,我选择了默认不堵,把它写进了局限里。

五、可复用的结论

  1. 检查锚点就要复刻渲染规则,别“美化”。把标点、emoji、重复标题的边界用例写进测试。
  2. 解析前先抹掉代码,并保持行号,误报会少一大截,报告也更好定位。
  3. 外链检查是“概率性”的:HEAD 优先、GET 兜底、退避重试,仍然会有漏判,要在文档里写明。
  4. 用本地小服务模拟各种异常响应,比直接去打真实网站稳定得多,也不会给别人的服务器添麻烦。

六、局限与下一步

目前它只检查链接本身,不检查链接内容是否相关;相对路径的大小写在不同系统上行为也不同,这一点我还没系统测试。下一步打算补上更多真实仓库的文档做回归,看看误报率到底如何。如果你也在维护 AI Agent 项目的文档,不妨把这类检查放进 CI,毕竟用户第一次接触项目,往往就是从点开一个链接开始的。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。