当前位置: 代码网 > it编程>前端脚本>Python > Python实现Markdown转Word文档的工具详解

Python实现Markdown转Word文档的工具详解

2026年04月01日 Python 我要评论
一、背景与动机在日常开发中,技术文档通常以 markdown 格式编写,但交付给客户或非技术人员时,往往需要 word(docx)格式。手工复制粘贴不仅效率低,还会丢失格式。基于 python 的 p

一、背景与动机

在日常开发中,技术文档通常以 markdown 格式编写,但交付给客户或非技术人员时,往往需要 word(docx)格式。手工复制粘贴不仅效率低,还会丢失格式。基于 python 的 python-docx 库,可以编写一个自动化脚本,将 markdown 文件批量转换为排版美观的 word 文档。

二、技术选型

依赖库版本用途
python-docx0.8+创建和修改 docx 文件
re内置正则匹配 markdown 语法
docx.shared设置字体大小、颜色、英寸等单位
docx.enum.text段落对齐方式等枚举
docx.oxml.ns操作 word 底层 xml 命名空间

核心依赖只有一个:

pip install python-docx

三、整体架构

markdown 文件(.md)
       │
       ▼
  读取文件内容(utf-8 编码)
       │
       ▼
  逐行解析 markdown 语法
  ┌────────────────────┐
  │ # ~ #####          │ → 标题(h1 ~ h5)
  │ ```代码块 ```   │ → 代码块(consolas 字体)
  │ | 表格 | 表格 |    │ → 带边框表格
  │ - 列表 / 1. 列表   │ → 无序/有序列表
  │ **粗体**           │ → 加粗文本
  │ `行内代码`         │ → 等宽高亮文本
  │ ---                │ → 分隔线
  │ 流程图字符         │ → 等宽小号文本
  │ 普通文本           │ → 正文段落
  └────────────────────┘
       │
       ▼
  生成 docx 文档并保存

四、核心实现

4.1 文档初始化与默认字体

word 默认字体不支持中文,需要手动设置中文字体(微软雅黑):

from docx import document
from docx.shared import pt
from docx.oxml.ns import qn

doc = document()

# 设置默认字体
doc.styles['normal'].font.name = '微软雅黑'
doc.styles['normal']._element.rpr.rfonts.set(qn('w:eastasia'), '微软雅黑')
doc.styles['normal'].font.size = pt(11)

关键点

  • font.name 只设置了西文字体
  • 需要通过底层 xml rfonts.set(qn('w:eastasia'), '微软雅黑') 单独设置中文字体,否则中文会回退到宋体

4.2 标题解析

通过正则匹配 # 的数量判断标题级别:

if line.startswith('# '):
    p = doc.add_heading(line[2:].strip(), level=1)
    p.alignment = wd_align_paragraph.left
elif line.startswith('## '):
    p = doc.add_heading(line[3:].strip(), level=2)
elif line.startswith('### '):
    p = doc.add_heading(line[4:].strip(), level=3)

doc.add_heading(text, level) 支持级别 1~5,word 会自动应用对应的标题样式。

4.3 代码块处理

代码块用三个反引号包裹,需要维护一个状态标志:

in_code_block = false
code_content = []

while i < len(lines):
    line = lines[i]

    if line.strip().startswith('```'):
        if not in_code_block:
            in_code_block = true
            code_content = []
        else:
            # 代码块结束,写入文档
            code_text = '\n'.join(code_content)
            p = doc.add_paragraph()
            run = p.add_run(code_text)
            run.font.name = 'consolas'
            run.font.size = pt(9)
            run.font.color.rgb = rgbcolor(39, 86, 136)  # 深蓝色
            p.style = 'no spacing'  # 取消段落间距
            in_code_block = false
        i += 1
        continue

    if in_code_block:
        code_content.append(line)
        i += 1
        continue

关键点

  • p.style = 'no spacing' — 取消代码块上下方的段落间距,避免代码块之间出现大段空白
  • 使用 consolas 等宽字体,字体大小设为 9pt(比正文小),颜色设为深蓝色以区分正文

