⌨ 技术

HTML简历Playwright转PDF

如何用 HTML + Playwright 构建精美简历并转换为 PDF

本文记录了从 Markdown 简历转换为 PDF 的探索过程,最终采用 HTML 模板 + Playwright 渲染的方案,实现了高度保真的简历输出。

目录


背景与痛点

最初有一份 Markdown 格式的简历,直接用 Markdown 转 PDF 工具(如 pandoc、md2pdf)转换后,样式往往会出现以下问题:

  • 字体、间距与预期不符
  • 代码块、表格渲染混乱
  • 无法精细控制分页位置
  • 生成的 PDF 在不同设备上显示不一致

核心诉求:希望拥有一套可以所见即所得的简历系统,编辑方便,输出专业,且能一键生成 PDF。


方案选型

方案一:Markdown → PDF(放弃)

工具 问题
WeasyPrint 对现代 CSS 支持有限,布局经常错乱
Pandoc + PDF 需要 LaTeX 环境,配置复杂
md2pdf 样式单一,定制性差

方案二:HTML 模板 + Playwright(采用)

利用 Playwright 控制真实浏览器进行渲染,可以完美支持:

  • 现代 CSS3 特性(Flexbox、Grid、动画)
  • 自定义字体(Google Fonts)
  • 外部图片资源
  • 任意 PDF 尺寸与边距设置

优势: 1. 浏览器渲染引擎保证视觉效果与编辑器完全一致 2. 支持 print_background=True 保留背景色 3. 可通过 JavaScript 动态获取页面高度,实现自适应 PDF 尺寸 4. 支持等待网络字体加载,避免字体替换导致的布局抖动


技术实现

环境准备

# 安装 Playwright
pip install playwright

# 安装 Chromium 浏览器
python -m playwright install chromium

# 国内镜像(可选)
pip install playwright -i https://pypi.tuna.tsinghua.edu.cn/simple
python -m playwright install chromium

项目结构

resume/
├── resume.html          # 简历 HTML 模板
├── photo.jpg            # 简历照片
├── html_to_pdf.py       # PDF 转换脚本
└── resume.pdf           # 生成的 PDF 文件

HTML 模板设计

简历模板采用模块化结构,使用 CSS 变量统一管理配色:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>元敏富 - 个人简历</title>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
    <style>
        :root {
            --bg-color: #f0f0f0;
            --black: #111111;
            --white: #ffffff;
            --yellow: #ffcc00;
            --blue: #1890ff;
            --red: #ff4d4f;
            --green: #52c41a;
            --gray-bg: #f5f5f5;
            --border-thick: 3px;
            --border-radius: 8px;
        }

        body {
            font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
            background-color: var(--bg-color);
            color: var(--black);
            line-height: 1.6;
            padding: 40px 20px;
        }

        .resume-container {
            max-width: 900px;
            margin: 0 auto;
            display: flex;
            flex-direction: column;
            gap: 24px;
        }

        .brutal-box {
            background-color: var(--white);
            border: var(--border-thick) solid var(--black);
            border-radius: var(--border-radius);
            padding: 24px 30px;
        }
    </style>
</head>
<body>
    <div class="resume-container">
        <!-- 头部信息 -->
        <div class="brutal-box header-box">
            <div class="header-info">
                <h1>元敏富</h1>
                <div class="subtitle">中央民族大学 · 计算机科学与技术 · 本科</div>
                <div class="contact-grid">
                    <div><i class="fas fa-phone"></i> 17534312005</div>
                    <div><i class="fas fa-envelope"></i> min742015@gmail.com</div>
                    <!-- 更多联系方式... -->
                </div>
            </div>
            <img src="photo.jpg" alt="头像" class="avatar">
        </div>

        <!-- 专业技能 -->
        <div class="brutal-box">
            <div class="section-title"><i class="fas fa-code"></i> 专业技能</div>
            <div class="skills-grid">
                <span class="tag">Python</span>
                <span class="tag">Java</span>
                <!-- 更多技能标签... -->
            </div>
        </div>

        <!-- 项目经验 -->
        <div class="brutal-box">
            <div class="section-title"><i class="fas fa-rocket"></i> 项目经验</div>
            <div class="item-card">
                <div class="item-header">
                    <div class="item-title">虚拟恋人 Agent</div>
                    <div class="item-badge">AI 应用</div>
                </div>
                <div class="item-desc">项目描述...</div>
                <ul class="item-list">
                    <li>核心亮点 1</li>
                    <li>核心亮点 2</li>
                </ul>
            </div>
        </div>
    </div>
</body>
</html>

核心代码

