一、导出功能概述与核心价值

“导出自由”不是营销话术,而是一种技术解耦能力:让你专注写内容(用 Markdown),无需操心最终交付形态。它意味着——无论你今天写的是 API 文档草稿、论文笔记,还是周报初稿,只需一次保存,就能在秒级内生成结构完整、样式精准、语义无损的 PDF 归档版、可嵌入网站的 HTML 版,或供 CLI 工具链消费的纯文本版。

对比手动导出痛点,差异立现:
❌ 复制粘贴到 Word → 标题层级塌陷、代码块变乱码、数学公式消失
❌ 用 Typora 导出 PDF → 中文缺字、页眉丢失、目录不生成
❌ 用 Pandoc 转 HTML → 需手写模板、高亮失效、图片路径全错

本教程面向三类高频场景:

  • 技术文档工程师:为 OpenAPI/SDK 文档构建轻量 CI 导出流水线
  • 学术研究者:将 Obsidian/Typora 笔记一键转为可投稿的 PDF + 网页版
  • 自动化报告开发者:将日志分析结果(Markdown 模板 + Jinja2 渲染)批量导出多端

我们支持三大目标格式,各司其职:

  • PDF:用于归档、邮件分发、打印 —— 要求字体嵌入、页眉页脚、自动生成目录
  • HTML:用于 GitHub Pages、内部 Wiki、Notion 嵌入 —— 要求响应式、语法高亮、相对资源可访问
  • 纯文本(.txt):用于 grep 检索、AI 模型微调输入、Git diff 审阅 —— 要求语义降级(非简单去标签),保留标题层级与列表结构

底层采用轻量原生方案:全程基于 Python 标准库 + 经过生产验证的稳定包(markdown, weasyprint, pygments, markdown-it-py),不依赖 Node.js 或 LaTeX,避免环境臃肿。Pandoc / mdbook 等重型工具留作进阶扩展选项,本文聚焦“最小可行导出系统”。

导出流程示意图:左侧 Markdown 源文件,中间三路箭头分别指向 PDF/HTML/TXT 图标,右侧展示对应效果预览

二、环境准备与依赖安装

最小依赖清单(Python 3.9+):

  • markdown==3.6.0(标准解析器,兼容 CommonMark)
  • weasyprint==64.0(CSS 渲染 PDF 的事实标准)
  • pygments==2.17.2(代码高亮引擎,支持 300+ 语言)
  • markdown-it-py==3.0.0(可选,用于纯文本 AST 解析,比正则更健壮)

跨平台安装命令(带注释)

# 【通用】Python 包安装
pip install markdown weasyprint pygments markdown-it-py

# 【macOS】WeasyPrint 系统依赖(必须!否则 PDF 中文报错)
brew install cairo pango gdk-pixbuf libffi

# 【Windows】使用 Chocolatey(管理员权限)
choco install gtk3-runtime

# 【Linux(Ubuntu/Debian)】
sudo apt-get install libpango-1.0-0 libcairo2 libgdk-pixbuf2.0-0 libffi7

# ✅ 快速验证安装成功
python -c "import weasyprint; print(f'WeasyPrint {weasyprint.__version__} OK')"

