Python实战之Markdown转PDF功能实现与页面设置
作者:缺点内向
在日常开发或文档编写中,Markdown 凭借其轻量、易读的语法,成为了许多技术文档和笔记的首选格式。然而,当需要将文档分享给非技术人员、进行打印归档,或者提交正式报告时,PDF 因其跨平台排版相对稳定的特性,往往是更合适的最终交付格式。手动使用编辑器导出虽然可行,但在涉及批量处理或需要集成到自动化流程中时,通过编程方式完成转换会更加高效。
本文将介绍如何利用 Python 的 Spire.Doc 库,以代码化的方式实现 Markdown 到 PDF 的格式转换,并针对页面设置进行基础的自定义调整。
环境准备与库简介
Spire.Doc for Python 是一款用于处理 Word 文档的库,它提供了文档创建、编辑与格式转换接口。从 12.6.0 版本开始,该库支持加载 Markdown 文档并将其转换为 Word、PDF 等格式。这意味着不需要依赖外部的 Pandoc 或浏览器引擎,直接在 Python 脚本中即可完成格式流转。
在开始之前,请确保 Python 环境已就绪,并通过 pip 安装该库:
pip install Spire.Doc
安装完成后,在脚本中导入核心模块,并注意区分 spire.doc 与 spire.doc.common 中的类,后者常用于处理文件格式枚举。
from spire.doc import * from spire.doc.common import *
基础转换:从 Markdown 到 PDF
最简化的转换流程仅需三步:创建文档对象、加载 Markdown 文件、保存为 PDF。Document.LoadFromFile 方法允许通过 FileFormat.Markdown 参数显式指定输入格式,从而正确解析 Markdown 中的标题、列表、代码块等结构。
# 创建 Document 实例
doc = Document()
# 加载 Markdown 源文件
doc.LoadFromFile("示例文档.md", FileFormat.Markdown)
# 直接导出为 PDF
doc.SaveToFile("输出文档.pdf", FileFormat.PDF)
# 释放资源
doc.Dispose()
这段代码执行后,会在当前目录生成一个与源文件同名的 PDF。对于内容结构标准、无需特殊版式调整的 Markdown 文件,这种方式已经能够满足基本的阅读与分享需求。生成的 PDF 会保留 Markdown 中的层级标题和段落样式,但在字体渲染上会回退到库内置的默认字体。
进阶调整:自定义 PDF 页面设置
直接转换虽然便捷,但默认的 A4 纵向页面和固定的页边距未必适合所有场景。例如,当 Markdown 中包含较宽的表格或长代码行时,横向页面能有效减少内容被截断的情况;而在准备打印稿时,适当增加页边距则能提升阅读舒适度。
Spire.Doc 允许在保存前访问文档的 Section 对象,进而修改 PageSetup 属性。需要注意的是,Markdown 加载后默认至少包含一个节(Section),我们可以通过索引 0 获取它。
doc = Document()
doc.LoadFromFile("示例文档.md", FileFormat.Markdown)
# 获取默认的节
section = doc.Sections.get_Item(0)
# 访问页面设置
pageSetup = section.PageSetup
# 调整纸张大小、方向与边距
pageSetup.PageSize = PageSize.A4()
pageSetup.Orientation = PageOrientation.Landscape # 横向
pageSetup.Margins.All = 50 # 统一设置四边边距(单位:磅)
doc.SaveToFile("自定义页面.pdf", FileFormat.PDF)
doc.Dispose()
上述代码中,PageOrientation.Landscape 将页面转为横向,适合宽幅内容;Margins.All 则是一个便捷属性,用于一次性设定上下左右边距。如果只需要调整单侧边距,也可以分别设置 Margins.Top、Margins.Bottom 等属性。
注意事项与局限
虽然 Spire.Doc 提供了便捷的转换通道,但在实际使用中仍需留意几个方面。
免费版本的输出限制。根据官方说明,Free Spire.Doc for Python 在读写文件时存在段落数(500 段)和表格数(25 个)的限制,且在将文档转换为 PDF 时,仅能获取输出结果的前 3 页。如果待转换的 Markdown 文档篇幅较长,免费版本可能无法输出完整内容。对于篇幅较短的笔记或测试用途,这一限制通常不构成问题;但若用于正式的长文档归档,则需要评估商业授权是否符合项目预算。
样式还原的边界。Markdown 本身是一种语义标记语言,它不包含复杂的排版描述。因此,当 Markdown 转换为 PDF 时,输出的视觉样式高度依赖于库对 Markdown 语法的解析策略以及内置的样式映射。对于绝大多数标准语法(如 # 标题、**粗体**、无序列表),还原效果是可预期的;但如果文档中混入了内联 HTML 或复杂的嵌套表格结构,转换结果可能与在 Markdown 编辑器中的预览存在差异。建议在正式批量处理前,先用具有代表性的样本文档进行验证。
字体的处理。默认情况下,生成的 PDF 会使用库内置的字体。如果源 Markdown 中涉及中文与英文混排,或者对特定字体有严格要求,可能需要在代码中进一步显式设置样式,或确认输出效果是否满足需求。对于仅用于内部流转或屏幕阅读的场景,默认字体通常足够清晰。
商业库的授权考量。Spire.Doc 是商业产品,免费版本在功能或页数上存在上述限制。如果项目规模较大或需要长期用于生产环境,建议查阅其官方授权条款,评估是否符合成本预期。对于偏好开源方案的场景,也可以考察 pandoc 配合 WeasyPrint 等工具的组合,但这些方案往往需要额外配置外部二进制依赖,在纯 Python 环境中的集成步骤与 Spire.Doc 有所不同。
方法补充
Markdown 本身无法直接生成 PDF,必须走 Markdown → HTML → PDF 两步路径。第一步由 markdown 库完成,第二步的渲染引擎选择决定了排版质量、依赖复杂度和中文字体处理的难度。
环境配置:先过系统依赖这一关
WeasyPrint 依赖 Pango、Cairo 和 GDK-Pixbuf,pip 安装本身不处理这些 C 库。
macOS(Homebrew):
brew install pango cairo gdk-pixbuf libffi pip install weasyprint markdown pygments
Linux(Debian/Ubuntu):
sudo apt install libpango-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 pip install weasyprint markdown pygments
安装完成后用 python -c "import weasyprint; print(weasyprint.__version__)" 验证。如果报 cairo.h not found 或类似错误,说明系统库缺失或版本过旧,需要先补齐再重试。
可直接运行的基础实现
以下代码覆盖了 Markdown 解析、HTML 组装和 PDF 渲染的最小闭环:
import markdown
from weasyprint import HTML
def md_to_pdf(md_path, pdf_path):
with open(md_path, "r", encoding="utf-8") as f:
md_text = f.read()
html_body = markdown.markdown(
md_text,
extensions=["tables", "fenced_code", "toc", "sane_lists"]
)
full_html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"></head>
<body>{html_body}</body>
</html>"""
HTML(string=full_html).write_pdf(pdf_path)
print(f"PDF 已生成: {pdf_path}")
md_to_pdf("input.md", "output.pdf")extensions 中的 tables 启用表格解析,fenced_code 启用围栏代码块,toc 生成标题锚点。没有 tables,Markdown 表格不会被解析为 <table>,后续的表格 CSS 修复也就无从生效。
必须处理的三个排版问题
1.中文字体:不做这一步,PDF 里全是方块
WeasyPrint 默认字体不含中文字形。必须在 CSS 中显式声明 font-family 回退栈,且不要只写一个字体名,因为不同系统上可用的中文字体不同:
body {
font-family: "Songti SC", "SimSun", "STSong",
"Noto Serif CJK SC", "Microsoft YaHei", sans-serif;
}
h1, h2, h3 {
font-family: "Heiti SC", "SimHei", "STHeiti",
"Noto Sans CJK SC", sans-serif;
}macOS 上 Songti SC(宋体)和 Heiti SC(黑体)通常可用,Linux 上 Noto Serif CJK SC 是稳妥的通用回退。如果 PDF 需要分发到没有安装相同字体的设备上,可以用 @font-face 嵌入字体文件,但路径必须用 file:/// 绝对路径,不能用相对 URL。
2.表格列宽失控:强制 table-layout: fixed
WeasyPrint 对 table-layout: auto 的支持较弱,Markdown 表格在分页或长内容下容易出现列宽错乱。修复方式:
table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 4pt 6pt;
word-wrap: break-word;
}table-layout: fixed 配合 word-wrap: break-word,才能让长单元格内容换行而不是撑破页面。如果表格本身列数很多,还需要在 Markdown 层面拆表,不要指望 CSS 自动处理超宽表格。
3.代码块折行与高亮
代码块常见的翻车是缩进丢失或横向溢出。给 pre 和 pre code 加上:
pre {
white-space: pre-wrap;
word-break: break-word;
background: #f5f5f5;
padding: 8pt;
font-size: 9pt;
}white-space: pre-wrap 保留代码缩进的同时允许折行,避免长行被裁切。如果需要语法高亮,安装 pygments 并在 Markdown 扩展中加入 codehilite:
html_body = markdown.markdown(
md_text,
extensions=["fenced_code", "codehilite", "tables", "toc"],
extension_configs={"codehilite": {"guess_lang": False}}
)WeasyPrint 不支持 JavaScript 动态高亮,高亮必须在 HTML 阶段由 Pygments 生成。
页面设置:通过 CSS @page 控制
WeasyPrint 的页面尺寸、页边距和页眉页脚全部写在 @page 规则里,没有对应的 Python 参数可以调:
@page {
size: A4;
margin: 22mm 18mm 20mm 18mm;
@bottom-right {
content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
font-size: 9pt;
color: #666;
}
}
h1, h2 { break-after: avoid; }size 接受 A4、Letter、210mm 297mm 或 landscape。纸张相关的尺寸用 mm 或 pt,不要用 px,因为 px 在打印上下文中的物理大小依赖 DPI,跨环境不可移植。break-after: avoid 防止标题和后续内容被分到两页。
页眉页脚的 content 不能直接引用 Python 变量,动态内容需要在 Python 里拼进 CSS 字符串再传给 HTML(string=...)。
完整封装:样式 + 分页 + 字体一步到位
把上述修复集中到一份 CSS 中,以下代码可以直接作为脚本使用:
import markdown
from weasyprint import HTML
PAGE_CSS = """
@page {
size: A4;
margin: 22mm 18mm 20mm 18mm;
@bottom-right {
content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
font-size: 9pt; color: #666;
}
}
body {
font-family: "Songti SC", "SimSun", "Noto Serif CJK SC",
"Microsoft YaHei", sans-serif;
font-size: 11pt; line-height: 1.7;
}
h1, h2, h3 {
font-family: "Heiti SC", "SimHei", "Noto Sans CJK SC", sans-serif;
break-after: avoid;
}
table {
table-layout: fixed; width: 100%; border-collapse: collapse;
}
th, td {
border: 1px solid #ccc; padding: 4pt 6pt; word-wrap: break-word;
}
pre {
white-space: pre-wrap; word-break: break-word;
background: #f5f5f5; padding: 8pt; font-size: 9pt;
}
"""
def md_to_pdf(md_path, pdf_path):
with open(md_path, "r", encoding="utf-8") as f:
md_text = f.read()
html_body = markdown.markdown(
md_text,
extensions=["tables", "fenced_code", "codehilite", "toc", "sane_lists"]
)
full_html = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8">
<style>{PAGE_CSS}</style></head>
<body>{html_body}</body></html>"""
HTML(string=full_html).write_pdf(pdf_path)
print(f"PDF 已生成: {pdf_path}")
md_to_pdf("input.md", "output.pdf")结语
Markdown 转 PDF 的实现方式并不复杂,真正需要权衡的是转换质量与运行环境之间的匹配度。本文介绍的方案依托纯 Python 库完成格式流转,代码量少、集成路径短,适合对排版要求不极端、但希望将转换步骤嵌入自动化流程的场景;而当文档包含复杂表格、内联 HTML 或对字体有严格要求时,则需要先验证输出效果,或考虑引入外部渲染工具。无论选择哪种方式,建议在正式使用前用有代表性的文档样本做一次完整验证,确认页数、样式和字体均符合预期,再将其纳入实际工作流。
到此这篇关于Python实战之Markdown转PDF功能实现与页面设置的文章就介绍到这了,更多相关Python Markdown转PDF内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
