干标书/方案这行最磨人的不是写内容,是排版。
方案写完了,往Word里一贴,字体乱了,缩进没了,表格挤成一团,目录页码全对不上。改到半夜还在跟"回车键到底按没按对"较劲。
我们踩了三个月坑,把Markdown转Word这条路跑通了。不是用花里胡哨的在线工具,就是两个能扔到服务器上跑的自写Python脚本。今天全给你。
一、为什么Markdown转Word总是翻车
先搞清楚痛点在哪。以前最常用的路子是:Markdown → HTML → Word,中间夹一层在线转换器。
翻车就翻在这层。
1. 字体丢失:中文默认变成宋体还好,代码块、表格里的英文直接用回Times,混在一起像贴了补丁。
2. 缩进错乱:Markdown的列表层级,转出来的Word要么没缩进要么缩进过头。
3. 表格崩坏:在线工具对中文表格支持差,列宽失控,表头不重复。
4. 图片漂移:图插进去位置不对,标书里一张架构图位置错了,整章都得重排。
结论是啥?别走HTML中转,直接用python-docx按规则生成。 Word文件本质上是个XML,python-docx让你直接操作结构,绕开那些"看一眼再猜"的转换器。
二、第一个脚本:Markdown到Word的自动转换器
先把环境安好。python-docx是唯一硬依赖:
然后上核心脚本 `markdown2docx.py`。它只做了四件事:识别标题层级、解析列表、画表格、设置中文字体。关键就是那个字体设置——必须单独设置eastAsia字体,否则中文不生效,这是python-docx最大的坑:
def set_run_font(run, name_cn="宋体", size=12, bold=False): run.font.name = "Times New Roman" # 西文字体 r = run._element.rPr.rFonts r.set(qn("w:eastAsia"), name_cn) # ← 中文必须走这个通道 run.font.size = Pt(size) run.font.bold = bold光设 `run.font.name` 只改英文字体,中文显示全靠 `w:eastAsia`。漏了这行,中文就永远用默认字体,怎么设size都不对。
转换的核心逻辑,用正则逐行扫,标题、列表、表格、引用分开处理:
import re from docx import Document from docx.shared import Pt, Inches from docx.oxml.ns import qn def md_to_docx(md_text, doc): lines = md_text.split("\n") i = 0 while i < len(lines): s = lines[i].strip() if not s: i += 1 continue # 标题:# / ## / ### → 层级1/2/3 h = re.match(r"^(#{1,3})\s+(.*)$", s) if h: add_heading(doc, h.group(2), len(h.group(1))) i += 1 continue # 表格:以 | 开头的一串行 if s.startswith("|"): j = i rows = [] while j < len(lines) and lines[j].strip().startswith("|"): raw = [c.strip() for c in lines[j].strip().strip("|").split("|")] if not all(re.match(r"^:?-+:?$", c) for c in raw): rows.append(raw) j += 1 add_table(doc, rows) i = j continue # 普通段落,首行缩进2字符 add_body(doc, s) i += 1表格是标书的重灾区,单独抽出。先建表再逐格填,并对齐表头:
def add_table(doc, rows): n_cols = len(rows[0]) table = doc.add_table(rows=len(rows), cols=n_cols) table.style = "Table Grid" # 全边框 table.alignment = WD_TABLE_ALIGNMENT.CENTER for i, row in enumerate(rows): for j, cell_text in enumerate(row): cell = table.cell(i, j) cell.text = cell_text.strip() # ← 这里别写成 cell (下面踩坑细说) for par in cell.paragraphs: for run in par.runs: set_run_font(run, "宋体", 11, bold=(i == 0)) # 表头加粗
这段代码里藏着一个我实测踩过的坑,一会儿专门讲。
三、第二个脚本:格式自动检测
转出来的Word还得过一道自检,因为人会漏,脚本不会。全角半角标点混入是标书最常见的低级错误——"招商银行:"和"招商银行:"混着用,甲方一眼就看出不专业。
`format_check.py` 干这个活。核心逻辑:扫出中文句子里的半角标点,报出来让作者改:
def check(text): problems = [] for idx, line in enumerate(text.split("\n"), 1): s = line.strip() if s.startswith("```") or not s: # 跳过代码块和空行 continue # 保护URL和代码,避免误报 s = re.sub(r"https?://\S+", lambda x: "#"*len(x.group()), s) # 中文出现半角标点 → 报错 for half in [",", ";", ":", "!", "?", "(", ")"]: if half in s and re.search(r"[\u4e00-\u9fff]", s): problems.append(f"L{idx}: 中文处出现半角『{half}』 <- {s[:40]}") return problems实测运行长这样(故意留了个半角冒号):
$ python3 format_check.py dirty.md ⚠️ 发现 4 处问题: L1: 中文处出现半角『:』,应改全角『:』 (原文: # 这一段的冒号用的是半角:) L2: 中文处出现半角『,』,应改全角『,』 (原文: ## 检测不到就出问题,,这里逗号也是半角) L4: 中文处出现半角『?』,应改全角『?』 (原文: > 引用里带了半角问号?这不对) L6: 中文处出现半角『:』,应改全角『:』 (原文: 文案里有URL: http://example.com/path?a=1 应该保留半角)
一个容易踩的坑:数字编号里的点不能被误报。 "1.1项目概述"、"方案1."这些点必须放行,第一版脚本把干净的文档全报了错。解法是剥离编号语义再查:
s = re.sub(r"(?:\d+\.)+\d*", "", s) # 先剥掉 1.1 / 1.2.1 这类编号 if "." in s: # 剩下的点才是真问题 problems.append(...)
四、我踩过的三个坑(都是流血的教训)
坑1:`cell = table.cell(i, j)` 不能写成 `cell.text` 去设。
我第一版写 `add_table` 时,以为遍历出来的 `cell` 可以直接 `.text=`,结果报错 `'str' object has no attribute 'text'`——因为 `for cell in row` 遍历出来的是字符串(表格的一行),不是单元格对象。正确姿势是 `table.cell(行号, 列号)` 定位。
坑2:中文字体必须设 `w:eastAsia`。
光设 `run.font.name`,中文永远是默认黑体方框。这是python-docx文档都不会明说的地方,不设的话转出来中文排版全废。
坑3:格式检查器会误报编号点。
第一版把"1.1项目概述"里的点当成半角句号报错,干净的文档也过不了检。不先剥离编号语义,检查器就是一堆狼来了。
五、完整链路,能直接上服务器跑
整个流程串起来就三步,几十个标书文件都能批量处理:
# 1. 格式自检(先改掉全角半角问题) python3 format_check.py 方案.md # 2. 转Word(n个文件循环跑) for f in 标书*.md; do python3 markdown2docx.py "$f" -o "${f%.md}.docx" done # 3. 检查生成的docx ls -la *.docx我算过一笔账。以前一份100页的标书,排版耗时2-3小时,返工率高;现在写内容(Markdown)→ 自检 → 一键转Word,排版时间压到3分钟,返工几乎为零。省下来的时间,都用来改内容质量了。
标书这行的核心竞争力从来不是排版,是内容。 排版那点破事,交给脚本,让人去干人该干的活。
投稿:硅基聊斋 / 栏目:专项攻略