4.4 表格解析

markdown 表格使用 | 分隔列,--- 分隔表头和数据行:

| 字段 | 类型 | 说明 |
|------|------|------|
| id   | long | 主键 |
| name | string | 名称 |

解析逻辑:

# 收集连续的表格行
table_lines = []
while i < len(lines) and lines[i].startswith('|'):
    table_lines.append(lines[i])
    i += 1

# 解析表格数据,跳过分隔行
rows = []
for tl in table_lines:
    if '---' not in tl:
        cells = [c.strip() for c in tl.split('|')[1:-1]]
        rows.append(cells)

# 创建 word 表格
table = doc.add_table(rows=len(rows), cols=len(rows[0]))
table.style = 'light grid accent 1'

for ri, row_data in enumerate(rows):
    for ci, cell_data in enumerate(row_data):
        cell = table.rows[ri].cells[ci]
        cell.text = cell_data
        set_cell_border(cell)  # 设置边框

        if ri == 0:  # 表头加粗
            run = cell.paragraphs[0].runs[0]
            run.font.bold = true
            run.font.size = pt(11)
        else:
            run = cell.paragraphs[0].runs[0]
            run.font.size = pt(10)

4.5 单元格边框设置

python-docx 创建的表格默认可能缺少边框,需要通过底层 xml 手动添加:

from docx.oxml import oxmlelement

def set_cell_border(cell):
    tcpr = cell._element.get_or_add_tcpr()
    tcborders = oxmlelement('w:tcborders')

    for border_name in ['top', 'left', 'bottom', 'right']:
        border = oxmlelement(f'w:{border_name}')
        border.set(qn('w:val'), 'single')
        border.set(qn('w:sz'), '4')        # 边框宽度(1/8 磅为单位)
        border.set(qn('w:space'), '0')
        border.set(qn('w:color'), '000000')  # 黑色
        tcborders.append(border)

    tcpr.append(tcborders)

原理:word 的单元格边框定义在 w:tcborders xml 元素中,python-docx 的高级 api 没有直接暴露此功能,需要操作 ooxml 底层元素。

4.6 行内格式解析

粗体(**text**)

elif '**' in line:
    p = doc.add_paragraph()
    parts = re.split(r'\*\*(.+?)\*\*', line)
    for j, part in enumerate(parts):
        if j % 2 == 1:      # 奇数索引 = 粗体部分
            run = p.add_run(part)
            run.bold = true
        else:                 # 偶数索引 = 普通文本
            if part:
                p.add_run(part)
    # 如果段落为空则移除
    if not p.text.strip():
        p._element.getparent().remove(p._element)

核心思路:用正则 re.split(r'\*\*(.+?)\*\*', line) 将文本按粗体标记分割,奇数位就是粗体内容,偶数位是普通文本。

行内代码(`code`)

elif '`' in line:
    p = doc.add_paragraph()
    parts = re.split(r'`(.+?)`', line)
    for j, part in enumerate(parts):
        if j % 2 == 1:      # 奇数索引 = 代码部分
            run = p.add_run(part)
            run.font.name = 'consolas'
            run.font.size = pt(10)
            run.font.color.rgb = rgbcolor(199, 37, 78)  # 红色
        else:
            if part:
                p.add_run(part)

4.7 列表解析

# 无序列表(- / * / +)
elif re.match(r'^\s*[\-\*\+]\s+', line):
    p = doc.add_paragraph(line.strip()[2:].strip(), style='list bullet')

# 有序列表(1. 2. 3.)
elif re.match(r'^\s*\d+\.\s+', line):
    p = doc.add_paragraph(
        re.sub(r'^\s*\d+\.\s+', '', line),
        style='list number'
    )

4.8 流程图与特殊字符

markdown 中的流程图(如用 ┌│└─├→ 等字符绘制的图)需要等宽小号字体才能对齐:

