
最近帮朋友整理一批博客源文件全是 Markdown 格式大概两百多篇涉及旧的图片路径、残缺的标签字段、还有不少链接格式不统一。手动改的话一个人干一整天都未必能改完而且最容易出现改漏、改错的情况。我最后用 Python 脚本加正则表达式花了一个多小时把替换、提取、校验三件事全部跑完顺带还生成了完整的目录文件。这篇文章就把这套实战过程完整拆开讲清楚思路、脚本和踩过的坑适合正在维护 Markdown 文档库、博客源文件、或者经常和批量文本处理打交道的人。1. 为什么我选择 Python 加正则表达式处理 Markdown1.1 三个真实的工作场景先说替换。最典型的是批量修改 Front Matter 里的字段比如给每篇文章统一加一个分类标签、修改 tags 列表、或者是把图片路径从blog/src/img改成images/blog。这种替换往往是重复的、结构化的但又不至于简单到按固定字符串替换就能完成因为你不知道每篇文章里路径长什么样、标签有没有已经有重复值。然后是提取。假设你需要给网站在首页生成一个全部文章的目录或者需要检查所有文章里的外链有没有失效。这时候正则表达式的提取能力就比手工复制粘贴强太多了。只要制定了匹配规则就能把几百篇文档里的链接、标题、字数统计一次性抽出来再生成结构化结果。最后是校验。校验没那么起眼但最值钱。比如检查文章里有没有不成对的中英文括号、全角空格混用、代码块有没有闭合、Front Matter 的字段格式是否合法。这些问题靠人眼看很难不漏但用正则做规则校验几秒钟就能把问题清单全部列出来。这三个场景组合起来基本覆盖了日常 Markdown 文件批处理的大部分需求。1.2 工具选型为什么不用 sed、编辑器宏或现成工具很多人第一反应是用编辑器自带的“批量查找替换”这个对单文件、少量文件确实没问题但一旦文件数量到几十上百个编辑器方案就很吃力。你需要逐个文件打开、确认替换结果、保存中间还容易因为某些无意的替换把内容改坏。Linux 体系下有人会想到sed它能做流式替换执行效率确实高。但sed的正则语法和 PCRE、Python 的re模块有差异一些比较复杂的分组、断言写法在sed里要绕好几个弯跨平台到 Windows 上还得用 Git Bash 或者 WSL不省心。PowerShell 的正则能力也不错但脚本整体写起来繁琐可读性不如 Python。Python 的re模块是标准库跨平台都能跑语法遵循 PCRE 风格生态里还有pathlib、os.walk这些文件遍历利器。最关键的一点是用 Python 写批量处理脚本时你可以很方便地把匹配结果、替换次数、异常文件全部记录到日志里方便事后核对。这是命令行工具难以做到的因为命令行工具默认不会为“多文件处理任务”保留这么细的执行上下文。1.3 正则表达式基础速览够用就行的核心语法如果你之前没系统学过正则别急着背语法表。针对 Markdown 批处理你只需要掌握几类核心结构。字符类用方括号表示比如[a-z]匹配任意小写字母[0-9]匹配数字[^\]表示不匹配某个字符。量词控制重复次数*表示零次或多次表示一次或多次?表示零次或一次{n,m}表示 n 到 m 次。分组用圆括号(...)捕获内容(?:...)只分组不捕获。断言在匹配时很有用(?...)是正向先行断言(?!...)是负向先行断言。还有一个很重要但是新手最容易忽略的概念贪婪匹配与懒惰匹配。默认情况下*和都是贪婪的会尽可能多地匹配内容。比如用code(.*)/code匹配codea/codecodeb/code你会拿到一整段而不是第一个code到第一个/code。这时候需要在量词后面加问号写成.*?或.?表示最小匹配。2. 理论与实践Markdown 结构如何对应正则规则2.1 匹配标题、列表、链接与图片Markdown 虽然语法简单但正因为简单它的结构特征非常适合用正则来描述。拿标题来说标题行永远是行首的#开始后面跟空格再跟标题文字。写成正则就是^#{1,6}\s(.)$。注意^用不用re.MULTILINE标志效果完全不同。默认情况下^只匹配字符串开头如果你把整个文件读成一个字符串那就只能匹配到文章的第一个标题。所以处理多行文本时必须给re.sub或re.findall传入re.M参数让^和$匹配每一行的行首行尾。列表的匹配稍微复杂一点因为无序列表和有序列表的标记不一样。无序列表是行首的-、*、之一后面跟空格有序列表是数字加点或加括号。为了不误伤粗体和斜体的星号我通常会在行首约束下^(?:[-*]\s|\d[.)]\s)。这样匹配到的才是真正的列表项。链接的匹配是重点。标准 Markdown 链接的格式是[显示文字](目标地址)有时候后面还会带一个可选标题标题。我常用的链接正则如下import re link_pattern re.compile( r\[([^\]])\]\(([^)\s])(?:\s[^]*)?\), re.MULTILINE )这个模式的第一组捕获链接文字第二组捕获 URL。为什么要用[^\]]而不是.因为.贪婪且范围过大如果一行里有多个链接它会一直吃到最后破坏分组结构。[^\]]明确表示“不包含 ] 的所有字符”这样遇到第一个]就停下来分组结果才准确。图片的匹配和链接很类似只是最前面多了一个感叹号格式是。正则写成!\[([^\]]*)\]\(([^)])\)。注意替代文字有时候是空的所以我使用*而是不是表示零个或多个字符。2.2 用正则处理代码块的技巧代码块在 Markdown 里是一个特殊区域如果处理不好会被误判成标题、列表或者链接。常规 Markdown 代码块有两种缩进式代码块和围栏式代码块。现在更常用的是围栏式也就是用三个反引号或三个波浪号包裹例如def hello(): print(world)如果你想在正则中区分“代码块内部”和“正文区域”最简单的思路是先把代码块整体摘出去再对剩余部分做其他匹配。具体做法用re.finditer(r.*?, text, re.DOTALL)找到所有代码块记录它们的位置然后把正文里这些片段先替换成占位符处理完其他规则后再把占位符还原。这样做的好处非常明显你不会在统计标题、提取链接时把代码块里的示例代码当正文。还有内联代码用单个或两个反引号包裹。它的处理和围栏代码块类似在需要严格统计时最好用r[^] 这类模式匹配出来并暂时排除。不过如果你只是做替换这一类简单操作内联代码误判的影响通常较小可以视情况不处理。2.3 特殊场景处理Front Matter、表格与内联代码很多静态博客平台会在 Markdown 文件开头用 Front Matter 记录元数据它通常是以三个短横线---开头、以---结尾的 YAML 块。处理和前端相关的内容时正则要匹配tags:、categories:这类键值对我习惯用^([a-zA-Z0-9_-]):\s*(.*)$加re.MULTILINE逐行匹配。如果只需要处理 Front Matter而不想动正文可以先按^---\n.*?^---\n把这块区域切出来结合re.DOTALL处理再把处理结果拼回去。表格的处理反而是最麻烦的。Markdown 表格用竖线分隔第二行还有一组对齐用的短横线。如果你要批量修改表格内容正则通常只用于“找表格”这个层面比如匹配整个表格块^\|.*\|$。真到修改单元格的时候我会按行分拆后使用str.split(|)这比用复杂正则更可靠。毕竟表格竖线本身在内容里可能被转义单纯靠正则逐列解析很容易出错。3. 实战一批量替换 Markdown 内容3.1 需求场景与脚本设计思路我这次的实际需求是两件事第一把旧图片路径中所有的assets/imgs/批量替换成images/blog/第二给每篇文章的 Front Matter 的 tags 字段补上一个统一的分类标签如果文章里已经存在该标签就不重复添加。批量替换类的脚本最忌讳跑完就直接改文件。我的习惯是脚本必须支持两种模式--dry-run预览模式和--apply写文件模式。预览模式打印出将要被替换的原始行与替换后的结果确认无误后再真正写回文件。这个习惯帮我避免过好几次因为正则没写对导致的全仓替换惨案。脚本设计的另一个要点是文件遍历。我用os.walk遍历指定目录匹配后缀为.md或.markdown的文件逐个处理。为了保险起见脚本会统计每个文件的替换次数如果一个文件里完全匹配不到目标不会做任何修改更不会因为写着空内容就覆盖原文件。3.2 完整脚本批量替换路径与补全标签下面这段代码就是我实际使用过的骨架我把敏感信息脱敏了逻辑保持一致import os import re import sys import shutil FRONT_MATTER_RE re.compile(r^---\n.*?\n---\n, re.DOTALL) TAGS_LINE_RE re.compile(r^tags:\s*(.*)$, re.MULTILINE) def process_file(filepath, dry_runTrue): with open(filepath, r, encodingutf-8) as f: content f.read() original content changes [] # 替换图片路径 new_content, count1 re.subn( rassets/imgs/, images/blog/, content ) if count1: changes.append(f图片路径替换 {count1} 处) # 定位 Front Matter 区域 fm FRONT_MATTER_RE.search(new_content) if fm: fm_text fm.group(0) def add_tag(match): tags match.group(1) if tech in tags: return match.group(0) return ftags: {tags.strip()}, tech updated_fm, count2 TAGS_LINE_RE.subn(add_tag, fm_text) if count2: new_content new_content.replace(fm_text, updated_fm) changes.append(ftags 字段补充 {count2} 处) if new_content original: return 0, [] if dry_run: print(f[预览] {filepath}) for c in changes: print(f - {c}) else: # 写回前先备份一份 .bak 文件 backup filepath .bak if not os.path.exists(backup): shutil.copy2(filepath, backup) with open(filepath, w, encodingutf-8) as f: f.write(new_content) print(f[已修改] {filepath}) for c in changes: print(f - {c}) return 1, changes def main(): root sys.argv[1] if len(sys.argv) 1 else . dry_run --apply not in sys.argv total_modified 0 for dirpath, _, filenames in os.walk(root): for fn in filenames: if fn.endswith((.md, .markdown)): fullpath os.path.join(dirpath, fn) modified, _ process_file(fullpath, dry_run) total_modified modified print(f共处理 {total_modified} 个文件) if dry_run: print(当前是预览模式如需真正写入请加 --apply 参数) if __name__ __main__: main()这个脚本里我用了re.subn而不是re.sub因为subn会返回替换次数方便统计。add_tag内部先检查 tags 里是否已经包含tech如果已有就不重复添加。当然这里只处理了“单行 tags 列表”的情况如果你的 Front Matter 里 tags 是多行列表格式需要额外写逻辑判断后面我会说到。3.3 替换时的关键细节与备份策略第一点必须在写文件前做备份。虽然我有版本控制但生产环境里不是所有文件都进 Git所以脚本里保留自动备份.bak的逻辑更稳妥。如果你确定不会写坏也可以把备份开关做成可选参数。第二点小心 Windows 的换行符。Markdown 文件在 Windows 里可能用\r\nPython 以文本模式打开后通常能自动处理但如果你用open(filepath, w)写回在 Linux 下会默认写成\n。如果团队里有人用 Windows 打开会看到整个文件“变形”成一种换行风格。我的做法是读文件时用二进制模式打开或者写回时显式指定newline尽可能保留原始换行风格。第三点正则里的路径分隔符。Windows 路径用反斜杠例如images\blog\demo.png。但在正则表达式里反斜杠是转义字符处理起来非常蛋疼。我建议在脚本里统一把路径分隔符转成正斜杠images/blog/demo.png既符合 Markdown 渲染规则又避免转义地狱。4. 实战二批量提取 Markdown 信息4.1 自动提取所有外链并检查是否失效批量检查外链这个需求来自一个博客迁移项目。旧文里有不少第三方外链迁移后想确认哪些已经打不开。用浏览器逐个打开显然不现实正则提取配合 HTTP 请求就能把完整清单整理出来。我先用findall提取全部 Markdown 链接里的 URL然后过滤出http://或https://开头的地址。提取到的链接可能存在重复就用set去重。接下来对每个链接发起一次轻量请求把状态码记录到结果表里。import re import os import requests LINK_RE re.compile(r\[([^\]])\]\((https?://[^)\s])\)) def extract_links_from_md(filepath): with open(filepath, r, encodingutf-8) as f: content f.read() return LINK_RE.findall(content) def check_url(url, timeout10): try: resp requests.head(url, timeouttimeout, allow_redirectsTrue) return resp.status_code except requests.RequestException: return -1 def main(): root . results [] for dirpath, _, filenames in os.walk(root): for fn in filenames: if fn.endswith(.md): filepath os.path.join(dirpath, fn) links extract_links_from_md(filepath) for text, url in links: if url not in [r[1] for r in results]: status check_url(url) results.append((filepath, text, url, status)) print(f{文件:40} {链接文字:20} {URL:40} {状态}) for r in results: print(f{r[0]:40} {r[1][:20]:20} {r[2][:40]:40} {r[3]}) if __name__ __main__: main()这里有个很关键的细节是requests.head只发 HEAD 请求速度快但有些网站不支持 HEAD 方法会返回 405 或 403。遇到这种情况时我会在脚本里加一个回退逻辑当状态码是 405 时改用 GET 请求只获取 headers不下载 body。如果你不想引入requests库也可以用标准库urllib.request但要处理异常会麻烦一些。4.2 自动生成文章目录 TOC博客系统如果要展示文章的大纲结构通常需要生成 TOC。手工写目录既慢又容易漏用正则提取标题是最合适的方式。提取标题时要注意两点第一标题是行首的#但匹配时不能把代码块里的#注释或者引用块里的标题都算进来。所以我会先做一个步骤把代码块内容排除掉再对剩下的正文做标题匹配。第二标题文本里可能包含行内代码或加粗在生成锚点链接时要去掉这些 Markdown 标记。import re CODE_BLOCK_RE re.compile(r.*?, re.DOTALL) HEADING_RE re.compile(r^(#{1,6})\s(.*)$, re.MULTILINE) def generate_toc(md_content): clean_content CODE_BLOCK_RE.sub(, md_content) toc_lines [] for match in HEADING_RE.finditer(clean_content): level len(match.group(1)) title match.group(2).strip() anchor re.sub(r[^\w\u4e00-\u9fff], -, title).strip(-) toc_lines.append(f{ * (level - 1)}- [{title}](#{anchor})) return \n.join(toc_lines)生成 TOC 的脚本可以单独执行也可以集成到发布流程里在构建前自动把 TOC 插入到文章头部。我在实际应用时会把 TOC 单独保存成一个.md文件方便人工检查调整而不是直接改原稿避免因为锚点不一致影响自动发布。4.3 批量统计字数与提取关键信息做内容运营的时候经常需要给一批文章统计字数按字数评估内容价值。正则提取在这里可以负责“分类统计”中文汉字数、英文字符数、代码块行数、图片数量、链接数量。先排除代码块再对剩余正文做统计。中文汉字的匹配用[\u4e00-\u9fff]英文字符用[A-Za-z]。数字的统计可以单独列。这类统计脚本实现的匹配逻辑很简单关键是统计口径要统一。比如“字数”包不包括标题、Front Matter、链接文字、代码块你要在一个项目里先定下来否则不同脚本统计出来的结果无法横向比较后面做报表就很头疼。我的做法是把统计结果输出成 CSV 表格import csv import re STATS_OUTPUT stats.csv def analyze_md(filepath): with open(filepath, r, encodingutf-8) as f: content f.read() without_code CODE_BLOCK_RE.sub(, content) chinese_chars re.findall(r[\u4e00-\u9fff], without_code) english_words re.findall(r[A-Za-z], without_code) img_count len(re.findall(r!\[[^\]]*\]\([^)]\), content)) link_count len(re.findall(r\[[^\]]\]\(https?://[^)\s]\), content)) return { file: filepath, chinese_chars: len(chinese_chars), english_words: len(english_words), images: img_count, links: link_count, code_blocks: len(CODE_BLOCK_RE.findall(content)) } def main(): rows [] for dirpath, _, filenames in os.walk(.): for fn in filenames: if fn.endswith(.md): rows.append(analyze_md(os.path.join(dirpath, fn))) with open(STATS_OUTPUT, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(rows[0].keys())) writer.writeheader() writer.writerows(rows) print(f统计完成结果写入 {STATS_OUTPUT})注意写 CSV 时我用的是utf-8-sig这样 Excel 打开中文不会乱码。这个细节看起来小但实际使用中很多同事在 Windows 下打开 CSV 就遇到乱码改成带 BOM 的 UTF-8 编码后问题迎刃而解。5. 实战三批量校验 Markdown 规范5.1 校验需求从源头减少排版问题写博客的人都知道Markdown 文件一旦源码不规范发布出去之后样式就会乱。最常见的几类问题包括标题层级跳跃从 H2 直接跳到 H4、代码块没有闭合、中英文括号混用、全角空格混用、图片引用路径为空、链接文字为空。这些错误在渲染时不一定报错但会影响阅读体验也让后期的批量处理变得很脆弱。校验脚本的价值就在这里它不是帮你改内容而是在发布之前把问题清单列出来让作者知道应该修哪里。这就在团队协作场景里特别有用我接触过的内容团队里常常是不同的人用不同的编辑器写稿排版风格五花八门。有了统一的校验规则至少能在提交前拦截一批低级错误。5.2 校验脚本示例与运行逻辑我写的校验脚本会返回一个问题列表每个问题带文件名、行号和问题描述。要实现行号定位最简单的方法不是在整个文件字符串上匹配而是用readlines()按行读取逐行用正则检查。逐行匹配在大文件上效率稍低但对于 Markdown 这种体量完全够用而且行号定位准确。import re import os import sys CODE_FENCE_RE re.compile(r^\s*) FULL_WIDTH_SPACE_RE re.compile(r\u3000) MIXED_BRACKETS_RE re.compile(r[(][^)]*[)]) def validate_file(filepath): issues [] with open(filepath, r, encodingutf-8) as f: lines f.readlines() code_fence_open False for line_no, line in enumerate(lines, 1): if CODE_FENCE_RE.match(line): code_fence_open not code_fence_open continue if code_fence_open: continue if FULL_WIDTH_SPACE_RE.search(line): issues.append((line_no, 包含全角空格)) # 检查成对括号中文括号中混入英文右括号或反之 if MIXED_BRACKETS_RE.search(line): for m in MIXED_BRACKETS_RE.finditer(line): segment m.group(0) if ( in segment and ) in segment) or (( in segment and in segment): issues.append((line_no, f括号混用{segment})) break # 检查图片地址是否为空 for m in re.finditer(r!\[[^\]]*\]\(([^)]*)\), line): if not m.group(1).strip(): issues.append((line_no, 图片地址为空)) # 检查代码块是否闭合 if code_fence_open: issues.append((len(lines), 代码块未闭合)) return issues def main(): root sys.argv[1] if len(sys.argv) 1 else . total_issues 0 for dirpath, _, filenames in os.walk(root): for fn in filenames: if fn.endswith(.md): filepath os.path.join(dirpath, fn) issues validate_file(filepath) if issues: total_issues len(issues) print(f{filepath}:) for line_no, desc in issues: print(f {line_no}: {desc}) print(f共发现 {total_issues} 个问题) if __name__ __main__: main()这段脚本里我用code_fence_open标记当前是否处于代码块内部。一旦遇到行首的三个反引号就切换状态在代码块内部跳过所有其他检查。括号混用检测的规则比较朴素同一段文字里如果同时出现中文左括号和英文右括号就认为是混用。它不能处理所有嵌套场景但已经能抓住绝大多数实际问题。5.3 校验结果输出与人工复核机制校验结果直接打印在终端里适合个人使用。如果要把校验接入到 CI 或者 Git 的 pre-commit 钩子里就需要输出成机器可读的格式比如 JSON然后由 CI 判断是否阻断提交。这里分享一个重要经验校验脚本的规则要有“可配置”的意识。不同团队的规范不一样比如有的团队允许正文里出现英文标点有的不允许。我一般把规则做成字典或者配置文件字段名对应检查项值设置为开启或关闭。这样其他人接手时不需要改代码改配置就行。否则你写死了规则换个项目又要改代码维护成本很高。另外发现问题后不要直接自动修复所有问题。部分规范化操作可以自动做比如全角空格替换成半角空格但有些问题必须人工判断比如一个链接文字是否合理、一个标题是否应该降级。我个人的原则是能自动修复的用脚本修复不能自动修复的输出问题清单分发给对应作者人工处理。6. 正则表达式踩坑实录我踩过的 5 个坑6.1 贪婪匹配吞掉了不该吞的内容这是新手最容易踩的坑我到现在偶尔还会犯。比如说你想提取一个 Markdown 链接的 URL如果正则写成\[.*\]\(.*\)当一行里有两个链接时正则引擎会贪婪地把第一个[到最后一个)全部匹配进来结果你拿到的不是单个链接而是两个链接加中间所有文字。解决办法就是尽量用排除型字符组比如[^\]和[^)]明确告诉正则引擎“遇到这个字符就停”或者用非贪婪量词.*?。实战中最稳的方案是能用否定字符组的地方就尽量用否定字符组不要依赖非贪婪量词。排除字符组表达意图更加明确性能通常也更好。6.2 反斜杠转义与 Windows 路径的恩怨Windows 路径分隔符是\在正则里是转义符如果你要匹配图片路径里的盘符或者目录分隔需要写成\\\\读起来像天书。更麻烦的是 Markdown 图片路径本身可能包含空格、括号这些在正则中都有特殊含义。我的建议是双管齐下第一在脚本里统一把路径分隔符替换成正斜杠省去后续所有转义烦恼第二凡是遇到需要精确匹配的文本优先用re.escape()做转义。比如你要把字符串变量里的路径当纯文本匹配直接re.escape(path_string)就安全了不需要自己手工加反斜杠。6.3 中文与特殊标点符号的处理Python 的re模块默认支持 Unicode所以中文和全角符号可以直接写进字符组比如[\u4e00-\u9fff]匹配汉字\u3000匹配全角空格。但要注意 Unicode 和字节字符串的区别你如果用bytes类型处理正则里就没法直接用汉字必须先 encode 再匹配。另外\.匹配的是真正的句点如果我要匹配中文句号。直接写字符就行不需要绕弯。中英文标点混用是 Markdown 文章里很常见的问题但校验规则不能一刀切。比如代码块、URL、标签名里经常需要英文标点不能简单判断行里出现英文括号就报错。所以校验脚本里我总会先排除代码块和行内代码再分析正文。6.4 长文本上的灾难性回溯如果正则写得太复杂比如多个嵌套量词(a)这种模式在遇到一段匹配失败的长文本时可能触发灾难性回溯导致脚本卡死。这在单个 Markdown 文件里还不明显但一旦你遍历几百个文件某个文件的一个异常片段就足以让整个脚本假死。避免方法有三个尽量少用嵌套量词在不需要捕获组的时候用非捕获组(?:...)在模式复杂时用re.regex加上超时或者干脆分多个简单正则逐段处理。另一个经验是如果匹配目标可以用字符串方法实现就先用字符串方法。例如判断行首是不是##直接line.startswith(## )比正则更清爽执行效率还高。6.5 编码问题UTF-8 与 BOM 的坑这个坑和正则本身没关系但所有批量脚本最终都会碰到。有的编辑软件在保存 UTF-8 文件时会自动加上 BOM 头也就是文件开头多出三个字节\xef\xbb\xbf。用普通正则匹配^#时BOM 会黏在第一个标题前面导致匹配不到。解决方法是读取时统一用utf-8-sig编码它会自动忽略 BOM 头写回时根据团队标准选择一个固定的写入编码。如果团队内部的编辑器多种多样建议统一为不带 BOM 的 UTF-8并且在脚本尾部做一个简单检查确保没有写入 BOM。这个检查只有几行代码但能省掉后续大量莫名其妙的显示问题。7. 脚本复用与进一步扩展7.1 把脚本整理成可复用的命令行工具上面这些脚本拆开来用没问题但如果你经常需要对不同类型的 Markdown 项目做批处理我强烈建议把它们整合成一个简单的命令行工具。可以实现成多个子命令比如python md_tool.py replace --root ./blog --old assets/imgs/ --new images/blog/ python md_tool.py extract-links --root ./blog --output links.csv python md_tool.py validate --root ./blog --format json整合成了一个工具之后你只需要写一次文件遍历逻辑、一次命令行参数解析、一次日志输出规范后续所有操作都在统一入口上。开发这个工具本身不需要复杂的框架用 Python 标准库里的argparse就能完成。维护起来也不费劲毕竟每个子命令背后都是正则加文件处理。7.2 结合 Git Hook 与发布流程自动运行批量校验脚本最有价值的使用方式不是手动运行而是接入到 Git 的 pre-commit 钩子或者发布流程里。每次提交前自动执行校验发现问题就直接阻止提交从流程层面保证 Markdown 文件的质量底线。这样做的成本其实很低pre-commit 只需要一个几行的钩子脚本调用校验命令但收益却是长期的。接入时要注意执行时间如果仓库很大、文件很多每次提交都跑全量校验会让人不耐烦。折中方案是只校验本次提交里变更的那些文件通过git diff --name-only --cached拿到提交文件列表再针对列表执行校验速度能快一个数量级。7.3 从 Markdown 到其他格式的扩展思路正则表达式处理的边界不是 Markdown很多内容处理场景都能复用这套思路。比如把 Markdown 文件批量转成其他格式、从网页抓取的 HTML 中提取正文、批量清理日志文件里的字段。核心思想是一致的先观察结构找出稳定可依赖的规则再写正则去匹配、替换、提取、校验。网上也有一些成熟的“任意格式转换为 Markdown 的开源项目”如果只是做简单的内容搬运用那些工具确实省事但一旦你有定制化的清洗需求比如需要保留某些特殊格式、排除某些区域、整合多个来源的数据还是要回到正则加脚本这条路。工具只能解决标准化问题而内容清理永远是定制化的活。8. 一个让我少走弯路的小习惯最后分享一个我自己的习惯不管替换、提取还是校验写正则之前先把目标文档里所有可能出现的情况列一遍尤其是边界情况。比如提取链接时先看看文档里有没有不带协议的相对路径提取标题时看看有没有#出现在代码块里校验括号混用时看看有没有合法的中英混排场景。列完之后我会写 5 到 10 个测试用例覆盖正常情况和边界情况跑一遍再上真实数据。这看起来多花了几分钟实际上省下来的时间远不止这几分钟。没有经过样本测试的正则直接扔到几百个文件里跑一旦出了岔子排错成本会非常高。正则表达式的核心不是“写得出来”而是“写得对”而“写得对”的唯一标准是样本数据全部通过外加让你对下一个处理批次有信心。