Python 把散落的 Markdown 笔记拼成一篇博客草稿:我的土办法和踩过的坑
我在本地记技术笔记的习惯很烂:一个坑建一个 .md 文件,命名全靠随手,有的叫 fix-redis-timeout.md,有的叫 2026-10-15.md,还有的干脆是 新建文本文档 (3).md。写博客的时候打开这个目录,东翻西找,复制粘贴,格式还经常粘乱——代码块缩进飞了,标题层级对不上。后来我写了个几十行的 Python 脚本,专门干一件事:扫描一个笔记目录,按我定的规则排序,把里面的 Markdown 合并成一份带目录的草稿。不是什么优雅方案,但用了大半年,确实把“整理笔记”这一步从半小时压到了一分钟。
脚本干了什么
核心逻辑就三步:遍历目录收集 .md 文件,按文件里的某一行元信息排序,拼成一篇带目录和分隔线的文档。
笔记文件头部我会塞一行 <!-- order: 10 -->,数字越小排得越靠前,不写就默认按修改时间倒序。正文原样保留,文件之间用 --- 隔开,标题自动降一级(笔记里的一级标题变成草稿的二级),最后在开头生成一个锚点目录。
import os
import re
from pathlib import Path
from datetime import datetime
NOTE_DIR = Path("./notes")
OUTPUT = Path("./draft.md")
ORDER_RE = re.compile(r"<!--\s*order:\s*(\d+)\s*-->")
TITLE_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE)
def collect_notes(root: Path):
notes = []
for p in sorted(root.glob("*.md")):
text = p.read_text(encoding="utf-8", errors="replace")
m = ORDER_RE.search(text)
order = int(m.group(1)) if m else 10**9
mtime = datetime.fromtimestamp(p.stat().st_mtime)
title_m = TITLE_RE.search(text)
title = title_m.group(1).strip() if title_m else p.stem
notes.append({
"path": p,
"text": text,
"order": order,
"mtime": mtime,
"title": title,
})
# 先按显式 order,没写的按修改时间倒序兜底
notes.sort(key=lambda n: (n["order"], -n["mtime"].timestamp()))
return notes
合并和降级标题的部分稍微烦一点,因为代码块里的 # 不能被误伤。我一开始用正则全局替换,结果 Python 代码里的注释 # 这是注释 被改成了 ## 这是注释,代码直接跑不了。后来改成逐行判断是否在代码围栏内部:
def merge_notes(notes, output: Path):
parts = ["# 草稿\n", "> 由笔记自动合并生成,发布前记得手动润色。\n"]
# 目录
toc = []
for i, n in enumerate(notes, 1):
anchor = f"note-{i}"
toc.append(f"{i}. [{n['title']}](#{anchor})")
parts.append("\n".join(toc) + "\n")
for i, n in enumerate(notes, 1):
parts.append(f"<a id=\"note-{i}\"></a>\n")
parts.append(demote_headings(n["text"]).strip() + "\n\n---\n")
output.write_text("\n".join(parts), encoding="utf-8")
def demote_headings(text: str) -> str:
lines = text.splitlines()
in_fence = False
out = []
for line in lines:
stripped = line.lstrip()
if stripped.startswith("```") or stripped.startswith("~~~"):
in_fence = not in_fence
out.append(line)
continue
if not in_fence and stripped.startswith("#"):
# 只处理行首一级标题,避免动到 ## 以下
if re.match(r"^#\s", stripped):
line = "##" + stripped[1:]
out.append(line)
return "\n".join(out)
跑一次 python merge_notes.py,目录里所有 .md 就被拼成一份 draft.md。代码不长,但自己用和“能用”之间,隔着好几个坑。
踩过的几个坑
编码问题比想象中普遍。 有些旧笔记是在 Windows 上拿记事本写的,存成了 GBK 或带 BOM 的 UTF-8,直接用 read_text(encoding="utf-8") 会抛 UnicodeDecodeError。我加了 errors="replace" 兜底,至少让脚本别崩,但代价是偶尔出现乱码字符,得回头手动改原文件。后来干脆在脚本里先探测编码,探测失败的跳到下一份并在终端打印文件名,比静默替换稍微好一点。
排序不能只看文件名。 我最初按 glob 返回的顺序直接拼,结果 sort 在 Linux 和 macOS 上表现还不一样——macOS 默认大小写不敏感,Z.md 可能排在 a.md 前面。改成显式 sorted() 加自定义 key 之后才稳定。显式 order 标记也是后来加的,因为有些笔记跨了几个月,单靠修改时间排出来的顺序和我想要的叙事顺序对不上。
标题锚点在博客园不一定认。 我在草稿里生成的 <a id="note-1"></a> 锚点在本地 Markdown 预览里跳转正常,但博客园发布时会对 HTML 做处理,有时候锚点会被剥掉或重写。我的应对很粗暴:草稿里同时保留一份纯文本目录(不带链接),发布时手动把目录改成博客园支持的 #标题 形式,多花十秒,但省心。
代码围栏的结束标记必须单独成行。 笔记里有人(主要是我自己)会把 ```python 写在某行代码后面当注释,严格的 Markdown 解析器会认为围栏没闭合,后面的标题降级全部失效。这个没找到完美的自动方案,我的做法是在合并时检测“围栏数量为奇数”的文件,打印警告,人工看一眼。
比起自己拼,现成工具也不是不能用
如果不想维护脚本,proj2md 这个工具的思路和我上面做的几乎一样,只不过它的目标是把整个项目(代码、配置、文档)拼成一份给 AI 读的 Markdown,支持文件索引、智能排序和编码自动识别。它的 --include-pattern 和忽略规则比我手写的靠谱得多,拼大目录的时候我偶尔会直接拿它代替自己的脚本。
另一个方向是走博客园的 API 直接发,跳过“本地拼草稿”这一步。博客园支持 MetaWeblog API,可以用 newPost 直接提交 Markdown 内容,但要注意它会把 Markdown 转成 HTML 存储,所以本地最好留一份源文件做索引,修改时从源文件重新读,而不是去拉已经转过的 HTML。有人把“笔记 → 草稿 → 审查 → 发布”串成了完整的 CLI 流水线,审查规则里还加了敏感信息拦截和 AI 套话检测,思路挺值得抄。不过对我来说,自动发布暂时没必要——我更需要的是“把碎片拼起来”,而不是“点一下发出去”。

浙公网安备 33010602011771号