flow_chars = ['┌', '│', '└', '─', '├', '┼', '┬', '┐', '┘', '┤', '┴', '▲', '▼', '→']

if any(x in line for x in flow_chars):
    p = doc.add_paragraph()
    run = p.add_run(line)
    run.font.name = 'consolas'
    run.font.size = pt(8)      # 更小的字号
    p.style = 'no spacing'     # 无间距

五、批量转换主函数

import os

def main():
    md_files = [
        '/path/to/doc1.md',
        '/path/to/doc2.md'
    ]

    output_files = [
        '/path/to/doc1.docx',
        '/path/to/doc2.docx'
    ]

    for md_file, output_file in zip(md_files, output_files):
        print(f'正在转换: {md_file}')

        with open(md_file, 'r', encoding='utf-8') as f:
            md_content = f.read()

        title = md_content.split('\n')[0].replace('#', '').strip()

        doc = markdown_to_docx(md_content, title)
        doc.save(output_file)
        print(f'已生成: {output_file}')

六、支持的 markdown 语法汇总

语法markdown 写法转换效果
一级标题# 标题word heading 1
二级标题## 标题word heading 2
三~五级标题### ~ #####word heading 3~5
代码块` ```code ````consolas 9pt 蓝色
行内代码`code`consolas 10pt 红色
粗体**text**加粗
无序列表- itemlist bullet 样式
有序列表1. itemlist number 样式
表格| col | col |带边框的 word 表格
分隔线---下划线
流程图┌───┐consolas 8pt 等宽

七、局限性与优化方向

当前局限

  • 不支持图片(markdown 的 ![alt](url) 未解析)
  • 不支持超链接
  • 不支持嵌套列表(多层缩进的列表)
  • 行内格式只支持粗体和行内代码,不支持斜体、删除线
  • 表格不支持合并单元格

优化方向

# 1. 图片支持:下载网络图片并插入
from docx.shared import inches
p = doc.add_paragraph()
run = p.add_run()
run.add_picture('image.png', width=inches(5.0))

# 2. 超链接支持
from docx.oxml.ns import qn
from docx.oxml import oxmlelement

def add_hyperlink(paragraph, text, url):
    part = paragraph.part
    r_id = part.relate_to(url, 'http://schemas.openxmlformats.org/officedocument/2006/relationships/hyperlink', is_external=true)
    hyperlink = oxmlelement('w:hyperlink')
    hyperlink.set(qn('r:id'), r_id)
    new_run = oxmlelement('w:r')
    rpr = oxmlelement('w:rpr')
    new_run.append(rpr)
    text_elem = oxmlelement('w:t')
    text_elem.text = text
    new_run.append(text_elem)
    hyperlink.append(new_run)
    paragraph._p.append(hyperlink)
    return hyperlink

更优的替代方案

如果对格式要求更高,可以考虑以下成熟工具:

工具特点安装方式
pandoc功能最全,支持几乎所有 markdown 扩展语法brew install pandoc
markdown2docx轻量级,专为中文优化pip install markdown2docx
mammoth反向转换(docx → html/markdown)pip install mammoth
# pandoc 一行命令即可完成转换
pandoc input.md -o output.docx --reference-doc=template.docx

pandoc 支持 --reference-doc 参数,可以指定一个 word 模板文件来控制输出样式(字体、颜色、页边距等),这是最推荐的方案。

八、完整代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
from docx import document
from docx.shared import pt, inches, rgbcolor
from docx.enum.text import wd_align_paragraph
from docx.oxml.ns import qn


def set_cell_border(cell):
    """设置单元格边框"""
    from docx.oxml import oxmlelement
    tcpr = cell._element.get_or_add_tcpr()
    tcborders = oxmlelement('w:tcborders')
    for border_name in ['top', 'left', 'bottom', 'right']:
        border = oxmlelement(f'w:{border_name}')
        border.set(qn('w:val'), 'single')
        border.set(qn('w:sz'), '4')
        border.set(qn('w:space'), '0')
        border.set(qn('w:color'), '000000')
        tcborders.append(border)
    tcpr.append(tcborders)