⚠️ 关键提醒:中文 PDF 支持 = 字体配置 + CSS 注入
WeasyPrint 默认不包含中文字体。你需:

  1. 确保系统已安装中文字体(如 macOS 的 PingFang SC,Windows 的 Microsoft YaHei,Linux 的 Noto Sans CJK SC
  2. 在后续 CSS 中通过 @font-face 显式声明(见第四节),不可仅靠 font-family 名称 fallback

三、基础导出:Markdown源文件到HTML(零配置)

无需模板引擎,5 行代码即可完成 .md → .html 转换,并内置语法高亮:

import markdown
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter

# 读取 Markdown
with open("input.md", encoding="utf-8") as f:
    md_text = f.read()

# 启用扩展(表格、代码块、自动链接)
html = markdown.markdown(
    md_text,
    extensions=[
        "tables", "fenced_code", "codehilite",
        "attr_list", "md_in_html"
    ],
    extension_configs={
        "codehilite": {
            "css_class": "highlight",
            "guess_lang": False,
            "pygments_style": "github-dark"
        }
    }
)

# 注入 Pygments 样式 + UTF-8 声明
css = HtmlFormatter(style="github-dark").get_style_html()
html_full = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Export</title>{css}</head>
<body>{html}</body></html>"""

with open("output.html", "w", encoding="utf-8") as f:
    f.write(html_full)

📌 注意事项

  • meta charset="utf-8" 是强制项,缺失将导致中文乱码
  • 图片/链接相对路径默认保留(![](img/logo.png) → HTML 中仍为 img/logo.png
  • 特殊字符(<, >, &)由 markdown 自动转义,无需额外处理

四、专业导出:Markdown源文件到PDF(样式可控)

核心思路:先生成语义正确的 HTML,再用 WeasyPrint 渲染为 PDF。CSS 是唯一控制层,告别 LaTeX 编译地狱。

from weasyprint import HTML, CSS
from pathlib import Path

# 复用上一步生成的 html_full 字符串
pdf_bytes = HTML(string=html_full).write_pdf(
    stylesheets=[
        CSS(string="""
            @page { margin: 2cm; size: A4; }
            @page :first { @top-center { content: '技术文档'; } }
            body { font-family: "Noto Sans CJK SC", sans-serif; line-height: 1.6; }
            h1 { color: #2c3e50; border-bottom: 2px solid #3498db; }
            .highlight { background: #2d2d2d; padding: 1em; border-radius: 4px; }
            @font-face {
                font-family: 'Noto Sans CJK SC';
                src: local('Noto Sans CJK SC'), local('PingFang SC');
            }
        """)
    ],
    presentational_hints=True,  # 继承 HTML 内联样式(如 <h1 style="color:red">)
    zoom=1.0  # 防止缩放失真
)

with open("output.pdf", "wb") as f:
    f.write(pdf_bytes)

✅ 关键 CSS 片段说明:

  • @page { margin: 2cm }:统一设置页边距
  • @page :first { @top-center { ... } }:首页页眉(WeasyPrint ≥62 支持)
  • @font-face:显式绑定中文字体,解决“Failed to load font”错误

五、极简导出:Markdown到纯文本(结构化提取)

目标:把 ## API 设计## API 设计- 参数校验- 参数校验,代码块 → 缩进 + // CODE: 注释,而非 <h2>API 设计</h2>API 设计(丢失层级)

推荐使用 markdown-it-py 解析 AST(比正则鲁棒):

from markdown_it import MarkdownIt
from mdit_py_plugins.front_matter import front_matter_plugin

def md_to_plain(md_text: str, indent_level: int = 0) -> str:
    md = MarkdownIt("commonmark").use(front_matter_plugin)
    tokens = md.parse(md_text)
    
    def walk(tokens, depth=0) -> str:
        result = []
        for t in tokens:
            if t.type == "heading_open":
                level = int(t.tag[1])
                result.append("#" * level + " " + tokens[t.map[0]].content.strip())
            elif t.type == "fence":
                code = t.content.strip()
                result.append(" " * (depth * 2) + "// CODE: " + t.info.strip())
                result.append(" " * (depth * 2) + code.replace("\n", "\n" + " " * (depth * 2)))
            elif t.type == "list_item_open":
                result.append(" " * (depth * 2) + "- " + tokens[t.map[0]].content.strip())
            elif t.type == "paragraph_open":
                result.append(" " * (depth * 2) + tokens[t.map[0]].content.strip())
        return "\n".join(result)
    
    return walk(tokens)

📌 注意事项:表格需特殊处理(转为 |列1|列2|),链接保留 [text](url) 原始语法。

六、一键封装:Shell/Python脚本实现三格式批量导出

完整脚本 export.py(支持 python export.py input.md --format all):
👉 [GitHub Gist 链接](文中略,实际部署时提供)

核心逻辑:

  • argparse 解析 --format pdf,html,txt
  • 自动推导输出名:input.mdinput.pdf, input.html, input.txt
  • 进度提示:✓ Generated input.pdf (124KB)
  • 异常捕获:对每个格式单独 try/catch,失败不影响其余格式

Shell 别名速配:

echo "alias md2all='python3 ~/scripts/export.py --format all'" >> ~/.zshrc
source ~/.zshrc
# 使用:md2all report.md

扩展新格式(如 EPUB)?只需在 export.py 中添加 elif fmt == "epub": 分支,调用 pandoc 子进程即可 —— 我们预留了 format_hooks 接口。

七、常见问题排查指南

错误现象精准修复步骤
WeasyPrint: Failed to load font① 检查 CSS 中 @font-face src: 路径是否为绝对路径或正确 local() 名;② macOS 执行 fc-list | grep -i "Noto|PingFang" 验证字体存在;③ 临时改用 src: url(/System/Library/Fonts/PingFang.ttc)
HTML 代码块无高亮① 确认 markdown 调用含 codehilite 扩展;② 检查 HtmlFormatter().get_style_html() 是否注入 <style> 标签;③ 浏览器 F12 查看 <pre class="highlight"> 是否存在
PDF 页眉不显示@page :first 仅 WeasyPrint ≥62 支持,运行 pip install --upgrade weasyprint;② 确保 CSS 在顶层(不在 <style scoped> 内);③ 删除 presentational_hints=False 干扰
纯文本列表缩进错乱① AST 方案中检查 list_item token 的 tight 属性(影响空行);② 为 list_item_open 添加深度计数器,递归传递 depth+1

八、进阶技巧与安全提醒

🔒 安全加固(生产必备)

  • 禁用原始 HTMLextensions=["fenced_code", "tables"] 中移除 "raw_html"
  • HTML 输出 XSS 防护html.escape() 包裹用户输入段落(非整个 HTML)
  • PDF 资源限制:WeasyPrint 默认禁止 http: https: 协议,仅允许 file: —— 无需额外配置

⚡ 性能优化

  • 大文件(>10MB):用 markdown-it-py 分块解析,避免内存溢出
  • PDF 缓存:计算源文件 MD5,若未变更则跳过渲染,直接复制缓存 PDF

♿ 可访问性增强

  • HTML 输出:为代码块添加 aria-label="Python 示例代码"
  • PDF 生成:write_pdf(..., optimize_size=True, embed_fonts=True) 确保屏幕阅读器识别字体

三格式导出成果对比图:左 PDF(带页眉/目录)、中 HTML(带高亮/响应式)、右 TXT(带缩进/语义标记)

导出自由的本质,是把重复劳动交给机器,把创造力还给人。当你不再为格式焦头烂额,真正的技术写作才刚刚开始。现在,打开你的第一个 .md 文件,执行 md2all —— 三份成品,已在等待。