md2Hexo 脚本解析与重构:把外部 Markdown 规范化导入 Hexo
背景:为什么需要 md2Hexo
Hexo 的文章通常要求在文件头部包含 YAML front matter,例如:
|
当我们从外部仓库/笔记/电子书/速查表批量迁移 Markdown 到 source/_posts/ 时,经常会遇到:
- 没有 front matter(Hexo 无法识别为文章)
- 文件数量巨大,需要批量处理
- 希望 tags/categories 能根据目录名自动补全
- 迁移过程中要尽量“安全”:已处理过的文件不要反复改写
这就是 tools/md2Hexo.py 的定位:批量为 Markdown 文件补齐 Hexo front matter。
本文基于仓库中的 tools/md2Hexo.py(当前版本 2.1.0)做一次“代码走读 + 质量测评 + 重构点说明”,让它看起来更像一个标准、可靠、可维护的 Python CLI 工具。
总体结构:从配置到批量转换
脚本主要由 4 个层次组成:
- 数据模型(dataclass):
ConversionConfig、ConversionResult - Front Matter 相关能力:
HexoFrontMatter - 转换核心:
Md2HexoConverter - CLI / 交互入口:
parse_args()、interactive_mode()、main()
数据流大致如下:
parse_args()解析命令行- 选择配置来源:配置文件 / CLI 参数 / 交互输入
- 合并“覆盖项”(dry-run/force/exclude 等)
Md2HexoConverter.convert_directory()遍历文件并调用convert_file()- 输出统计结果与错误摘要
模块 1:ConversionConfig(配置模型)
ConversionConfig 是工具的“行为说明书”,重要字段:
tags: List[str]:固定附加的标签categories: List[str]:固定附加的分类include_folder_as_tag: bool:是否把“最后一级目录名”加入 tagsinclude_folder_as_category: bool:是否把“最后一级目录名”加入 categoriesuse_file_mtime: bool:date 是否用文件修改时间(mtime)skip_existing_frontmatter: bool:若文件已有 front matter 是否跳过(默认跳过,安全)exclude_patterns: List[str]:排除列表(支持精确文件名与通配符)dry_run: bool:预览模式(不写回文件)
兼容性
仓库内已有 JSON 配置样例(例如 tools/configs/awesome-cheatsheets.json),字段与 dataclass 一一对应;这使得批量导入不同知识库时,只需切换配置文件即可。
模块 2:ConversionResult(结果统计)
ConversionResult 负责统计与可观测性:
total:扫描到的.md文件数converted:实际写入/转换数量skipped:跳过数量(排除文件、已有 front matter 等)errors:错误列表(路径 + 错误信息)
工具最终会打印这些数字,便于判断一次批量导入是否“收敛”。
模块 3:HexoFrontMatter(Front Matter 生成与检测)
这个模块做两件事:
- 检测文件开头是否已经存在 YAML front matter
- 生成新的 YAML front matter 并插入到正文前面
3.1 为什么要改进检测逻辑
早期实现常见的写法是:
content.strip().startswith('---')
这会带来两个问题:
- 容易误判:Markdown 里的水平分割线也可能是
--- - 性能不佳:为了判断“是否有 front matter”,不得不读取整个文件
在当前版本中,检测改成了:
- 使用正则
\A--- ... ---只在文件开头匹配 - 检测时只读取文件前 8KB(足够覆盖绝大多数 front matter)
3.2 YAML 输出(去依赖版)
为了让脚本在“纯 Python3 环境”里就能直接跑(避免额外安装 PyYAML),当前版本内置了一个极简 YAML 序列化器来输出 front matter:
- 字符串统一用 JSON 风格双引号包裹(等价于 YAML 的双引号字符串),对中文/空格/冒号等特殊字符更安全
tags/categories输出为 YAML 列表;为空时输出[]extra_fields仅支持“标量或字符串列表”,避免复杂类型带来的歧义
模块 4:Md2HexoConverter(核心转换引擎)
4.1 目录遍历
convert_directory() 使用 Path.rglob('*.md') 递归查找 Markdown 文件。
- 若输入路径是“文件”,也会作为单文件处理(更灵活)
- 若是目录,则递归遍历
4.2 排除规则 should_skip_file
排除规则做了两层兼容:
- 精确匹配:
README.md、SUMMARY.md - 通配符匹配:
readme*、*.draft.md
这能覆盖大部分“目录说明文件不该被当文章导入”的情况。
4.3 tags / categories 的构建策略
- 基础 tags/categories 来自配置文件或 CLI
- 若开启
include_folder_as_tag/category:把“相对目录的最后一级目录名”附加进去
这是一种折中:
- 不会把整条路径拆成多级分类(过于复杂)
- 但能在大多数情况下把内容按集合归类
4.4 I/O 与健壮性
当前版本做了几项“标准脚本该有的稳健处理”:
- 编码读取:优先
utf-8,失败再尝试utf-8-sig - 跳过判断只读前缀:减少大文件的无谓读取
- 写入采用“临时文件 + replace”原子替换:降低写入中断导致文件损坏的概率
模块 5:CLI 与交互体验
5.1 parse_args(命令行接口)
CLI 的基本参数:
-d/--directory:要处理的目录或文件--config:JSON 配置文件-t/--tags、-c/--categories:逗号分隔输入--dry-run:预览不写入-f/--force:强制覆盖已有 front matter--exclude:追加排除规则(可多次指定)-v/-vv:日志等级(INFO/DEBUG)
特别要提的是:
--skip-existing / --no-skip-existing被做成互斥选项,避免“默认 True + store_true”这种很容易写错的 argparse 陷阱。
5.2 interactive_mode(交互模式)
交互模式适用于少量文件快速处理:
- 提示是否继续
- 输入 tags / categories
- 是否 dry-run
按 Ctrl+C 或回答 n 会直接退出(返回码 0),不会继续执行转换。
使用方式(推荐)
方式 A:用配置文件批量导入(最推荐)
python3 tools/md2Hexo.py \ |
方式 B:临时指定 tags/categories
python3 tools/md2Hexo.py -d source/_posts \ |
方式 C:预览模式(不写入)
python3 tools/md2Hexo.py -d source/_posts --dry-run |
方式 D:强制覆盖已存在 front matter(慎用)
python3 tools/md2Hexo.py -d source/_posts -f |
完整代码与逐段解析(读完即可复制使用)
这一节的目标是:把 tools/md2Hexo.py 的完整源码按文件顺序拆段展示,并逐段解释“为什么这么写、做了什么、边界在哪里”。
如果你只想要代码:把本节所有 python 代码块拼起来(或直接复制最后的“完整源码”代码块)即可。
说明:本文展示的是仓库内当前版本(
2.1.0)的实现;以仓库文件为准。
1)文件头与导入:定位、依赖与版本
#!/usr/bin/env python3 |
解析要点:
#!/usr/bin/env python3:让脚本在类 Unix 环境可以直接执行(配合chmod +x)。__future__.annotations:让类型标注更现代(例如list[str]风格、前向引用)且不引入运行时负担。- 依赖全部为标准库(
argparse/logging/pathlib/json/...):避免“运行脚本前还要装包”的门槛。
2)配置与结果模型:用 dataclass 固定“输入/输出契约”
|
解析要点:
ConversionConfig统一承载配置文件/命令行/交互输入的最终结果,避免“参数满天飞”。exclude_patterns默认排除README.md等“目录说明文件”,这是迁移导入场景最常见的噪声。ConversionResult让批处理结果可观测:至少要知道“扫了多少、转了多少、跳了多少、哪里报错”。
3)HexoFrontMatter:检测与生成(无第三方依赖)
|
解析要点:
has_frontmatter()只匹配文件开头:避免把正文里的---误当成 front matter。- 生成 YAML 的实现刻意“极简且可控”:
- 只输出我们需要的字段:
title/date/tags/categories。 - 字符串统一用 JSON 双引号(对中文、空格、冒号等更稳)。
- 不支持复杂嵌套结构:避免 YAML 语义分歧。
- 只输出我们需要的字段:
4)Md2HexoConverter:转换引擎(I/O、跳过、写入策略)
|
解析要点:
_read_prefix():只读前缀用于“是否跳过”的判断,这是批量场景里最关键的优化之一。- 编码优先
utf-8,其次utf-8-sig:后者可兼容带 BOM 的文件。 _write_text_atomic():先写.tmp再替换,能降低写入中途中断导致文件被截断的风险。
接下来是“规则层”方法:日期、标题、排除、tag/category 构建。
|
解析要点:
get_file_date()默认用 mtime:迁移外部文档时,这通常比“现在时间”更接近真实创作时间。should_skip_file()同时支持:- 精确匹配(
README.md) - 通配符(
readme*、*.draft.md)
- 精确匹配(
build_tags/build_categories():- 先复制基础列表,避免原配置被污染
- 再按需追加最后一级目录名
最后是核心转换:单文件转换 + 目录遍历。
|
解析要点:
- 跳过策略:默认
skip_existing_frontmatter=True,因此对已处理文件是“幂等”的。 - dry-run:只统计,不写入。
- 错误处理:单文件失败不会中断整个批处理,错误会收集到
result.errors。
5)CLI/交互入口:参数解析、日志、主流程与退出码
|
解析要点:
- 交互模式用于“少量文件试运行”。
- 回答
n或 Ctrl+C 都会SystemExit(0):属于正常取消,不应算错误。
继续看配置载入与日志初始化:
|
解析要点:
load_config_from_file():JSON 配置字段若不匹配 dataclass,直接报出“字段不合法”,比默默忽略更安全。_setup_logger():- 默认 WARNING(输出更干净)
-v提升到 INFO,-vv提升到 DEBUG
接下来是参数解析:
|
解析要点:
--exclude允许重复传入:适合临时扩展排除列表。--skip-existing/--no-skip-existing做成互斥组,避免 argparse 的“默认 True + store_true”坑。--force保留为“一键覆盖”开关,语义直观。
最后是主流程与退出码:
|
解析要点:
- 退出码约定:
0:成功(或用户主动取消)1:路径不存在或转换中出现错误2:配置解析失败
- 统计输出固定且简洁:适合在终端里快速判断批处理结果。
完整源码(可直接复制粘贴成 tools/md2Hexo.py)
如果你不想分段复制,直接复制这一整段即可:
#!/usr/bin/env python3 |
测评:优点、问题与改进点
优点
- 配置模型清晰:适合“多集合、多来源”的批量迁移
- 默认安全:跳过已有 front matter,避免重复污染
- 扩展空间好:dataclass + 分层结构,继续加功能不容易失控
需要注意/可能的坑
- Front Matter 与 Markdown 分割线冲突:极少数以
---开头的纯 Markdown 可能被误判为 front matter(建议文件开头留空行或加标题) - 编码问题:如果源文件不是 UTF-8(例如 GBK),会报 decode 错误,需要先统一编码
- 目录名策略:目前只取“最后一级目录名”,如果你希望多级分类,需要在后续版本中扩展
性能与效率
- 主要耗时来自磁盘 I/O(读取 + 写入)
- 本次重构通过“前缀检测”减少了跳过场景的读取量
结语
md2Hexo 的本质是一个“迁移/清洗工具”。它的价值不在于复杂算法,而在于:
- 可预期的行为(默认安全)
- 清晰可控的配置
- 出错时能定位原因(日志 + 错误汇总)
后续如果要继续演进,优先考虑:
- 增加
--output(输出到新目录而不是原地改写) - 提供
--encoding或自动编码探测(避免遇到 GBK 文件就失败) - 输出 JSON 报告(便于 CI/统计)
(完)