def markdown_to_docx(md_content, title):
    """将 markdown 内容转换为 docx 文档"""
    doc = document()
    doc.styles['normal'].font.name = '微软雅黑'
    doc.styles['normal']._element.rpr.rfonts.set(qn('w:eastasia'), '微软雅黑')
    doc.styles['normal'].font.size = pt(11)

    lines = md_content.split('\n')
    i = 0
    in_code_block = false
    code_content = []

    while i < len(lines):
        line = lines[i]

        # 代码块
        if line.strip().startswith('```'):
            if not in_code_block:
                in_code_block = true
                code_content = []
            else:
                p = doc.add_paragraph()
                run = p.add_run('\n'.join(code_content))
                run.font.name = 'consolas'
                run.font.size = pt(9)
                run.font.color.rgb = rgbcolor(39, 86, 136)
                p.style = 'no spacing'
                in_code_block = false
            i += 1
            continue

        if in_code_block:
            code_content.append(line)
            i += 1
            continue

        # 标题
        if line.startswith('# '):
            doc.add_heading(line[2:].strip(), level=1)
        elif line.startswith('## '):
            doc.add_heading(line[3:].strip(), level=2)
        elif line.startswith('### '):
            doc.add_heading(line[4:].strip(), level=3)

        # 表格
        elif line.startswith('|') and '|' in line:
            table_lines = []
            while i < len(lines) and lines[i].startswith('|'):
                table_lines.append(lines[i])
                i += 1
            rows = []
            for tl in table_lines:
                if '---' not in tl:
                    cells = [c.strip() for c in tl.split('|')[1:-1]]
                    rows.append(cells)
            if rows:
                table = doc.add_table(rows=len(rows), cols=len(rows[0]))
                table.style = 'light grid accent 1'
                for ri, row_data in enumerate(rows):
                    for ci, cell_data in enumerate(row_data):
                        cell = table.rows[ri].cells[ci]
                        cell.text = cell_data
                        set_cell_border(cell)
            continue

        # 列表
        elif re.match(r'^\s*[\-\*\+]\s+', line):
            doc.add_paragraph(line.strip()[2:].strip(), style='list bullet')
        elif re.match(r'^\s*\d+\.\s+', line):
            doc.add_paragraph(re.sub(r'^\s*\d+\.\s+', '', line), style='list number')

        # 粗体
        elif '**' in line:
            p = doc.add_paragraph()
            parts = re.split(r'\*\*(.+?)\*\*', line)
            for j, part in enumerate(parts):
                if j % 2 == 1:
                    run = p.add_run(part)
                    run.bold = true
                elif part:
                    p.add_run(part)

        # 行内代码
        elif '`' in line:
            p = doc.add_paragraph()
            parts = re.split(r'`(.+?)`', line)
            for j, part in enumerate(parts):
                if j % 2 == 1:
                    run = p.add_run(part)
                    run.font.name = 'consolas'
                    run.font.size = pt(10)
                    run.font.color.rgb = rgbcolor(199, 37, 78)
                elif part:
                    p.add_run(part)

        # 普通段落
        elif line.strip():
            doc.add_paragraph(line.strip())

        i += 1

    return doc

九、总结

本脚本通过逐行解析 markdown 文本,利用 python-docx 库生成对应格式的 word 文档,主要涉及以下关键技术点:

  • 中文字体设置:通过 ooxml 底层 xml 设置东亚字体
  • 状态机解析:用 in_code_block 标志处理多行代码块
  • 正则分割:用 re.split 处理行内格式(粗体、行内代码)
  • ooxml 操作:通过 oxmlelement 手动添加表格边框
  • 样式控制:使用 no spacing 样式消除代码块间距

对于简单的文档转换需求,这个脚本足够使用;如果需要更完整的格式支持,建议使用 pandoc 等成熟工具。

到此这篇关于python实现markdown转word文档的工具详解的文章就介绍到这了,更多相关python markdown转 word内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com