ARTICLE · 1272860
使用 Playwright 将网页转 PDF,三种方式别选错了
最近在折腾用 Playwright 把页面转成 PDF,踩了不少坑,也理清了一些东西。可能有人会问:Playwright 转 PDF,内容从哪来有讲究吗?
有,而且讲究很大。内容来源有好几种情况,选不对方式,轻则样式全丢,重则一片空白。
今天就把这三种方式捋清楚,看完你大概会有一句感慨:原来坑都在第一步。
一、整件事就三步,分歧全在中间那步
先说个反常识的:Playwright 转 PDF 的代码,从头到尾就那么三行核心。
with sync_playwright() as p:browser = p.chromium.launch() # 启动浏览器page = browser.new_page() # 开个页面page.pdf(path="out.pdf", format="A4") # 导出 PDF
看出来了吗?真正会变的,只有中间"页面内容怎么来的"这一步。
而这一步有三种写法,对应三种完全不同的机制。选错了,后面全是坑。
二、方式一:加载本地文件
HTML 已经在硬盘上了,用 file:// 直接打开:
page.goto('file:///D:/docs/input.html', wait_until='networkidle')page.pdf(path='out.pdf', format='A4')
适合:已经生成好的静态 HTML,比如测试框架吐出来的报告,想固化成 PDF 存档或发邮件。
好处是省事——不用起 HTTP 服务,也不用管网络。
但有个细节得提醒:Windows 上路径必须写成 file:///D:/...,三根斜杠加正斜杠。手写容易错,尤其路径里有中文或空格的时候。
别自己遭罪,一行搞定:
from pathlib import Pathuri = Path('D:/docs/input.html').resolve().as_uri()page.goto(uri, wait_until='networkidle')
三、方式二:直接塞 HTML 字符串
内容不是文件,而是接口返回的、或者在代码里拼出来的字符串,那就直接用 set_content:
page.set_content(html_content, wait_until='networkidle')page.pdf(path='out.pdf', format='A4')
它快。纯内存操作,不读盘不发请求,微服务里做高并发文档生成首选。
但它有个要命的坑——相对路径全废。
set_content 只是把字符串塞进当前文档,不导航。此时页面的 document.baseURI 还停在 about:blank,于是这些全都加载不出来:
<linkrel="stylesheet"href="./style.css"> <!-- 废 --><imgsrc="./images/logo.png"> <!-- 废 -->
解法是:样式全内联,图片转 base64。或者干脆注入一个 `` 告诉它该去哪找。
记住一句话:用 set_content,就得让 HTML 自己养活自己。
四、方式三:访问线上网址
想转的是线上页面,那就直接 goto:
page.goto("https://example.com/dashboard", wait_until='networkidle')page.pdf(path='out.pdf', format='A4')
它最真实。渲染结果跟你在浏览器里看到的几乎一模一样,跨域、Cookie、登录态全都正常。
适合:归档线上文档、把看板拍成 PDF 存档。
代价是它不受你控制。远端改版、反爬、登录墙,任何一个都能让结果变样。要登录的页面,得先把登录态带上:
context = browser.new_context(storage_state='state.json')page = context.new_page()
五、一张表看清楚怎么选
| 相对路径 | |||
| 外部 CSS/JS | |||
| 速度 | 最快 | ||
| 需要外网 | 是 | ||
| 可控性 | 最高 |
一句话原则:
有文件、有相对路径 →
goto file://有字符串、自包含 →set_content要抓线上 →goto https://
六、这三种方式,共通的坑
不管选哪种,下面几个坑你大概率都会撞上。
1. page.pdf() 只认 Chromium,而且必须无头
browser = p.chromium.launch() # ✅ 默认就是 headlessFirefox、WebKit 都不支持。开了有头模式,pdf() 直接报错——这是 Chromium 协议本身的限制,不是 Playwright 的问题。
2. networkidle 不等于"渲染完成"
这是个高频误解。含图表、字体的页面,资源加载完了但渲染可能还没结束,导出来就是空白页。
等一个明确的信号更靠谱:
page.wait_for_selector("#chart svg", state="visible")3. 容器里中文变方框
Docker 镜像里通常没装中文字体,导出全是豆腐块。构建时加上:
RUN apt-get install -y fonts-noto-cjk fonts-wqy-zenhei4. 忘了 print_background=True
这是最容易被漏掉的一项。不加它,页面的背景色、代码块底色都不会出现在 PDF 里——你会得到一份"漂白了"的文档。
page.pdf(path="out.pdf", format="A4", print_background=True)5. PDF 走的是打印样式
@media print 生效,@media screen 不生效。页面上那些导航栏、按钮,打印时想藏掉就这么写:
@media print { .no-print { display: none; } }七、写在最后
回头看,Playwright 转 PDF 这件事,代码是真的简单。
真正的门槛全在理解和选择上:
搞清楚
goto是导航、set_content是注入,很多诡异现象一秒就解释通了用
goto(file://)还是set_content,取决于你的内容有没有落脚点print_background、networkidle、中文字体这些坑,踩过一次就记住了
工具不难,难的是知道它什么时候会咬你。
这篇算是把三种方式的脾气都摸了一遍,希望你能少走点弯路。
完整可运行示例代码我整理好了,关注公众号后台回复「html2pdf」自取。
转 PDF 这事,真没那么难。
觉得有用的话,点个赞再走吧。















































