HTML 转 PDF 脚本(html_to_pdf_playwright.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HTML 简历转 PDF 脚本 - 使用 Playwright + Chromium
"""

import sys
import os
import asyncio
from playwright.async_api import async_playwright


async def html_to_pdf(html_file_path):
    html_path = os.path.abspath(html_file_path)
    if not os.path.exists(html_path):
        print(f"错误: 文件不存在 {html_path}")
        sys.exit(1)

    base_dir = os.path.dirname(html_path)
    filename = os.path.splitext(os.path.basename(html_path))[0]
    pdf_path = os.path.join(base_dir, f"{filename}.pdf")

    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        # 加载本地 HTML 文件
        file_url = "file:///" + html_path.replace("\\", "/")
        await page.goto(file_url, wait_until="networkidle")

        # 等待字体和样式完全加载
        await page.wait_for_timeout(2000)

        # 获取页面实际内容高度(关键!解决分页空白问题)
        content_height = await page.evaluate("""
            () => {
                const body = document.body;
                const html = document.documentElement;
                return Math.max(
                    body.scrollHeight,
                    body.offsetHeight,
                    html.clientHeight,
                    html.scrollHeight,
                    html.offsetHeight
                );
            }
        """)

        # 生成 PDF - 宽度固定 A4 高度自适应
        await page.pdf(
            path=pdf_path,
            width="210mm",
            height=f"{content_height}px",
            print_background=True,
            margin={
                "top": "0",
                "right": "0",
                "bottom": "0",
                "left": "0"
            }
        )

        await browser.close()

    print(f"✅ PDF 已生成: {pdf_path}")


def main():
    if len(sys.argv) < 2:
        default_html = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resume.html")
        if os.path.exists(default_html):
            asyncio.run(html_to_pdf(default_html))
        else:
            print("用法: python html_to_pdf_playwright.py <html文件路径>")
            sys.exit(1)
    else:
        asyncio.run(html_to_pdf(sys.argv[1]))


if __name__ == "__main__":
    main()

使用方法

# 转换单个文件
python html_to_pdf_playwright.py resume.html

# 或直接运行(会自动处理同目录下的 resume.html)
python html_to_pdf_playwright.py

踩坑记录

坑 1:PDF 分页导致大量空白

问题描述:使用固定 A4 尺寸时,第一页底部和第二页顶部之间出现大量空白。

原因:固定 format="A4" 会在 297mm 处强制分页,内容被截断。

解决方案:动态获取页面实际高度,设置 height=f"{content_height}px"

# 之前(有问题)
await page.pdf(
    path=pdf_path,
    format="A4",  # 固定尺寸,分页位置不可控
    ...
)

# 之后(修复)
await page.pdf(
    path=pdf_path,
    width="210mm",
    height=f"{content_height}px",  # 高度自适应,无分页
    ...
)

坑 2:网络图片在 PDF 中不显示

解决方案: - 将图片替换为本地文件(与 HTML 同目录) - 或确保网络稳定 + 设置 wait_until="networkidle"

坑 3:字体加载延迟导致布局抖动

解决方案:增加等待时间

await page.wait_for_timeout(2000)  # 等待字体完全加载

坑 4:Windows 路径问题

问题file:/// 协议需要正确处理反斜杠

解决方案

file_url = "file:///" + html_path.replace("\\", "/")

设计亮点

1. 头像放置在顶部信息右侧

通过 CSS order 属性调整 Flex 布局顺序,无需改变 HTML 结构:

.avatar {
    order: 2;  /* 右侧 */
}

.header-info {
    order: 1;  /* 左侧 */
}

2. 密集型顶部信息区

将个人信息、教育背景、专业技能整合到一个模块,减少视觉层级:

┌─────────────────────────────────────────────────────────────┐
│  元敏富                      ┌──────┐                        │
│  中央民族大学 · 计算机 · 本科 │ 头像 │                        │
│  📞 175... ✉️ min... 📍 北京 │      │                        │
└─────────────────────────────────────────────────────────────┘

3. 卡片式项目经验

每个项目采用统一卡片结构,带有醒目的标签和结果展示:

┌─────────────────────────────────────────────────────┐
│  项目名称                        [AI应用]            │
├─────────────────────────────────────────────────────┤
│  项目描述...                                     │
│                                                     │
│  [Python] [FastAPI] [LangChain]                    │
│                                                     │
│  ■ 核心亮点 1                                     │
│  ■ 核心亮点 2                                     │
│  ■ 核心亮点 3                                     │
│                                                     │
│  ┌─────────────────────────────────────────────┐  │
│  │ 🏆 项目成果/数据展示                          │  │
│  └─────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

最终效果

生成的文件

文件 说明
resume.html 简历 HTML 模板,可直接在浏览器中预览和编辑
photo.jpg 简历照片(1:1 比例)
html_to_pdf_playwright.py PDF 转换脚本
resume.pdf 最终输出的 PDF 文件

工作流程

编辑 resume.html → 浏览器预览效果 → python html_to_pdf_playwright.py → 生成 resume.pdf

总结

通过 HTML + Playwright 的方案,我们实现了:

  1. 所见即所得:编辑 HTML 后浏览器直接预览,效果即最终 PDF 效果
  2. 高度可定制:CSS 完全可控,任意调整样式
  3. PDF 精准输出:高度自适应,无分页空白问题
  4. 现代化设计:支持 Flexbox、Grid、动画、图标字体等

相比传统 Markdown/LaTeX 转 PDF 的方案,这种方式更灵活、更直观,非常适合对简历视觉效果有较高要求的开发者使用。


本文档生成于 2026年5月

评论区

共 0 条
🍃 还没有评论,沙发虚位以待~
🚇

穿越虫洞?

虫洞会把你随机传送到另一位陌生博主的网站——
可能是技术大佬,也可能是诗歌爱好者,开盲盒的时间到了。

森林精灵