WeasyPrint 技术入门指南------用 Web 技术生成 PDF 的专业库使用详解
基于官方文档 WeasyPrint 70.0(stable)撰写,涵盖命令行与 Python API 的完整使用方法、分页排版能力与输出控制细节。按需求省略安装指导。
摘要
WeasyPrint 是一个把 HTML + CSS 渲染为 PDF 的视觉渲染引擎 ,其 CSS 布局引擎用 Python 编写、专为分页设计,不依赖 WebKit 或 Gecko。它的使用方式只有两条入口:命令行 weasyprint [options] <input> <output>,以及 Python 的 HTML(...).write_pdf(...)。真正决定输出质量的,是三件事:对 @page 等分页媒体规范的运用、对 DEFAULT_OPTIONS 中各项渲染参数的掌握、以及通过 URLFetcher 控制外部资源获取。本文按此逻辑展开,并给出 v70.0 的完整能力边界。
一、定位、架构与版本策略
从技术上说,WeasyPrint 是"针对打印的 web 标准"实现:它解析 HTML 与 CSS,完成布局与分页,再输出 PDF。它的独特之处在于------它不是一个浏览器内核,而是 Kozea 用 Python 自写的布局引擎,"designed for pagination, and meant to be easy to hack on"。这带来两个直接后果:一是 SVG 会以矢量形式嵌入 PDF 而非被栅格化;二是复杂 CSS 框架可能明显拖慢渲染。
主要运行时依赖为:Python ≥ 3.10.0、Pango ≥ 1.44.0、pydyf ≥ 0.11.0、CFFI ≥ 0.6、tinyhtml5 ≥ 2.0.0b1、tinycss2 ≥ 1.5.0、cssselect2 ≥ 0.8.0、Pyphen ≥ 0.9.1、Pillow ≥ 9.1.0、fontTools ≥ 4.59.2。
关于版本,官方文档的态度非常坦率,值得使用者特别注意:
- API 稳定性:文档所记载的公共 API 会尽量保持向后兼容,"but there is no hard promise"。未在文档中记载的内部接口随时可能变更。
- 版本策略 :WeasyPrint 频繁发布大版本(类似 Firefox、Chrome 的版本号策略)。即使某个版本没有破坏 API,它也会改变文档的渲染结果------"which is what really matters at the end"。因此官方认为提供小版本号只会给开发者造成"可以无脑升级"的错觉。
- 实践含义:升级 WeasyPrint 必须回归验证既有模板的渲染结果,就像升级浏览器一样。
二、两条入口:命令行与 Python
命令行
css
weasyprint [options] <input> <output>
input 可以是 URL、文件名,或 -(标准输入);output 是文件名或 -(标准输出)。最简形式:
bash
weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf
调试时若不想创建 CSS 文件,可用 shell 的进程替换把样式直接喂给 -s:
bash
weasyprint https://weasyprint.org /tmp/weasyprint-website.pdf \
-s <(echo 'body { font-family: serif !important }')
同样的方式也常用于临时调整页面尺寸(官方推荐的调整页面尺寸方式就是 @page,而非命令行参数------WeasyPrint 不提供设置页面大小和页边距的命令行开关):
bash
weasyprint input.html output.pdf -s <(echo "@page { size: A3 landscape; margin: 3cm }")
需要批量转换时,官方建议改用 Python API 在长驻进程中完成,以避免每次重复付出启动开销。
Python 快速上手
python
from weasyprint import HTML
HTML('https://weasyprint.org/').write_pdf('/tmp/weasyprint-website.pdf')
带用户样式表:
python
from weasyprint import HTML, CSS
HTML('https://weasyprint.org/').write_pdf(
'/tmp/weasyprint-website.pdf',
stylesheets=[CSS(string='body { font-family: serif !important }')])
开发期可以配合 watchexec 在文件变更时自动重生成:
bash
watchexec --watch document.html --watch style.css \
-- python -m weasyprint document.html document.pdf
三、命令行选项全表
| 分组 | 选项 | 说明 |
|---|---|---|
| 基本信息 | -i, --info |
打印系统信息并退出 |
--version |
打印版本号并退出 | |
| 渲染 | -s, --stylesheet |
用户样式表,可多次传入 |
-a, --attachment |
附加文件,可多次传入 | |
--attachment-relationship |
附件关系,可多次传入 | |
--pdf-identifier |
PDF 文件标识符 | |
--pdf-variant |
PDF 变体(见第十节完整取值) | |
--pdf-version |
PDF 版本号 | |
--pdf-forms |
包含 PDF 表单 | |
--pdf-tags |
为可访问性生成标签 | |
--uncompressed-pdf |
不压缩 PDF 内容(调试用) | |
--xmp-metadata |
写入 XMP 元数据的文件,可多次传入 | |
--custom-metadata |
将自定义 HTML meta 写入 PDF 元数据 | |
--output-intent |
输出意图色彩空间:srgb、device-cmyk 或 CSS 标识符 |
|
-p, --presentational-hints |
遵循 HTML 表现式提示(如 <font color>、列表 type) |
|
--optimize-images |
无损优化内嵌图片体积 | |
-j, --jpeg-quality |
JPEG 质量,0(最差)到 95(最好) | |
-D, --dpi |
内嵌图片最大分辨率 | |
--full-fonts |
尽可能嵌入未修改的字体文件 | |
--hinting |
保留字体 hinting 信息 | |
-c, --cache-folder |
把图片缓存放到磁盘目录(自动创建,生成后清理) | |
| HTML | -e, --encoding |
强制输入字符编码 |
-m, --media-type |
@media 使用的媒体类型,默认 print |
|
-u, --base-url |
相对 URL 的基准,默认取输入自身的路径或当前目录 | |
| 抓取 | -t, --timeout |
HTTP 请求超时秒数 |
--allowed-protocols |
只授权逗号分隔的协议列表 | |
--no-http-redirects |
不跟随 HTTP 重定向 | |
--fail-on-http-errors |
任何 HTTP 错误即中止渲染 |
四、Python API:HTML 类
构造输入
HTML 由 tinyhtml5 解析。传入位置参数时,类会自动猜测 输入是文件名、绝对 URL 还是文件对象;为避免猜测,使用唯一一个具名参数:
python
from weasyprint import HTML
HTML('../foo.html') # 等价于 HTML(filename='../foo.html')
HTML('https://weasyprint.org') # 等价于 HTML(url='https://weasyprint.org')
HTML(sys.stdin) # 等价于 HTML(file_obj=sys.stdin)
内存中的字符串必须用具名参数 (否则 <h1>foo 会被当成文件名):
python
HTML(string='''
<h1>The title</h1>
<p>Content goes here
''')
CSS(string='@page { size: A3; margin: 1cm }')
同时指定多个输入会抛 TypeError,例如 HTML(filename='foo.html', url='localhost://bar.html')。
可选参数:encoding(强制编码)、base_url(解析 <img src="../foo.png"> 等相对 URL 的基准;HTML(string=...) 时若不提供,相对 URL 会失效)、url_fetcher、media_type(默认 'print')。
render() 与 write_pdf()
python
render(font_config=None, counter_style=None, color_profiles=None, **options) -> Document
write_pdf(target=None, zoom=1, finisher=None, font_config=None,
counter_style=None, color_profiles=None, **options) -> bytes | None
render()只做布局与分页,返回Document对象,便于访问单页、链接、书签等信息,之后再自行输出。write_pdf()是render()+Document.write_pdf()的快捷方式。target省略(或为None)时返回 PDF 的bytes;给了文件名或可写文件对象则直接写入------注意:给文件名时会静默覆盖已存在的文件。zoom:PDF 单位 / CSS 单位的缩放因子。文档明确警告------所有 CSS 单位都会受影响,包括cm这样的物理单位和A4这样的命名尺寸,取值非 1 时物理单位是"错误"的。因此不要用 zoom 来做缩放适配。finisher:一个可调用对象,接收document与pydyf.PDF两个参数,在 trailer 写入之前执行,可用于对 PDF 做后处理(例如注入自定义对象)。
五、CSS 与字体配置
CSS 由 tinycss2 解析,构造方式与 HTML 相同。CSS 对象本身没有公开属性或方法,只能配合 HTML.write_pdf() / HTML.render() 使用。
唯一需要额外注意的是:CSS 中若包含 @font-face,必须提供 FontConfiguration,且同一个文档上应用的多个 CSS 对象必须共用同一个 FontConfiguration 实例:
python
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
font_config = FontConfiguration()
html = HTML(string='<h1>The title</h1>')
css = CSS(string='''
@font-face {
font-family: Gentium;
src: url(https://example.com/fonts/Gentium.otf);
}
h1 { font-family: Gentium }''', font_config=font_config)
html.write_pdf('/tmp/example.pdf', stylesheets=[css], font_config=font_config)
样式表来源分三层:HTML5 用户代理样式表、文档内嵌或链接的作者样式表 、通过 API 传入的用户样式表 。要点是:用户样式表的层叠优先级低于作者样式表 ,除非在声明中使用 !important 提升优先级------这也是官方示例里写 font-family: serif !important 的原因。
若渲染结果缺少你预期的 HTML 表现(如 <font color>、列表 type、表格对齐属性),开启 presentational_hints=True(或 CLI 的 -p)通常能解决。
六、Document、Page 与 DocumentMetadata
Document
HTML.render() 返回的对象,核心成员:
| 成员 | 说明 |
|---|---|
pages |
Page 对象列表。同一文档中的页面尺寸不一定相同 |
metadata |
DocumentMetadata 对象 |
fonts |
文档用到的字体字典 |
url_fetcher |
该文档使用的 URLFetcher |
copy(pages='all') |
取页面子集,返回新 Document |
make_bookmark_tree(scale=1, transform_pages=False) |
生成书签树 |
write_pdf(...) |
输出 PDF |
两个官方给出的典型用法------拆分奇偶页、合并多文档:
python
# 注意:Python 列表从 0 计数,页码从 1 计数;[::2] 取到的是奇数页
document.copy(document.pages[::2]).write_pdf('odd_pages.pdf')
document.copy(document.pages[1::2]).write_pdf('even_pages.pdf')
# 合并多个文档,沿用第一个文档的元数据
all_pages = [p for doc in documents for p in doc.pages]
documents[0].copy(all_pages).write_pdf('combined.pdf')
打印目录(官方示例基于 CSS 2.1 规范文档,输出形如 1. Introduction to CSS 2.1 (page 2)):
python
def print_outline(bookmarks, indent=0):
for i, bookmark in enumerate(bookmarks, 1):
page = bookmark.destination[0]
print('%s%d. %s (page %d)' % (
' ' * indent, i, bookmark.label.lstrip('0123456789. '), page))
print_outline(bookmark.children, indent + 2)
print_outline(document.make_bookmark_tree())
注:API 参考中 make_bookmark_tree() 被描述为返回 (label, target, children, state) 形式的子树列表,而上述官方示例以属性方式访问(bookmark.label / bookmark.destination[0] / bookmark.children)。使用时请以实际版本行为为准。
Page
| 属性 | 内容 |
|---|---|
width / height |
页面宽高(含页边距),单位 CSS 像素 |
bleed |
出血宽度字典,键为 top/right/bottom/left |
anchors |
锚点名 → (x, y)(相对页面左上角,CSS 像素) |
links |
(link_type, target, rectangle, box) 列表;link_type 为 'external'(绝对 URL)、'internal'(锚点名,可能在其他页,多处定义时取首次出现)、'attachment'(指向附件的绝对 URL) |
bookmarks |
(level, label, target, state) 列表,来自同名 CSS 属性 |
forms |
表单元素 → [(element, attributes, rectangle), ...];键为 None 时存放不属于任何 form 的 input |
rectangle 均为 (x, y, width, height),坐标原点为页面左上角。
DocumentMetadata 与 HTML 的映射
| 元数据字段 | HTML 来源 | 写入 PDF 的位置 |
|---|---|---|
title |
<title> |
/Title |
authors |
<meta name=author>(可多个) |
/Author |
generator |
<meta name=generator> |
/Creator |
keywords |
<meta name=keywords> |
/Keywords |
description |
<meta name=description> |
/Subject |
created |
<meta name=dcterms.created> |
/CreationDate |
modified |
<meta name=dcterms.modified> |
/ModDate |
lang |
<html lang=...>(BCP 47) |
--- |
attachments |
<link rel=attachment> |
/EmbeddedFiles |
custom |
其他自定义 meta(需开启 custom_metadata) |
PDF info 字典 |
xmp_metadata |
通过 API/CLI 提供的 XML 字节串列表 | XMP |
日期遵循 W3C 定义的 ISO 8601 六种格式之一。自定义字段默认不写入 PDF,需用 --custom-metadata 或 custom_metadata=True 开启:
python
HTML(string='<meta name="recipe" content="fries">').write_pdf(
'recipe.pdf', custom_metadata=True)
七、渲染选项:DEFAULT_OPTIONS
weasyprint.DEFAULT_OPTIONS 的键值即 Python API 全部可用选项(与命令行一一对应):
attachment_relationships attachments cache custom_metadata dpi full_fonts
hinting jpeg_quality optimize_images output_intent pdf_forms pdf_identifier
pdf_tags pdf_variant pdf_version presentational_hints stylesheets
uncompressed_pdf xmp_metadata
要点补充:
stylesheets:可以是CSS对象、文件名、URL 或类文件对象的混合列表。attachments:元素可以是Attachment对象、文件名、URL 或类文件对象。cache:传dict表示内存缓存,传路径字符串(str/Path)表示磁盘缓存目录。pdf_forms/pdf_tags/uncompressed_pdf/custom_metadata/presentational_hints/optimize_images/full_fonts/hinting为布尔型。jpeg_quality(0--95)、dpi(最大分辨率)为数值型;pdf_identifier为bytes。
八、URL Fetcher:控制外部资源
WeasyPrint 读取外部资源(图片、样式表)一律经由 URL fetcher。默认 fetcher 支持普通文件、HTTP、FTP 与 data URI,会跟随 HTTP 重定向,但不支持 cookie 与身份认证 等高级特性------这些要靠自定义 fetcher 解决。默认 HTTP/HTTPS/FTP 超时为 10 秒(对 file:// 无效)。
python
class URLFetcher(timeout=10, ssl_context=None, http_headers=None,
allowed_protocols=None, allow_redirects=True, fail_on_errors=False)
自定义 fetcher 继承该类并实现兼容签名的 fetch(url, headers=None),返回 URLFetcherResponse。典型模式:处理自定义协议,其余交给父类:
python
from weasyprint import HTML
from weasyprint.urls import URLFetcher, URLFetcherResponse
class MyFetcher(URLFetcher):
def fetch(self, url, headers=None):
if url.startswith('graph:'):
graph_data = [float(value) for value in url[6:].split(',')]
string = generate_graph(graph_data)
return URLFetcherResponse(url, string, {'Content-Type': 'image/png'})
return super().fetch(url, headers)
source = '<img src="graph:42,10.3,87">'
HTML(string=source, url_fetcher=MyFetcher()).write_pdf('out.pdf')
默认所有 fetcher 异常都会被 WeasyPrint 捕获并转为警告。若希望某些错误是致命的(例如样式表加载失败),抛出 FatalURLFetchingError:
python
class MyFetcher(URLFetcher):
def fetch(self, url, headers=None):
try:
return super().fetch(url, headers)
except Exception as exception:
if url.endswith('.css'):
raise FatalURLFetchingError(f'Problem with stylesheet at {url}') from exception
raise exception
只调参数时无需子类化:HTML(string='<html>', url_fetcher=URLFetcher(timeout=20)).write_pdf('out.pdf')。
Web 框架集成(Flask-WeasyPrint、Django-Weasyprint)正是靠自定义 fetcher 实现的:它们让静态与媒体文件走文件系统而非网络请求,同时解决服务端渲染时的权限/cookie 问题。
九、分页排版能力(WeasyPrint 的强项)
@page 与页面选择
支持 CSS Paged Media Module Level 3 的全部特性:@page 规则与 :left、:right、:first、:blank 选择器;页边距盒(margin boxes);基于页面的计数器(有已知限制,issue #93);size、bleed、marks 属性;命名页(named pages)。
css
@page {
size: A3 landscape; /* 默认是 A4 portrait */
margin: 3cm;
}
GCPM 的页面选择器也可用:@page:nth(3)(第三页)、@page:nth(2n+1)(奇数页)、@page:nth(1 of chapter)(各章首页)。
页眉页脚与计数器
页边距盒 + 计数器是标准做法;string-set 与 content: string() 可把"当前章节标题"带到页眉(named strings):
css
@top-center { content: string(chapter) }
h2 { string-set: chapter "Current chapter: " content() }
目录、交叉引用与 leader
css
/* 交叉引用:页码与文字 */
a::after { content: ", on page " target-counter(attr(href), page) }
a::after { content: ", see " target-text(attr(href)) }
/* 目录条目:点线引导 + 页码 */
li a::after {
content: ' ' leader(dotted) ' ' target-counter(attr(href), page);
}
脚注
支持 float: footnote 把盒子放入脚注区,::footnote-marker 与 ::footnote-call 定义脚注标记与正文中的脚注调用,footnote-display(compact 不支持)与 footnote-policy 控制显示与困难页面的处理。
PDF 书签(大纲)
用 bookmark-level、bookmark-label、bookmark-state 控制。用户代理样式表已默认为 <h1>--<h6> 生成书签;若只有一个顶层 <h1> 且不想让它出现在书签里:
css
h1 { bookmark-level: none }
分页控制
支持 break-before / break-after / break-inside(对页面有效,对多栏与区域无效 )及 CSS2 的 page-break-* 别名;支持 orphans、widows、box-decoration-break(背景总是重复,不按 slice 语义延展)、margin-break。
其他排版相关
- 断词:
hyphens、hyphenate-character、hyphenate-limit-chars、hyphenate-limit-zone。要自动断词需同时 设置hyphens: auto且lang属性为 Pyphen 支持的语言。 - 多栏:支持
column-width/column-count/columns、栏间距与栏线、column-span(仅直接子元素)、column-fill;但受限高度、跨栏与分栏断点不支持,分页与溢出未充分测试。 - 运行元素(running elements):可用
element()把 HTML 盒子放进页边距(不支持start参数)。
十、输出变体与合规格式
--pdf-variant / pdf_variant 的全部取值(v70.0):
bash
pdf/a-1b pdf/a-2b pdf/a-3b pdf/a-2u pdf/a-3u pdf/a-4u
pdf/a-1a pdf/a-2a pdf/a-3a pdf/a-4e pdf/a-4f
pdf/ua-1 pdf/ua-2
pdf/x-1a pdf/x-3 pdf/x-4 pdf/x-5g
debug
python
HTML(string="<p>document</p>").write_pdf("document.pdf", pdf_variant="pdf/a-3u")
重要 :WeasyPrint 只是尽力生成合规文档,不保证结果一定合规------所用的 HTML/CSS/PDF 特性必须遵守相应规范的约束,验证责任在使用者。
PDF/A(归档)
PDF/A 是面向归档的 PDF 子集:无音频/视频/JavaScript、色彩空间受限、必须嵌入字体等。官方建议优先使用 PDF/A-3u:它允许 A-1 禁止的透明层,也允许 A-2 禁止的任意附件格式;后缀 "u" 表示文本以 Unicode 提供。
- 文档含图片时,必须设置
image-rendering: crisp-edges,因为抗锯齿在 PDF/A 中被禁止。 pdf_identifier用于标识某个 PDF 是另一 PDF 的新版本,默认会自动生成合法值,可用--pdf-identifier/pdf_identifier覆盖。
PDF/UA(无障碍)
生成带结构与文档信息的额外元数据。要得到有效的 PDF/UA,主要约束是HTML 结构必须正确 ,因为 HTML 顺序会被用作 PDF 内容顺序。HTML 中必须提供 <title> 标签和 <html lang="..."> 属性。
PDF/X(图形交换)
面向印刷交换,要求色彩配置文件。做法是全篇使用设备相关 CMYK:
css
body { color: device-cmyk(0% 10% 0% 80%) }
@color-profile device-cmyk {
components: cyan, magenta, yellow, black;
src: url(path/to/cmyk-profile.icc);
}
官方建议优先使用 PDF/X-4(允许前序版本禁止的透明层)。
Factur-X / ZUGFeRD(电子发票)
法德混合电子发票标准,是欧洲语义标准 EN 16931 的首个实现,基于 PDF/A-3b。需要用户提供两份元数据文件:RDF 元数据(含文档元数据与 PDF/A 扩展信息)和 Factur-X/ZUGFeRD 元数据(含发票金额与买卖双方信息)。
命令行:
bash
weasyprint invoice.html invoice.pdf \
--attachment=factur-x.xml --attachment-relationship=Data \
--xmp-metadata=rdf.xml --pdf-variant=pdf/a-3a
Python API:
python
from weasyprint import Attachment, HTML
document = HTML("invoice.html").render()
factur_x_xml = Path("factur-x.xml").read_text()
attachment = Attachment(string=factur_x_xml, name="factur-x.xml", relationship="Data")
document.metadata.attachments = [attachment]
xmp_metadata = Path("rdf.xml").read_text().encode()
document.metadata.xmp_metadata = [xmp_metadata]
document.write_pdf("invoice.pdf", pdf_variant="pdf/a-3b")
(注:官方文档的 CLI 示例使用 pdf/a-3a,Python 示例使用 pdf/a-3b;Factur-X 规范本身基于 PDF/A-3b。)
PDF 表单
默认表单字段会被渲染成纯文本与图形。要生成可在阅读器中填写、甚至能提交数据的真实 PDF 表单,用 --pdf-forms 或 pdf_forms=True:
python
HTML(string="<input value='test'>").write_pdf("test.pdf", pdf_forms=True)
也可以只对特定字段生效:在这些元素上设置 appearance: auto(支持文本输入、复选框、文本域与下拉选择),此时需自行覆盖用户代理样式表的默认样式:
html
<style>
label { display: block }
.pdf-form { appearance: auto }
.pdf-form::before { visibility: hidden }
</style>
<label>Can't be modified in PDF <input value="static"></label>
<label>Can be modified in PDF <input class="pdf-form" value="dynamic"></label>
表单支持度高度依赖 PDF 阅读器,遇到问题时先确认阅读器是否支持该特性再报 bug。
十一、附件与元数据的三种写法
附件(embedded files)有三种添加方式:
- 链接式:
<a rel="attachment" href="note.txt">view attached note</a>(点击可保存) - 全局式:
<link rel="attachment" href="note.txt">(放入 head,写进/EmbeddedFiles) - 通过选项:
--attachment note.txt --attachment photo.jpg(CLI),或 Python 传attachments=[Attachment("note.txt"), Attachment("photo.jpg")]
Attachment 支持 name、description、created、modified、relationship(默认 Unspecified,其他常见值见 ISO-32000-2:2020 §7.11.3)等参数;构造方式与 HTML 相同,但不支持 encoding 与 media_type。<a>/<link> 上的 title 属性会被用作附件描述。
十二、图片、字体与性能
图片
python
# 原始高质量图片:更快,但 PDF 更大
HTML('https://weasyprint.org/').write_pdf('weasyprint.pdf')
# 优化后的较低质量图片:稍慢,但 PDF 更小
HTML('https://weasyprint.org/').write_pdf(
'weasyprint.pdf', optimize_images=True, jpeg_quality=60, dpi=150)
optimize_images=True:无损减小内嵌图片体积,渲染时间略增。jpeg_quality:0--95,越低越小。dpi:限制栅格图片分辨率(每英寸最大像素数)。cache:默认按文档缓存;可跨文档共享以节省网络与 CPU:
python
cache = {}
for i in range(10):
HTML(f'https://weasyprint.org/').write_pdf(f'example-{i}.pdf', cache=cache)
也可把 cache 设为路径字符串(或 CLI -c/--cache-folder)以使用磁盘缓存。
字体
字体由 Pango 经 Fontconfig 查找(Windows、macOS 上同样走 Fontconfig),可用 fc-list 查看、fc-match 查看匹配结果;把字体文件复制到 ~/.local/share/fonts 通常即可安装。字体会自动嵌入 PDF 并默认子集化 (只保留用到的字形),子集化优先用 hb-subset,不可用时回退到较慢的 fontTools。需要完整字体可用 --full-fonts,需保留 hinting 用 --hinting。
缺失字形时显示 .notdef 并在日志告警,其宽度(advance)可能不准,但其余字形布局正确,文本搜索与选择不受影响。若 PDF 中一个字都画不出来或全是方块,说明需要安装字体或用 @font-face 显式指定。
性能建议
官方明确说明"优化不是 WeasyPrint 的主要目标",并给出三条实用建议:
- 避免大型 CSS 框架:大量 CSS 属性 × 大量 HTML 标签会让层叠阶段极其耗时。
- 慎用表格,尤其是跨多页的表格;能用普通块布局替代会快很多。
- 图片与字体的优化会减小 PDF 体积但增加渲染时间;用图片缓存可让同一图片只解析优化一次。
日志
多数错误(不支持的 CSS 属性、缺失图片等)不致命,只会记录日志。终端中默认输出到 stderr;作为库使用时默认完全不显示 ,需自行配置 weasyprint logger:
python
import logging
logger = logging.getLogger('weasyprint')
logger.setLevel(logging.WARNING) # 显示 warning 及以上
logger.addHandler(logging.FileHandler('weasyprint.log'))
logger.addHandler(logging.StreamHandler())
另有 weasyprint.progress logger 用于报告渲染进度(CLI 的 --verbose/--debug 即用它),适合把进度反馈给终端用户。
十三、能力边界速查
明确不支持或受限(易踩坑):
| 类别 | 不支持 / 受限 |
|---|---|
| 文本 | RTL 与双向文本(bidi)、writing-mode、line-break、hanging-punctuation、text-shadow、text-emphasis-*、text-underline-position |
| 选择器 | :hover/:active/:focus/:target/:visited 合法但永不匹配;不支持 :dir、输入伪类、列选择器 |
| 变换 | 仅 2D 变换;不支持 3D 变换、perspective、transform-style、backface-visibility |
| 尺寸 | 不支持 min-content/max-content/fit-content();unset 关键字不支持 |
| 布局 | Grid 仅适用于简单场景:inline-grid、grid-auto-flow: column、subgrid、repeat(auto-fill/auto-fit)、基线对齐、网格项的 min/max 宽高等不支持;Flexbox 仅简单用例且未深度测试 |
| 多栏 | 不支持受限高度、跨栏、分栏断点 |
| 表格 | visibility: collapse、表格相关盒子的 min/max 高度 |
| 颜色 | 不支持 color-mix()、contrast-color();支持 light-dark()、device-cmyk()、@color-profile |
| 其他 | image() 记法(背景图)、@font-feature-values、quotes(content: *-quote)、box-shadow 模糊(用渐变近似,PDF 阅读器可能渲染不佳,建议避免) |
支持良好 :CSS 2.1(自 0.11 起通过 Acid2 测试)、Selectors 3/4、CSS Text 3、CSS Fonts 3、@font-face、CSS Paged Media 3 全部特性、GCPM(页面选择器、运行元素、脚注)、CSS Generated Content 3(named strings、交叉引用、书签、leader)、CSS Color 4、Backgrounds & Borders 3(含 border-image、border-radius)、Transforms 1(2D)、CSS Variables、Fragmentation(页面)、CSS Logical Properties、Grid/Flexbox 基础用法、CSS Box Sizing 的 box-sizing、多栏基础用法。
单位方面,除常规单位外还支持页面相对单位(pvw、pvh 等,相对含页边距的整页 尺寸;其他单位相对页面区域尺寸);所有数学函数(calc() 等)均支持;attr() 可用于 content 与 string-set。
十四、服务端使用的安全注意
若用不可信的 HTML/CSS 渲染,官方列出七类风险,并给出通用缓解建议:
- 超长渲染 / 死循环 / 超大数值 :即使是小文档也可能造成极高 CPU 与内存占用。需限制进程时间与内存(uWSGI 的
harakiri、evil-reload-on-as,Linuxulimit),并截断与清洗输入。 - 无限请求 :HTTP(S)/FTP 默认 10 秒超时,但
file://不受超时约束(/dev/urandom这类无限文件会导致无限渲染)。 - 本地文件访问 :
file://URI 可被用来探测服务器文件系统并把文件嵌入 PDF。应使用沙箱,并用自定义 URL fetcher 禁止file://或按路径过滤。 - 附件泄露:默认情况下,进程能访问的所有文件都可能被嵌入 PDF;部分阅读器允许执行附件(如 shell 脚本)。
- 系统信息泄露 :本地已安装字体、网络配置(IPv4/IPv6、地址、防火墙,通过
https://URI 与渲染耗时推断)、Python/Pango 等库版本都可能被探测。 - SVG:渲染 SVG 存在同样风险,且走同一个 URL fetcher。
通用原则:不要以 root 运行,用低权限用户与容器限制文件系统、网络与内存访问。
十五、实践要点清单
- 页面尺寸与页边距只用
@page控制 ,命令行没有对应开关;zoom会破坏物理单位,不要用。 - 用户样式表优先级低于作者样式表 ,需要覆盖时加
!important。 HTML(string=...)时务必设base_url,否则相对 URL 失效。- 有
@font-face就必须创建并在多个CSS间共享同一个FontConfiguration。 - 升级版本后必须回归验证渲染结果------每个版本都可能改变渲染,尽管 API 未变。
- 需要 cookie/鉴权、或要禁止本地文件访问时,写自定义
URLFetcher;关键资源失败时抛FatalURLFetchingError。 - 生成合规 PDF(A/UA/X、Factur-X)后自行验证合规性,WeasyPrint 不作保证。
- 库模式下日志默认不输出,排障前先配置
weasyprintlogger。 - 追求性能时先做两件事:去掉大型 CSS 框架、把跨页表格改成块布局。
- 批量生成时复用长驻进程 + 共享
cache,避免重复解析与下载图片。
参考资料
- WeasyPrint 官方文档首页(v70.0 stable)
- First Steps(命令行、Python 库用法、URL Fetchers、安全)
- API Reference(命令行 API、Python API、支持特性)
- Common Use Cases(尺寸调整、PDF/A-UA-X、表单、元数据、附件、缓存与优化、日志)
- Changelog
- WeasyPrint 项目仓库(源码、示例、问题追踪)
- HTML5 用户代理样式表 html5_ua.css
- 表单样式表 html5_ua_form.css(
--pdf-forms所用) - pydyf API 参考(
finisher参数所用 PDF 对象) - Flask-WeasyPrint · Django-Weasyprint
- WeasyPerf(各版本时间与内存对比)
- WeasyPrint 示例样张