日常工作中有一个常见痛点:产品文档、需求规格、接口说明都放在 Excel 里。Git 无法有效 diff,Code Review 看不清变更,搜索还要依赖全局 grep,效率很低。
这个脚本解决一个问题:把目录下所有 Excel 的每个 sheet 转成独立 Markdown 文件,并自动生成索引。转换结果可以直接放进任何文档系统,比如 Obsidian、Notion、GitHub Wiki。
为什么不做成 Web 应用
先回答最常见的质疑。
Excel → Markdown 是离线批处理场景,不是交互场景。用户面对的是几十上百个文件,而且分布在多级目录下。Web 应用需要上传、等待、下载;本地脚本只要直接执行 python excel2md.py ./docs。
其次,依赖越少越好。脚本只依赖 openpyxl,一行 pip install 就能完成安装。不需要 Node.js,不需要数据库,也不需要浏览器。
核心设计
入口:一行命令
pythonexcel2md.py./requirements--output./output
脚本会遍历目录下所有 .xlsx 文件,为每个 sheet 产出独立 .md 文件,最后生成 INDEX.md。
output/
├── 需求文档__接口列表.md
├── 需求文档__数据字典.md
├── 设计稿__组件规范.md
└── INDEX.md
三种渲染模式
脚本不是用一套模板处理所有内容。不同 sheet 的用途不同,渲染策略也不同。
模式一:保留表格
多语言 sheet 这类内容本身就是对照表,转成列表反而难读。脚本会直接输出标准 Markdown 表格:
## 多语言
| 中文 | English | 日本語 |
|------|---------|--------|
| 登录 | Login | ログイン |
模式二:大纲文档
这是最常用、也最复杂的模式。很多需求文档长这样:第一列是层级标题,用缩进和 · 符号表达大纲结构,某些行则用加粗和大字号标记章节标题。
脚本的处理逻辑:
·前缀 + 缩进 → Markdown 列表项,缩进层级由前导空格数决定- 加粗 + 字号 ≥ 16 →
#一级标题 - 加粗 + 字号 ≥ 14 →
##二级标题 - 加粗 + 字号 ≥ 12 →
###三级标题 - 无
·前缀且不加粗 → 默认###标题 - 高亮单元格 → 反引号包裹的
`inline code`
示例:
· 用户管理 → - 用户管理
· 新增用户 → - 新增用户
· 权限校验 → - 权限校验
加粗 18pt 标题 → # 加粗 18pt 标题
模式三:表格转条目
多列表格(≥2 列有表头)会转成结构化条目。第一列作为条目标题,其余列作为属性列表:
### 字段名
-**类型**: string
-**必填**: 是
-**说明**: 用户昵称
格式保留
Excel 里用户设置的格式标记,脚本会翻译成 Markdown 等价表达:
| Excel 格式 | Markdown 输出 |
|---|---|
| 背景高亮 | `反引号` |
| 加粗 | **加粗** |
| 加粗 + 大字 | # 标题 |
(加粗样式) 后缀注释 |
自动移除 |
高亮判断逻辑并不复杂:检查 cell 的 fill.patternType 是否为 solid,以及颜色是否在“无色”集合里。openpyxl 会用 00000000、00FFFFFF 等值表示无填充。
文件安全
Excel 的 sheet 名称里可能包含 /、:、* 这些文件名非法字符。脚本没有用正则替换,而是用 str.maketrans 映射到全角字符:
ESCAPE_MAP = str.maketrans({
"/": "/", "\\": "\", ":": ":",
"*": "*", "?": "?", '"': """,
"<": "<", ">": ">", "|": "|"
})
全角字符的好处是:视觉上几乎一致,但操作系统会把它们当作普通字符处理,不需要额外做编码转义。
空格会被替换为下划线,避免某些工具链在解析路径时出错。
脏数据防御
实际 Excel 文件远比想象中更脏:
- 全空行:表格底部几十行空行,脚本会跳过
- 全空列:用户复制粘贴留下的空列,脚本会自动剔除
- 页头 sheet:名为“页头”“封面”的 sheet 通常只存元数据,直接跳过
- 损坏文件:用 try/except 包住单个文件处理,一个文件损坏不会影响其他文件
- .xls 旧格式:
openpyxl不支持,单独列出警告后跳过
版本号提取
脚本会从目录路径中自动提取 semver 版本号,用于索引文件头:
# feat-1.4.5-main → 1.4.5
re.search(r"(\d+\.\d+\.\d+)", path)
不强制用户手动输入,约定优于配置。
架构拆解
整个脚本 413 行,函数职责清晰:
| 函数 | 职责 | 行数 |
|---|---|---|
find_excel_files |
递归查找 .xlsx/.xls |
~15 |
process_excel |
单文件处理:加载 workbook,遍历 sheet | ~35 |
table_to_md |
核心路由:判断渲染模式 | ~100 |
_render_table |
表格模式渲染 | ~40 |
_cell_text |
单元格文本样式翻译 | ~15 |
_outline_level |
大纲层级解析 | ~15 |
_heading_level |
字号 → 标题级别映射 | ~15 |
_is_highlighted |
高亮检测 | ~12 |
write_index |
生成 INDEX.md |
~15 |
每个函数不超过 40 行(除核心路由 table_to_md 外),无全局状态,无类继承。_ 前缀标记内部函数,外部无依赖。
为什么不写成类
有同学会问:这不该做成一个 ExcelToMarkdown 类吗?
不需要。这个脚本没有需要封装的状态。输入目录、输出目录、文件列表,都是通过函数参数传递。加一个类只会多一层 self. 前缀,没有实际收益。
函数式不是银弹,但在“数据输入 → 数据输出”这种纯转换场景里,它就是最简单的方案。
局限与下一步
当前限制:
- 不支持
.xls(旧版 Excel 97-2003 格式),需要额外用xlrd处理 - 合并单元格未特殊处理,
openpyxl只返回左上角单元格的值 - 公式结果用
data_only=True读取缓存值,未打开过的文件可能为空 - 图片、图表不提取
可以继续扩展的方向:
- 合并单元格自动填充到所有覆盖区域
- 通过
xlrd回退支持.xls - 图片提取为独立文件,并在 Markdown 中引用
- 输出格式可选:Markdown / HTML / Confluence
使用
# 安装依赖
pipinstallopenpyxl
# 转换整个目录
pythonexcel2md.py./your-excel-dir
# 指定输出目录
pythonexcel2md.py./your-excel-dir--output./markdown-output
脚本不大,但解决的是真实痛点:400 行 Python,零配置,批量处理,比引入一个文档转换 SaaS 轻得多。
夜雨聆风