WeasyPrint 技术入门指南——用 Web 技术生成 PDF 的专业库使用详解

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 输出意图色彩空间:srgbdevice-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_fetchermedia_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:一个可调用对象,接收 documentpydyf.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)通常能解决。

六、DocumentPageDocumentMetadata

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-metadatacustom_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_identifierbytes

八、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-WeasyPrintDjango-Weasyprint)正是靠自定义 fetcher 实现的:它们让静态与媒体文件走文件系统而非网络请求,同时解决服务端渲染时的权限/cookie 问题。

九、分页排版能力(WeasyPrint 的强项)

@page 与页面选择

支持 CSS Paged Media Module Level 3 的全部特性:@page 规则与 :left:right:first:blank 选择器;页边距盒(margin boxes);基于页面的计数器(有已知限制,issue #93);sizebleedmarks 属性;命名页(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-setcontent: 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-displaycompact 不支持)与 footnote-policy 控制显示与困难页面的处理。

PDF 书签(大纲)

bookmark-levelbookmark-labelbookmark-state 控制。用户代理样式表已默认为 <h1>--<h6> 生成书签;若只有一个顶层 <h1> 且不想让它出现在书签里:

css 复制代码
h1 { bookmark-level: none }

分页控制

支持 break-before / break-after / break-inside(对页面有效,对多栏与区域无效 )及 CSS2 的 page-break-* 别名;支持 orphanswidowsbox-decoration-break(背景总是重复,不按 slice 语义延展)、margin-break

其他排版相关

  • 断词:hyphenshyphenate-characterhyphenate-limit-charshyphenate-limit-zone。要自动断词需同时 设置 hyphens: autolang 属性为 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-formspdf_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)有三种添加方式:

  1. 链接式:<a rel="attachment" href="note.txt">view attached note</a>(点击可保存)
  2. 全局式:<link rel="attachment" href="note.txt">(放入 head,写进 /EmbeddedFiles
  3. 通过选项:--attachment note.txt --attachment photo.jpg(CLI),或 Python 传 attachments=[Attachment("note.txt"), Attachment("photo.jpg")]

Attachment 支持 namedescriptioncreatedmodifiedrelationship(默认 Unspecified,其他常见值见 ISO-32000-2:2020 §7.11.3)等参数;构造方式与 HTML 相同,但不支持 encodingmedia_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-modeline-breakhanging-punctuationtext-shadowtext-emphasis-*text-underline-position
选择器 :hover/:active/:focus/:target/:visited 合法但永不匹配;不支持 :dir、输入伪类、列选择器
变换 仅 2D 变换;不支持 3D 变换、perspectivetransform-stylebackface-visibility
尺寸 不支持 min-content/max-content/fit-content()unset 关键字不支持
布局 Grid 仅适用于简单场景:inline-gridgrid-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、多栏基础用法。

单位方面,除常规单位外还支持页面相对单位(pvwpvh 等,相对含页边距的整页 尺寸;其他单位相对页面区域尺寸);所有数学函数(calc() 等)均支持;attr() 可用于 contentstring-set

十四、服务端使用的安全注意

若用不可信的 HTML/CSS 渲染,官方列出七类风险,并给出通用缓解建议:

  • 超长渲染 / 死循环 / 超大数值 :即使是小文档也可能造成极高 CPU 与内存占用。需限制进程时间与内存(uWSGI 的 harakirievil-reload-on-as,Linux ulimit),并截断与清洗输入。
  • 无限请求 :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 运行,用低权限用户与容器限制文件系统、网络与内存访问。

十五、实践要点清单

  1. 页面尺寸与页边距只用 @page 控制 ,命令行没有对应开关;zoom 会破坏物理单位,不要用。
  2. 用户样式表优先级低于作者样式表 ,需要覆盖时加 !important
  3. HTML(string=...)务必设 base_url,否则相对 URL 失效。
  4. @font-face 就必须创建并在多个 CSS 间共享同一个 FontConfiguration
  5. 升级版本后必须回归验证渲染结果------每个版本都可能改变渲染,尽管 API 未变。
  6. 需要 cookie/鉴权、或要禁止本地文件访问时,写自定义 URLFetcher;关键资源失败时抛 FatalURLFetchingError
  7. 生成合规 PDF(A/UA/X、Factur-X)后自行验证合规性,WeasyPrint 不作保证。
  8. 库模式下日志默认不输出,排障前先配置 weasyprint logger。
  9. 追求性能时先做两件事:去掉大型 CSS 框架、把跨页表格改成块布局。
  10. 批量生成时复用长驻进程 + 共享 cache,避免重复解析与下载图片。

参考资料

  1. WeasyPrint 官方文档首页(v70.0 stable)
  2. First Steps(命令行、Python 库用法、URL Fetchers、安全)
  3. API Reference(命令行 API、Python API、支持特性)
  4. Common Use Cases(尺寸调整、PDF/A-UA-X、表单、元数据、附件、缓存与优化、日志)
  5. Changelog
  6. WeasyPrint 项目仓库(源码、示例、问题追踪)
  7. HTML5 用户代理样式表 html5_ua.css
  8. 表单样式表 html5_ua_form.css(--pdf-forms 所用)
  9. pydyf API 参考(finisher 参数所用 PDF 对象)
  10. Flask-WeasyPrint · Django-Weasyprint
  11. WeasyPerf(各版本时间与内存对比)
  12. WeasyPrint 示例样张
相关推荐
志尊宝1 天前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
三乐2281 天前
一文搞懂 JS 事件流:捕获、冒泡、事件委托
javascript·html
IMPYLH1 天前
HTML 的 <slot> 元素
前端·网络·html
乐迪绘防伪油墨技术分享1 天前
特种功能油墨选型指南:温变 / 遇水 / 荧光 / UV LED 技术对比与供应商参考
前端·html·uv
阿虎儿1 天前
我的 HTML 样板(HTML Boilerplate)
前端·html
志尊宝1 天前
Vue3 零基础每日笔记(043):defineModel 与 defineExpose 的 TS 用法——3.4+ 新 API 进阶
前端·javascript·vue·html·vue3
IMPYLH1 天前
HTML 的 <small> 元素
前端·网络·html
AnalogElectronic2 天前
家常菜抽奖转盘.html
html
志尊宝2 天前
Vue3 零基础每日笔记(035):什么是 Composables——mixin 之死与逻辑复用新方案
笔记·vue·html·前端开发·软件开发