写 Markdown 文档时间长了,你大概率会遇到这种尴尬:内容明明很清爽,一发布到网页、导出 PDF,或者复制进某些办公系统,所有超链接瞬间变成一串蓝色下划线,把排版搅得乱七八糟。尤其在做文档交付、知识库归档、技术方案整理这类活时,外链要么已经失效,要么链接本身就成了干扰项。这事的解法很简单——把 Markdown 里所有超链接去掉,只保留可读的文字部分。
关于去掉 markdown 网页中所有超链接,网上能搜到的答案大多是一句正则替换,但真正落地时你会发现,链接的写法远不止一种,命令行怎么组合、正则怎么避坑、批量文件怎么处理,都有不少讲究。这篇我就从命令、脚本到各种坑,把这条链路完整走一遍,保证你能直接照着抄。
1. 先搞明白你要去掉的是哪一类超链接
1.1 为什么会有这种操作需求
很多人刚开始接触 Markdown 时,觉得链接语法是刚需,写文档恨不得每句话都挂个出处。可实际用一段时间就会发现,链接在某些场景下反而是累赘:
- 导出的 PDF 或打印稿里,超链接没法点击,只显示一堆丑陋的下划线,文字还变蓝,阅读体验很差。
- 发布到某些内容平台(比如公众号编辑器、公司内网 Wiki),外部链接会被自动过滤,留着反而产生一堆残缺代码。
- 做文档交接或归档时,旧链接大概率已经失效,与其留一堆死链,不如全部清理成纯文本。
- 需要把 Markdown 内容批量搬运到不支持 Markdown 语法的系统时,链接语法不转义就会直接暴露成原始文本,特别难看。
这时候,写个命令或者小脚本批量“拆链”,就成了刚需。我自己最早的需求来自一次知识库整理,手头 200 多个 md 文件,里面链接五花八门,手动清理得干到天亮,后来花半小时把命令和脚本理顺,跑完也就一眨眼的工夫。
1.2 Markdown里其实藏着三种“链接”
如果你以为 Markdown 链接只有[文字](URL)这一种写法,那可能连需求都没分析清楚就开始处理了。实际管用的“超链接”至少分三类:
- 行内式链接:
[文字](https://example.com),这是最常见的一种。 - 参考式链接:
[文字][id],然后在文档任意位置写上[id]: https://example.com来定义目标地址。 - 自动链接和裸地址:
<https://example.com>、<foo@bar.com>,以及某些渲染器下直接出现的https://example.com裸链接。
不同的写法对应完全不同的正则和命令。只处理行内式,参考式链接的文字会保留,但底部定义列表会残留;只处理<...>自动链接,普通链接又纹丝不动。所以开工前,先花两分钟扫一下你的文档里到底混了哪几种链接,再决定要用哪些命令组合,这能省掉不少来回试错的工夫。
2. 用sed命令一行搞定行内链接
2.1 最小可用的替换命令怎么写
在 Linux 或 macOS 的命令行里,sed是最顺手的文本替换工具。Markdown 行内链接的结构是[文字](链接地址),目标是把[文字](链接地址)整个替换成文字。对应的 sed 命令长这样:
sed -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' input.md > output.md我来拆一下这个正则,让不熟悉的人也能看懂:
\[匹配左方括号,\]匹配右方括号,因为方括号在正则里是特殊字符,所以必须转义。([^]]*)是第一个捕获组,匹配链接文字。[^]]*表示“连续不含右方括号的任意字符”。\(和\)匹配链接地址外面的圆括号,同样需要转义。([^)]*)是第二个捕获组,匹配链接地址,用“连续不含右括号的任意字符”来约束。- 替换部分
\1表示只保留第一个捕获组,也就是链接文字。
-E参数是让 sed 用扩展正则表达式,这样括号和+这类符号不用整天加反斜杠,可读性高很多。命令执行后,input.md里所有行内链接就变成了纯文字,输出到output.md。先输出到新文件的好处是,万一正则写错了,原文件不至于直接被废掉。
如果确认正则没问题,想原地修改原文件,就加-i参数:
sed -i -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' input.mdmacOS 的sed默认语法和 GNU 版本略有差异,用-i时建议写成-i '',也就是sed -i '' -E '...' input.md,不然会报错。这个坑我当年踩过,给苹果用户提前提个醒。
2.2 保留链接文字和直接删除有什么区别
去掉链接,有两种理解:一种是把整段链接语法删除,链接文字也不要;另一种是只要把[文字](URL)变成文字,链接信息全部剥离,但文字内容保留。
我建议绝大多数场景选第二种。原因很实际:链接文字通常承载了语义,比如“点击查看用户协议”,你把整句删了,读者就不知道这里原本在说什么;更常见的是链接文字就是名词本身,比如某个术语、某篇文章的标题,删掉后正文缺了一大块,还得重新补。
如果你确实需要把链接文字也删掉,那只要把替换部分改成空字符串就行:
sed -E 's/\[[^]]*\]\([^)]*\)//g' input.md注意这时候正则不需要捕获组了,\[[^]]*\]\([^)]*\)整体匹配整段链接语法,替换成空。但我不太推荐这种操作,除非你确定链接文字毫无价值。另一个折中方案是把链接地址以脚注形式保留在链接文字后面,比如[文字](URL)变成文字 (URL),这样信息不丢,又去掉了超链接功能。命令如下:
sed -E 's/\[([^]]*)\]\(([^)]*)\)/\1 (\2)/g' input.md这种“保留 URL 但不带跳转”的格式,在导出 PDF、打印场景里其实挺实用,读者真需要时还能手动复制地址。
2.3 顺手把图片链接和参考式链接也处理掉
很多人第一次跑完行内链接的正则,以为万事大吉,结果一检查发现图片语法全乱了。原因很简单:图片链接的语法是,它的结构只是比普通链接多了一个感叹号!,你的正则不会区分它,会把[alt](image.png)匹配掉,留下一个孤零零的!alt。
处理顺序很关键:先用单独的正则处理图片,再处理普通链接。
sed -E 's/!\[[^]]*\]\([^)]*\)//g' input.md > output.md sed -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' output.md > final.md第一条把整条图片语法删掉,因为图片没了链接地址也没法显示,只留 alt 文字意义不大;如果你场景特殊需要保留 alt 文字,把替换部分改成\1即可,但要确保前面的正则把!也吃进去,别留下!alt这种半吊子。
参考式链接处理起来要两步走。第一步先把[文字][id]替换成文字,第二步把底部定义行[id]: URL删掉:
sed -E 's/\[([^]]*)\]\[[^]]*\]/\1/g' input.md sed -E '/^\[[^]]*\]:[[:space:]]*http/d' input.md第一条命令里的\[[^]]*\]匹配参考标识符[id],整体替换成链接文字;第二条用地址定则/^\[[^]]*\]:[[:space:]]*http/d,凡是以[某id]: http开头的行,直接删除。如果你的参考式链接定义行可能没有http开头,而是相对路径,把/http这个条件去掉就行,改成/^\[[^]]*\]:/d。注意这两条命令最好按顺序执行,先替换正文里的引用,再删定义列表,不然正文里残留[文字][id]标记,就不好收拾了。
3. 批量清理整个目录的链接:find + sed 组合拳
3.1 Linux/macOS 下一条命令遍历所有md文件
单文件处理很简单,可实际工作中基本都是几十上百个 Markdown 文件堆在一个项目目录里,这时候就得用find配合sed来做批量处理。
最常用的写法是这样:
find . -name "*.md" -exec sed -i -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' {} +解释一下:find .从当前目录开始查找,-name "*.md"限定只找 Markdown 文件,-exec sed ... {} +表示把所有匹配到的文件名批量传给sed -i做原地修改。用{} +而不是{} \;的好处是它会一次性把所有文件路径作为参数传给一条 sed 命令,性能更好;如果文件数量特别大,也可以改用{} \;逐个执行,更保险但稍慢。
需要注意的是,如果目录下有二级目录、三级目录,find默认会递归查找,所以这个命令会把项目里所有.md后缀的文件都处理一遍。如果只想处理当前层,不递归子目录,加一个-maxdepth 1参数:
find . -maxdepth 1 -name "*.md" -exec sed -i -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' {} +跑批量修改前,我的习惯是先跑一个不带-i的预览版本,把改动结果输出到另一个文件或者直接输出到终端,确认正则效果没问题,再加-i真正落地。比如先跑:
find . -name "*.md" -exec sed -E 's/\[([^]]*)\]\(([^)]*)\)/\1/g' {} \; | head -50或者在单个文件上用diff对比修改前后,都是很稳妥的做法。
3.2 Windows PowerShell 的等价写法
Windows 用户没有原生的 sed,但 PowerShell 处理这种事也很快。核心逻辑是用正则替换整个文件内容,然后重新写回。
Get-ChildItem -Path . -Filter *.md | ForEach-Object { $content = Get-Content $_.FullName -Raw -Encoding UTF8 $content = [regex]::Replace($content, '\[([^\]]*)\]\([^)]*\)', '$1') Set-Content $_.FullName -Value $content -Encoding UTF8 }这段脚本做了三件事:用Get-ChildItem找到所有.md文件;Get-Content -Raw把整个文件一次性读进变量;[regex]::Replace执行正则替换,这里用$1表示第一个捕获组,和 sed 的\1稍有区别。最后Set-Content把处理后的内容写回原文件。
注意 PowerShell 5.1 及以下版本里,Set-Content -Encoding UTF8默认可能会带 BOM 头,这对 Markdown 渲染一般没影响,但如果你用 Git 管理文档,BOM 头的变动会被记录成整个文件差异。介意的话可以改用-Encoding UTF8NoBOM,这在 PowerShell 7 里是默认行为。
PowerShell 的正则默认也是大小写不敏感,但替换逻辑和 sed 差别不大。把上面脚本里的正则换成参考式链接的处理版本同样适用,比如:
$content = [regex]::Replace($content, '\[([^\]]*)\]\[[^\]]*\]', '$1') $content = [regex]::Replace($content, '(?m)^\[[^\]]*\]:.*$', '')第二行里的(?m)表示多行模式,让^和$匹配每一行的开头和结尾,这样就能把底部定义列表一行行删干净。
3.3 没有命令行基础的编辑器替代方案
如果你看到命令行就头疼,或者在图形界面环境里不方便开终端,还有捷径:直接用支持正则替换的文本编辑器。
以 VS Code 为例,按Ctrl+H打开替换面板,勾选右上角的.*正则模式,然后在查找框输入:
\[([^\]]*)\]\([^)]*\)替换框输入:
$1最后点“全部替换”,当前文件里的所有行内链接就都变成纯文字了。VS Code 的替换语法用的是$1,不是\1,这一点和 sed 不一样,别搞混。如果你的目录下有大量文件,可以进一步用 VS Code 的“在文件中替换”功能(Ctrl+Shift+H),一次跨所有打开的文件甚至整个文件夹进行替换,效果和命令行批量处理一样,但全程图形化,适合不习惯终端的人。
同样的思路也适用于 Typora、Obsidian 等编辑器。Typora 本身没有全局替换入口,但把文本复制到 VS Code 里处理再粘贴回去,也是不少人常用的笨办法。我不建议在浏览器在线工具里直接替换整篇文档,除非内容不敏感,否则数据安全不值得省这点事。
4. 渲染成HTML之后再剥掉链接的更稳做法
4.1 为什么渲染后处理比改源码更可靠
直接改 Markdown 源码最大的问题在于正则的局限性。Markdown 语法看起来简单,但实际写的时候总有各种意外:链接文字里嵌套了括号、链接地址里带着参数含括号、链接文字跨了行、用了 HTML 标签写的<a href="...">链接。面对这些情况,纯粹靠正则一条条匹配,很容易漏掉或者误伤。
一个更稳的思路是:先把 Markdown 渲染成 HTML,再用 HTML 解析器去找到所有<a>标签,把它们替换成纯文本。因为一旦渲染成 HTML,链接的结构就标准化了——不管你是用行内式、参考式还是自动链接写的,最终表现都是<a href="...">文字</a>,按标签处理就一劳永逸。
这种做法的额外好处是能顺便处理<a>标签的各种变体,比如带title属性、带target="_blank"的标签,都能统一剥掉。如果用正则去匹配 HTML,遇到嵌套或不规则标签又是一场噩梦,但用解析器就没这些问题。
4.2 用 Python + BeautifulSoup 去掉 标签
Python 是目前处理文本最顺手的语言,配合markdown库和BeautifulSoup,整个流程非常直白。先安装依赖:
pip install markdown beautifulsoup4然后写个脚本:
import markdown from bs4 import BeautifulSoup with open('input.md', 'r', encoding='utf-8') as f: md_text = f.read() html_body = markdown.markdown(md_text, extensions=['extra']) soup = BeautifulSoup(html_body, 'html.parser') for a in soup.find_all('a'): a.replace_with(a.get_text()) clean_html = str(soup) print(clean_html)脚本逻辑分四步:
- 读取 Markdown 源文件。
- 用
markdown.markdown()渲染成 HTML,extensions=['extra']是为了支持表格、脚注等扩展语法,让渲染结果更完整。 - 用
soup.find_all('a')找到所有<a>标签,replace_with(a.get_text())把标签替换成它内部的文字内容。 - 输出处理后的 HTML。
需要注意markdown.markdown()默认只会生成 HTML 片段,不会自动补全<html>、<head>这些结构。如果你要生成完整网页,可以继续用模板包一层,或者在脚本里手动加:
full_html = f"<!DOCTYPE html><html><head><meta charset='utf-8'></head><body>{html_body}</body></html>"如果你不想用 BeautifulSoup,非要用正则硬刚渲染后的 HTML,最简版本是这样:
import re clean_html = re.sub(r'<a[^>]*>([^<]*)</a>', r'\1', html_body)这个正则的问题是内部如果要保留其他内联标签(比如加粗<strong>),就会漏掉。而且遇到嵌套的<a>标签基本处理不了。所以我的建议很明确:能上解析器就上解析器,正则只适合快速一次性处理,不适合当成工具长期用。
拿到干净的 HTML 之后,你也可以反向再把它转换回纯 Markdown,或者直接对接后续的 PDF 生成、发布流程,这个看你的具体需求了。
4.3 VSCode 预览和替换阶段的辅助技巧
如果你用 VS Code 写 Markdown,其实没必要每次都跑到终端执行脚本。日常处理少量文件时,VS Code 里的正则替换已经足够;但如果你要处理整个项目,又不希望命令行那么别扭,可以装一个名叫“Find and Transform”的插件,它允许你为每个文件执行更复杂的替换逻辑,甚至用 JS 处理捕获组,比单纯的正则替换灵活得多。
另外,VS Code 自带的 Markdown 预览(Ctrl+Shift+V)也能帮你判断替换结果是否正常。我通常的处理流程是:先全局预览一遍文档结构,记住哪些位置有明显链接;然后用正则替换跑一遍;再用Ctrl+Shift+V预览,确认没有残留链接语法、图片是否完好、文字内容是否完整。预览时如果发现![alt]被误伤成!alt,马上撤销接着改正则。
还有个小技巧:用 VS Code 的“在文件中替换”跨整个文件夹替换时,注意点击右侧“替换”按钮旁边的小箭头,确保选择了“包含排除的文件”选项,避免把.git目录或者node_modules目录下的文件也一并改了。别问我为什么强调这点,问就是我曾经把依赖包里的 Markdown 注释清了一遍,最后只能git checkout恢复。
5. 正则避坑指南:这五个坑我全踩过
5.1 贪婪匹配差点把正文吞光
正则里最经典的坑就是贪婪匹配。假设你把行内链接的正则写成\[.*\]\(.*\),遇到下面这行:
[链接一](url1) 和 [链接二](url2) 都是参考资料.*会尽可能多地匹配字符,导致正则从第一个[链接一]开始,一直匹配到最后一个url2)结束,把中间“和 链接二 都是参考资料”全部当成匹配对象吞掉,替换结果就只剩下一个空壳。
正确的写法是用排除字符集[^]]*来约束链接文字,而不是用.*。因为链接文字里正常情况下不会出现右方括号,用[^]]*会让正则匹配到第一个]就停下来,这样一条链接只匹配自身,不会殃及后面的内容。同理,链接地址部分用[^)]*也是这个道理。
5.2 图片链接 ![alt] 被误伤
这个坑前面提过,但值得再强调一次,因为它的破坏性很强:只要文档里有图片,你跑普通链接替换,图片语法一定会出问题。比如:
普通链接正则\[([^]]*)\]\([^)]*\)会匹配到[架构图](./images/arch.png)这一整段,替换成架构图,结果图片语法就变成了!架构图,渲染出来既不是图片,也不是干净文字,而是莫名其妙的感叹号加文字。
解决方法是分两步走,或者用负向断言。分两步走就是先处理图片、再处理普通链接,这个前面已经写了。如果你用 Perl 或者支持负向断言的工具,也可以一步到位:
perl -pe 's/(?<!!)\[([^]]*)\]\([^)]*\)/\1/g' input.md(?<!!)表示“前面不能是感叹号”,这样图片链接![...]就不会被匹配到。但我个人还是觉得分两步更稳,因为命令更直观,别人接手你的脚本时也能看懂。
5.3 链接地址里带着括号的时候
Markdown 标准语法里有个经典问题:链接地址如果包含括号,比如维基百科的 URL 经常带(disambiguation),或者参数里带编码括号,正则用[^)]*会在第一个)处提前截断,留下半截 URL。
看这个例子:
[词条](https://en.wikipedia.org/wiki/Markdown_(markup))[^)]*会匹配到https://en.wikipedia.org/wiki/Markdown_,正好在(markup)的右括号前停下,最后整个正则匹配失败,链接处理不了。
一种简单的兼容方案是放宽 URL 的匹配条件,比如[^\s)]*,意思是“不包含空白符和右括号的任意字符”,这对大多数 URL 够用,但仍然处理不了 URL 里既有括号又有空格的情况。说实话,如果链接地址里括号嵌套太复杂,纯正则很难完美解决。这时候我建议直接用第 4 节的 Python 渲染方案,让 Markdown 解析器帮你去解析链接,而不是自己死磕正则。工具选型就这么回事:正则适合快速处理 90% 的常规情况,剩下 10% 复杂场景,换解析器是最省时间的。
5.4 多行链接、自动链接和邮箱链接
有些人写长链接文字时会手动换行,比如:
[这是一个跨了 多行的链接文字](https://example.com)大多数 Markdown 渲染器会拼接换行为空格,但你的正则默认只处理单行,[^]]*匹配不到换行符,所以这种链接会漏掉。解决方式是给 sed 加多行模式,或者在 Python 里给re.sub加re.DOTALL标志。但说实话,这种情况在真实文档里不算多,如果文档来源是你自己写的,通常不会有这种写法;如果是团队协作别人写的,那就只能靠多行模式兜底了。
自动链接<https://example.com>和邮箱<foo@bar.com>是另一类。它们在渲染后会直接变成<a>标签,但在源码里没有[文字]结构,所以行内链接的正则管不到。处理方式简单直接:<和>包裹的内容替换成内部文本。用 sed 可以这样:
sed -E 's/<([^>]+)>/\1/g' input.md但这里有一个风险:如果你在 Markdown 里嵌入了真正的 HTML 标签,比如<div>、<span>,这个正则会把它们也剥掉。所以这条命令最好只在确认文档里没有 HTML 标签的情况下使用,否则就把 HTML 标签的情况也纳入正则替换范围,比如先处理 HTML 标签、再处理自动链接,两件事分开做更安全。
5.5 参考式链接不能指望同一套正则
参考式链接的常见写法是[文字][参考id],有时候还能简写成[文字][],此时参考 id 和文字相同。用行内链接的正则去匹配后者,一个都匹配不到,因为括号里是[参考id]而不是 URL。
处理参考式链接必须单独写规则:
sed -E 's/\[([^]]*)\]\[[^]]*\]/\1/g' input.md[文字][参考id]会被替换成文字。但注意[文字][]这种简写形式,正则里的\[[^]]*\]能匹配空的[],所以也能正常工作。
这还没完,底部还有定义行:
[参考id]: https://example.com你需要把以[id]:开头的整行删掉。前面已经给过命令:
sed -E '/^\[[^]]*\]:[[:space:]]*http/d' input.md我在第 2.3 节强调过顺序问题:先替换正文引用,再删定义行。因为如果先删了定义行,正文里的[文字][id]还留在那,渲染出来会变成错误语法,难看。这两条命令配合使用,参考式链接才能清理干净。
6. 实操心得和扩展建议
6.1 修改前一定先备份
这是个老生常谈,但每次批量处理文档时我都要强调一遍。命令行工具跑起来很快,但误操作也很快。一个正则覆盖到半篇文档,几秒钟的事,回滚起来却可能搭上大半天。我的习惯是批量处理前先跑一句:
find . -name "*.md" -exec cp {} {}.bak \;或者更简单,直接用 git 管理文档目录,改完检查结果没问题再提交,有问题直接git checkout .一键还原。如果你不熟悉 git 也没关系,至少把原文件复制到一个 backup 目录,再对副本跑命令。用sed -i的时候,GNU 版本还支持直接生成备份文件:
sed -i.bak -E 's/\[([^]]*)\]\([^)]*\)/\1/g' input.md这样原文件会被保存为input.md.bak,处理完确认没问题再手动删备份。多花一两秒,换来的是心安。
6.2 什么时候别用命令,直接手改更快
虽然这篇文章一直在讲命令和脚本,但我还是要说句公道话:不是所有场景都适合用命令去批量处理。如果你的文档里只有三五个链接,而且分布在两三个文件里,打开编辑器用 Ctrl+H 手动替换,可能比开终端、敲命令还快。而且手动替换能让你在处理过程中顺便扫一眼上下文,避免把一些特殊写法误删。
还有一种情况是文档里有大量链接是你想保留一部分的,比如只想去掉失效的站外链接,保留站内锚点链接。这种复杂业务逻辑,单靠一条正则很难表达清楚,不如直接在编辑器里逐条过一遍,顺便清理无效链接。工具是为人服务的,没必要为了显得“专业”而硬套命令。
6.3 把去链接脚本固化成自己的常用工具
如果你和我一样,每隔一段时间就要处理一批 Markdown 文档的链接,那我强烈建议把整套逻辑写成一个独立脚本,存起来复用,而不是每次临时敲命令。
我现在电脑里就有个unlink-md.sh,功能包括:
- 处理行内链接
- 处理图片语法(可选保留 alt 文字)
- 处理参考式链接和定义行
- 处理自动链接和裸邮箱
- 保留 URL 文本的“去跳转化”模式
- 自动备份、自动统计处理了多少个链接
核心其实就是把前面讲的内容拼装起来,再用一个循环遍历文件夹。脚本维护好后,我通常在做完一次大改后跑一遍,对比前后文件差异,确认没有误伤,然后整个流程就结束了。你也可以根据自己的需求做成 Python 脚本、PowerShell 脚本,或者甚至是一个 VS Code 任务,怎么方便怎么来。
我个人这几年的体会是:这种“去掉 Markdown 链接”的处理,真正要注意的不是命令本身,而是你对文档里链接形态的判断是否完整。只要把行内式、参考式、自动链接、图片语法、HTML 链接这几种情况都梳理清楚,选好工具,再复杂的文档也能一次处理干净。