PDF选型WeasyPrintPlaywright
HTML 转 PDF 选型:wkhtmltopdf vs WeasyPrint
wkhtmltopdf 的内核停在 2016 年并已归档,新项目别再用。静态内容选 WeasyPrint(CSS Paged Media 做页码页眉页脚),要跑 JS 选 Playwright;附中日文字体的坑和 SSRF 提醒。
一句话结论
两者解决的根本不是同一个问题:
- wkhtmltopdf 是「网页截图器」——用浏览器内核把页面渲染出来,再切成 PDF。所见即所得,能跑 JS。
- WeasyPrint 是「排版引擎」——从一开始就为印刷设计,理解页码、页眉页脚、分页规则。不跑 JS。
2026 年的现实
wkhtmltopdf 已经死了,不该用于新项目。见下方「项目现状」。 新项目在 WeasyPrint(静态内容)和 Playwright / Puppeteer(需要 JS)之间选。
它们各自是什么
wkhtmltopdf
一个 C++ 编译的独立二进制,内嵌 Qt WebKit 浏览器内核。工作方式跟浏览器的「打印成 PDF」几乎一样:加载 HTML → 执行 JS → 渲染成位图化的页面 → 输出 PDF。
因为是完整浏览器内核,页面长什么样,PDF 就长什么样。代价是它对「印刷」这件事没有概念——分页只是把长图切断。
WeasyPrint
一个 Python 库,不含浏览器内核。它自己实现了 W3C 的渲染与排版规范,重点是 CSS Paged Media(@page、counter(page)、break-before 这些印刷专用的 CSS)。
它把 HTML+CSS 当作排版描述而非网页,所以能做到「第 3 页页脚显示章节名」「表格跨页时重复表头」这类事。代价是它不是浏览器,不执行 JavaScript。
项目现状(2026-08 核实)
| wkhtmltopdf | WeasyPrint | |
|---|---|---|
| 状态 | ⛔ 已归档,只读 | ✅ 活跃维护 |
| 关键时间 | 仓库 2023-01-02 归档;GitHub 组织 2024-07-10 归档 | v68 发布于 2026-01-19 |
| 最后版本 | 0.12.6(2020-06-10) | v68 / v69 系 |
| 维护方 | 无 | CourtBouillon |
wkhtmltopdf 为什么会死:它依赖的 QtWebKit 早在 Qt 5.6(2016)就被 Qt 官方弃用,改推 QtWebEngine。此后每个 Qt 官方版本都不再附带 QtWebKit——wkhtmltopdf 的地基被抽走了,维护成本高到无法承担。
用 wkhtmltopdf 的实际风险
内核停留在 2016 年前的 WebKit,往后只会越来越糟:
- 现代 CSS 大量不支持(Grid、Flexbox 的新特性、CSS 变量等)
- TLS/加密套件过时 —— 抓取 https 资源可能直接失败
- 安全漏洞不会再修 —— 如果它处理用户提交的 HTML,这是实打实的攻击面
核心对比
| 特性 | wkhtmltopdf | WeasyPrint |
|---|---|---|
| 核心引擎 | Qt WebKit 浏览器内核 | 自实现的 W3C 渲染/排版引擎 |
| 语言 / 生态 | C++,独立二进制 | Python 库,易嵌入 Python 工程 |
| JavaScript | ✅ 完整支持 | ❌ 基本不支持 |
| CSS Paged Media | ❌ 支持很弱 | ✅ 原生支持 |
| 页眉页脚 | 靠 --header-html 传独立 HTML 文件 |
CSS @page 里直接写 |
| 页码 | 需命令行参数配合 | counter(page) |
| 分页控制 | 弱 | break-before / break-inside 等 |
| 部署 | 装二进制,容器里体积大 | pip 安装,但依赖系统字体库 |
怎么选
需要执行 JavaScript 才能出内容?
(ECharts / Highcharts 前端渲染、Ajax 拉数据…)
│
├─ 是 ──→ Playwright 或 Puppeteer
│ (不要用 wkhtmltopdf —— 同样是浏览器内核,
│ 但它的内核停在 2016 年)
│
└─ 否 ──→ 内容是静态 HTML
│
├─ 需要印刷级排版?──→ WeasyPrint
│ (发票、合同、报告、书籍:页码、页眉页脚、
│ 精确分页、双面打印边距)
│
└─ 只是简单转换 ──→ WeasyPrint 也够用
几乎不存在「必须选 wkhtmltopdf」的场景。它唯一的优势(跑 JS)已经被 Playwright/Puppeteer 完全覆盖,而且后者用的是现代 Chromium。
只有一种情况会留着它:遗留系统已经在用,迁移成本高于风险。这种情况下至少要确保它不处理不可信的 HTML 输入。
实操
WeasyPrint
pip install weasyprint
macOS 上还需要系统库(渲染文字要用到):
brew install pango libffi
命令行:
weasyprint input.html output.pdf
Python:
from weasyprint import HTML, CSS
HTML("invoice.html").write_pdf(
"invoice.pdf",
stylesheets=[CSS("print.css")],
)
# 字符串 / URL からも
HTML(string="<h1>見積書</h1>").write_pdf("out.pdf")
HTML(url="https://example.com").write_pdf("out.pdf")
印刷排版的核心是 CSS,不是代码:
@page {
size: A4;
margin: 20mm 15mm;
@top-center { content: "見積書"; }
@bottom-right { content: counter(page) " / " counter(pages); }
}
/* 章ごとに改ページ */
h1 { break-before: page; }
/* 表が途中で切れないように */
tr { break-inside: avoid; }
/* 表が跨页したらヘッダを繰り返す */
thead { display: table-header-group; }
wkhtmltopdf(仅供维护遗留系统参考)
wkhtmltopdf --header-html header.html --footer-html footer.html \
--margin-top 25mm page.html out.pdf
页眉页脚必须是独立的 HTML 文件,页码要靠它注入的 JS 变量([page] / [topage])取——这就是「维护成本高」的具体体现。
⚠️ 中日文字体(这个坑最常见)
两者都不自带字体,靠系统字体渲染。Docker 容器里默认没有 CJK 字体,结果就是中日文全变成豆腐块 □□□。
容器里要装字体:
RUN apt-get update && apt-get install -y \
fonts-noto-cjk fonts-noto-color-emoji \
&& rm -rf /var/lib/apt/lists/*
CSS 里指定实际装了的字体,别只写 sans-serif:
body {
font-family: "Noto Sans CJK JP", "Noto Sans CJK SC", sans-serif;
}
WeasyPrint 走 Pango/fontconfig,装好字体后一般就正常。日文排版要注意 font-family 的顺序——CJK JP 和 CJK SC 对同一个汉字的字形取法不同(例如「直」「骨」),混用会出现字形不一致。
现代替代方案
| 方案 | 适用 | 说明 |
|---|---|---|
| Playwright | 需要 JS | 现代 Chromium,page.pdf() 直接出 PDF。多语言 SDK(Python/Node/Java/.NET) |
| Puppeteer | 需要 JS,Node 生态 | Chrome DevTools Protocol,Node 优先 |
| WeasyPrint | 静态内容 + 印刷排版 | 本文推荐 |
Playwright 生成 PDF:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page()
pg.goto("https://example.com", wait_until="networkidle")
pg.pdf(path="out.pdf", format="A4",
margin={"top": "20mm", "bottom": "20mm"})
b.close()
注意 wait_until="networkidle" —— 动态图表要等渲染完再截,否则出空白。这是 JS 路线的通病。
🔒 安全提醒
WeasyPrint v68 修复了一个 SSRF 漏洞(服务端请求伪造),与自定义 URL fetcher 和允许的协议限制有关。
如果你的系统满足以下任一条:
- 使用了自定义 URL fetcher
- 限制了允许的协议
- 处理用户提交的 HTML
务必升级到 v68 以上。用户提交的 HTML 里如果带 <img src="http://内网地址/">,旧版可能被利用来探测内网。
参考
- WeasyPrint 官网:https://weasyprint.org/
- WeasyPrint 文档:https://doc.courtbouillon.org/weasyprint/stable/
- CourtBouillon(维护方):https://www.courtbouillon.org/
- wkhtmltopdf(已归档):https://github.com/wkhtmltopdf/wkhtmltopdf
- CSS Paged Media 规范:https://www.w3.org/TR/css-page-3/
- Playwright:https://playwright.dev/
- Puppeteer:https://pptr.dev/