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

 更新时间:2026年04月01日 09:28:31   作者:未来转换  
这篇文章主要为大家详细介绍了一个基于Python的Markdown转Word文档工具的实现方案,该工具主要使用python-docx库,能够将Markdown文件自动转换为排版规范的Word文档,希望对大家有所帮助

一、背景与动机

在日常开发中,技术文档通常以 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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一文带你了解Python中的生成器和迭代器

    一文带你了解Python中的生成器和迭代器

    生成器(Generators)和迭代器(Iterators)是 Python 中用于处理序列数据的强大工具,本文主要来和大家介绍一下它们的具体使用,方便大家更好的了解它们,需要的可以学习下
    2022-03-03
  • Python实现在数字中添加千位分隔符的方法小结

    Python实现在数字中添加千位分隔符的方法小结

    在数据处理和数据可视化中,经常需要对大数值进行格式化,其中一种常见的需求是在数字中添加千位分隔符,本文为大家整理了三种常见方法,希望对大家有所帮助
    2024-01-01
  • caffe的python接口绘制loss和accuracy曲线

    caffe的python接口绘制loss和accuracy曲线

    这篇文章主要为大家介绍了caffe的python接口绘制loss和accuracy曲线示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • 用python实现一个文件搜索工具

    用python实现一个文件搜索工具

    大家好,本篇文章主要讲的是用python实现一个搜索工具,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • Python实现Window路径格式转换为Linux路径格式的代码

    Python实现Window路径格式转换为Linux路径格式的代码

    这篇文章主要介绍了Python实现Window路径格式转换为Linux路径格式的方法,文中通过代码示例讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-07-07
  • python基础教程之匿名函数lambda

    python基础教程之匿名函数lambda

    这篇文章主要介绍了 python基础教程之匿名函数lambda的相关资料,需要的朋友可以参考下
    2017-01-01
  • python比较2个xml内容的方法

    python比较2个xml内容的方法

    这篇文章主要介绍了python比较2个xml内容的方法,涉及Python操作XML文件的相关技巧,需要的朋友可以参考下
    2015-05-05
  • Python并发执行的几种实现方法

    Python并发执行的几种实现方法

    在Python中多线程是实现并发的一种方式,多线程可以让程序在同一时间内进行多个任务,从而提高程序的效率和执行速度,这篇文章主要给大家介绍了关于Python并发执行的几种实现方法,需要的朋友可以参考下
    2024-08-08
  • Python使用os模块操作文件与目录的完整指南

    Python使用os模块操作文件与目录的完整指南

    os 模块是Python标准库的一部分,无需额外安装即可使用,它封装了不同操作系统的底层系统调用,它可以让我们在Python中执行各种操作系统级别的任务,所以本文给大家介绍了Python使用os模块操作文件与目录的完整指南,需要的朋友可以参考下
    2026-01-01
  • 解决django的template中如果无法引用MEDIA_URL问题

    解决django的template中如果无法引用MEDIA_URL问题

    这篇文章主要介绍了解决django的template中如果无法引用MEDIA_URL问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-04-04

最新评论