乐于分享
好东西不私藏

Excel 转 Markdown:一个 400 行 Python 脚本的设计思路

Excel 转 Markdown:一个 400 行 Python 脚本的设计思路

日常工作中有一个常见痛点:产品文档、需求规格、接口说明都放在 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 会用 0000000000FFFFFF 等值表示无填充。

文件安全

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 轻得多。