Install
$ agentstack add skill-jamesxjz-html-to-pdf-agent-skill-html-to-pdf-agent-skill Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Destructive filesystem operation.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
HTML转PDF转换
将远程HTML网页转换为本地PDF文件,完全控制渲染选项。支持两种模式:
转换模式选择
重要:首先判断用户需要哪种模式:
- 单页模式 - 用户只提到"转换这个网页"、"单个页面"
- 完整文档模式 - 用户提到"所有章节"、"完整文档"、"包含所有页面"、"侧边栏所有链接"
快速开始
单页转换流程
- 判断HTML源的复杂度(静态页面 vs JavaScript重度页面)
- 选择合适的库
- 如需要,处理身份验证/请求头
- 配置PDF输出选项
- 保存到本地文件
完整文档转换流程
- 访问文档首页
- 真实爬取侧边栏所有链接(不要猜测URL!)
- 等待JavaScript加载完成(至少10-15秒)
- 提取所有文档链接
- 逐个转换每个页面
- 合并成单个PDF文件
库选择指南
根据页面需求选择:
| 库 | 最适合 | 核心特性 | |---------|----------|--------------| | Playwright | 现代Web应用、单页应用、JavaScript重度页面 | 完整浏览器自动化、JS执行、截图 | | WeasyPrint | 静态HTML、CSS样式页面 | 纯Python、优秀的CSS支持、无外部依赖 | | pdfkit | 通用场景、混合内容 | wkhtmltopdf封装、良好兼容性 |
默认推荐:优先使用Playwright,可靠性和功能完整性最好。
基础转换
使用 Playwright(推荐)
from playwright.sync_api import sync_playwright
def html转pdf(网址, 输出路径, **选项):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
页面.goto(网址)
页面.pdf(path=输出路径, **选项)
浏览器.close()
# 示例
html转pdf("https://example.com", "输出.pdf")
使用 WeasyPrint(静态HTML)
from weasyprint import HTML
def html转pdf(网址, 输出路径):
HTML(网址).write_pdf(输出路径)
# 示例
html转pdf("https://example.com", "输出.pdf")
高级功能
身份验证与请求头
# Playwright带自定义请求头
def html转pdf带认证(网址, 输出路径, 请求头=None):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
上下文 = 浏览器.new_context(extra_http_headers=请求头 or {})
页面 = 上下文.new_page()
页面.goto(网址)
页面.pdf(path=输出路径)
浏览器.close()
# 带身份验证的示例
请求头 = {
"Authorization": "Bearer 你的令牌",
"User-Agent": "自定义User Agent"
}
html转pdf带认证("https://example.com", "输出.pdf", 请求头)
等待JavaScript渲染
# 等待特定内容加载
def html转pdf带等待(网址, 输出路径, 选择器=None, 超时时间=30000):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
页面.goto(网址, wait_until="networkidle")
if 选择器:
页面.wait_for_selector(选择器, timeout=超时时间)
页面.pdf(path=输出路径)
浏览器.close()
# 等待特定元素
html转pdf带等待("https://example.com", "输出.pdf", 选择器="#content")
PDF格式化选项
# 完全控制PDF输出
def html转pdf格式化(网址, 输出路径):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
页面.goto(网址)
页面.pdf(
path=输出路径,
format="A4", # 纸张大小
print_background=True, # 包含背景图形
margin={ # 页边距
"top": "20mm",
"right": "20mm",
"bottom": "20mm",
"left": "20mm"
},
display_header_footer=True, # 显示页眉/页脚
header_template="我的页眉",
footer_template="第 页,共 页",
prefer_css_page_size=False, # 使用format而非CSS
landscape=False # 纵向方向
)
浏览器.close()
常见工作流
单页转换
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
页面.goto("https://example.com")
页面.pdf(path="输出.pdf", format="A4", print_background=True)
浏览器.close()
批量转换
def 批量html转pdf(网址列表, 输出目录):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
for i, 网址 in enumerate(网址列表):
页面 = 浏览器.new_page()
页面.goto(网址)
输出路径 = f"{输出目录}/页面_{i+1}.pdf"
页面.pdf(path=输出路径)
页面.close()
浏览器.close()
# 转换多个页面
网址列表 = ["https://example.com/page1", "https://example.com/page2"]
批量html转pdf(网址列表, "./pdfs")
使用自定义CSS转换
def html转pdf带css(网址, 输出路径, 自定义css=None):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
页面.goto(网址)
# 注入自定义CSS
if 自定义css:
页面.add_style_tag(content=自定义css)
页面.pdf(path=输出路径)
浏览器.close()
# 转换前隐藏元素
自定义css = """
.advertisement { display: none !important; }
.navigation { display: none !important; }
"""
html转pdf带css("https://example.com", "输出.pdf", 自定义css)
错误处理
始终处理常见错误:
def 安全html转pdf(网址, 输出路径):
try:
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
# 设置超时和错误处理
页面.set_default_timeout(30000)
响应 = 页面.goto(网址)
if 响应.status != 200:
raise Exception(f"HTTP {响应.status}: 加载页面失败")
页面.pdf(path=输出路径)
浏览器.close()
return True
except Exception as e:
print(f"转换错误 {网址}: {str(e)}")
return False
安装要求
向用户说明所需的包:
Playwright(推荐,单页和完整文档都需要):
pip install playwright
playwright install chromium
PyPDF2(仅完整文档模式需要,用于合并PDF):
pip install PyPDF2
WeasyPrint(可选,静态HTML单页转换):
pip install weasyprint
pdfkit(可选,备选方案):
pip install pdfkit
# 还需要在系统上安装wkhtmltopdf
决策树
根据用户需求选择转换模式:
第1步:判断转换模式
用户说了什么?
- "转换这个网页" / "把这个URL转成PDF" / "单个页面"
→ 使用单页模式
- "所有章节" / "完整文档" / "包含侧边栏所有页面" / "包含所有子页面"
→ 使用完整文档模式
第2步:选择库(单页模式)
- 是否需要执行JavaScript?
- 是 → 使用Playwright
- 否 → 继续步骤2
- 页面是否需要身份验证或自定义请求头?
- 是 → 使用Playwright
- 否 → 继续步骤3
- 是否为带CSS样式的静态HTML?
- 是 → 使用WeasyPrint(更快、更轻)
- 否 → 使用Playwright(最安全的默认选择)
第3步:完整文档模式的关键点
必须遵守的规则:
- ⚠️ 绝对不要猜测URL路径!
- ❌ 错误:假设路径是
/docs/agent/pane - ✅ 正确:从页面上真实提取链接
- ⚠️ 必须等待JavaScript加载!
- ❌ 错误:立即提取(只能找到2-3个链接)
- ✅ 正确:等待10-15秒后提取(能找到50+个链接)
- ⚠️ 使用 page.evaluate() 提取链接!
- ✅ 在浏览器上下文中运行JavaScript
- ✅ 能获取动态渲染的内容
- ⚠️ 需要安装 PyPDF2 来合并!
- 如果未安装:
pip install PyPDF2
完整文档模式的详细实现
import time
# 步骤1:真实提取导航链接
def 提取所有文档链接(页面, 首页url):
"""
关键:真实爬取,不猜测!
"""
页面.goto(首页url, timeout=60000)
# 重要!等待足够长的时间
time.sleep(15)
# 使用JavaScript提取所有链接
链接数据 = 页面.evaluate("""
() => {
const links = Array.from(document.querySelectorAll('a'));
return links.map(a => ({
text: a.textContent.trim(),
href: a.href
})).filter(l => l.text && l.href.includes('/docs/'));
}
""")
# 去重
唯一链接 = {}
for 项 in 链接数据:
url = 项['href']
if url not in 唯一链接:
唯一链接[url] = 项['text']
return [(标题, url) for url, 标题 in 唯一链接.items()]
# 步骤2:批量转换
def 批量转换并合并(文档列表, 输出文件):
"""
转换所有页面并合并
"""
from PyPDF2 import PdfMerger
import tempfile
临时目录 = tempfile.mkdtemp()
pdf文件列表 = []
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
for i, (标题, url) in enumerate(文档列表, 1):
try:
页面.goto(url, wait_until="domcontentloaded", timeout=30000)
time.sleep(1)
# 隐藏导航
页面.add_style_tag(content="nav, header, .sidebar { display: none !important; }")
pdf路径 = os.path.join(临时目录, f"{i:03d}.pdf")
页面.pdf(path=pdf路径, format="A4", print_background=True)
pdf文件列表.append(pdf路径)
except:
pass
浏览器.close()
# 合并
merger = PdfMerger()
for pdf in pdf文件列表:
merger.append(pdf)
merger.write(输出文件)
merger.close()
# 清理
import shutil
shutil.rmtree(临时目录)
常见问题与解决方案
单页转换问题
问题:PDF为空白或不完整
- 解决方案:添加
wait_until="networkidle"或等待特定选择器
问题:需要身份验证
- 解决方案:使用
extra_http_headers或带cookies的浏览器上下文
问题:背景图形缺失
- 解决方案:在PDF选项中设置
print_background=True
问题:页面布局错乱
- 解决方案:设置合适的
format、margin和prefer_css_page_size选项
问题:内容被截断
- 解决方案:调整边距或使用
height参数进行全页捕获
完整文档转换问题
问题:只找到2-3个链接,大部分页面缺失
- 原因:JavaScript还没加载完成就提取了链接
- 解决方案:在
page.goto()后必须time.sleep(15)等待足够长时间
问题:很多页面返回404
- 原因:URL路径是猜测的,不是真实爬取的
- 解决方案:必须使用
page.evaluate()从页面真实提取 `` 标签
问题:单页应用(SPA)导航不可见
- 原因:侧边栏是动态渲染的
- 解决方案:
- 等待时间增加到 15 秒
- 尝试滚动页面触发懒加载
- 使用
wait_for_selector()等待特定导航元素
问题:知乎、微信等网站返回403
- 原因:反爬虫保护
- 解决方案:
- 添加真实的User-Agent
- 使用非无头模式(headless=False)
- 添加反检测脚本
- 最后手段:建议用户手动打印
问题:合并PDF失败
- 原因:缺少 PyPDF2 库
- 解决方案:
pip install PyPDF2
问题:日志显示失败但PDF实际生成成功
- 原因:某些操作(如CSS注入)可能返回None但不影响PDF生成
- 解决方案:已在新版本修复,现在基于PDF文件是否真实生成来判断成功/失败
最佳实践与技巧
1. 性能优化
# 对于大量页面转换,复用浏览器实例
def 批量转换优化(url列表, 输出目录):
with sync_playwright() as p:
浏览器 = p.chromium.launch()
页面 = 浏览器.new_page()
for i, url in enumerate(url列表):
# 复用同一个页面对象,避免频繁创建/销毁
页面.goto(url, wait_until="domcontentloaded")
页面.pdf(path=f"{输出目录}/{i:03d}.pdf")
浏览器.close()
2. 智能等待策略
# 根据页面复杂度动态调整等待时间
def 智能等待(页面对象, 网址):
页面对象.goto(网址, wait_until="domcontentloaded")
# 检查是否为SPA
是否spa = 页面对象.evaluate("() => !!window.React || !!window.Vue || !!window.Angular")
if 是否spa:
print("检测到SPA,等待15秒...")
time.sleep(15)
else:
print("静态页面,等待3秒...")
time.sleep(3)
3. 自定义链接过滤
# 使用正则表达式或自定义函数过滤链接
def 高级链接过滤(所有链接, 自定义条件):
"""
自定义条件示例:
lambda link: '/docs/' in link['href'] and 'api' not in link['href']
"""
return [链接 for 链接 in 所有链接 if 自定义条件(链接)]
# 使用示例
文档链接 = 高级链接过滤(
所有链接,
lambda l: '/guide/' in l['href'] and len(l['text']) > 3
)
4. 错误恢复与重试
def 转换带重试(页面对象, 网址, 输出路径, 最大重试=3):
"""失败后自动重试"""
for 尝试次数 in range(最大重试):
try:
响应 = 页面对象.goto(网址, timeout=30000)
if 响应.status == 200:
页面对象.pdf(path=输出路径)
if os.path.exists(输出路径):
return True
except Exception as e:
if 尝试次数 Array.from(document.querySelectorAll('a'))
.map(a => ({text: a.textContent.trim(), href: a.href}))
""")
所有页面.extend(链接)
# 查找"下一页"按钮
try:
下一页 = 页面对象.query_selector('a[aria-label*="next"], a:has-text("下一页")')
if 下一页:
下一页.click()
time.sleep(2)
当前页 += 1
else:
break
except:
break
return 所有页面
关键代码片段
提取侧边栏链接的正确方法
# ✅ 正确方法:真实爬取
页面.goto("https://example.com/docs", timeout=60000)
time.sleep(15) # 关键!等待JavaScript渲染
链接 = 页面.evaluate("""
() => {
const links = Array.from(document.querySelectorAll('a'));
return links
.filter(a => a.href.includes('/docs/'))
.map(a => ({text: a.textContent.trim(), href: a.href}));
}
""")
# ❌ 错误方法:猜测URL
文档列表 = [
("概览", "https://example.com/docs/overview"), # 可能是错的!
("安装", "https://example.com/docs/install"), # 可能是错的!
]
处理反爬虫保护
# 添加反检测
页面.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")
# 使用真实浏览器设置
上下文 = 浏览器.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
locale='zh-CN'
)
输出验证
转换后验证PDF:
import os
def 验证pdf已创建(输出路径, 最小大小kb=10):
if not os.path.exists(输出路径):
print(f"❌ PDF未创建: {输出路径}")
return False
大小kb = os.path.getsize(输出路径) / 1024
if 大小kb {
return Array.from(document.querySelectorAll('a'))
.map(a => ({text: a.textContent.trim(), href: a.href}))
.filter(l => l.text && l.href.includes('/docs/'));
}
""")
# 去重
文档列表 = []
已见 = set()
for 项 in 链接:
if 项['href'] not in 已见:
已见.add(项['href'])
文档列表.append((项['text'], 项['href']))
print(f"找到 {len(文档列表)} 个文档页面")
# 第2步:批量转换
临时目录 = "/tmp/pdfs"
os.makedirs(临时目录, exist_ok=True)
pdf列表 = []
for i, (标题, url) in enumerate(文档列表, 1):
try:
页面.goto(url, wait_until="domcontentloaded", timeout=30000)
time.sleep(1)
页面.add_style_tag(content="nav, header { display: none !important; }")
pdf路径 = f"{临时目录}/{i:03d}.pdf"
页面.pdf(path=pdf路径, format="A4", print_background=True)
pdf列表.append(pdf路径)
print(f"[{i}/{len(文档列表)}] ✅ {标题}")
except:
print(f"[{i}/{len(文档列表)}] ❌ {标题}")
浏览器.close()
# 第3步:合并
merger = PdfMerger()
for pdf in pdf列表:
merger.append(pdf)
merger.write("完整文档.pdf")
merger.close()
print(f"✅ 完成!生成了完整文档.pdf")
完整工作流示例
模式1:单页转换
from playwright.sync_api import sync_playwright
import os
def 转换单个网页(网址, 输出路径, **选项):
"""
转换单个网页为PDF
参数:
网址: 要转换的远程URL
输出路径: 保存PDF的本地路径
**选项: 额外选项(请求头、等待选择器、自定义CSS、PDF选项)
"""
请求头 = 选项.get('请求头', {})
等待选择器 = 选项.get('等待选择器')
自定义css = 选项.get('自定义css')
pdf选项 = 选项.get('pdf选项', {})
默认pdf选项 = {
'format': 'A4',
'print_background': True,
'margin': {'top': '20mm', 'right': '20mm', 'bottom': '20mm', 'left': '20mm'}
}
默认pdf选项.update(pdf选项)
try:
with sync_playwright() as p:
浏览器 = p.chromium.launch()
上下文 = 浏览器.new_context(extra_http_headers=请求头) if 请求头 else 浏览器
页面 = 上下文.new_page() if 请求头 else 浏览器.new_page()
响应 = 页面.goto(网址, wait_until="networkidle")
if 响应.status != 200:
raise Exception(f"HTTP {响应.status}")
if 等待选择器:
页面.wait_for_selector(等待选择器, timeout=30000)
if 自定义css:
页面.add_style_tag(content=自定义css)
页面.pdf(path=输出路径, **默认pdf选项)
浏览器.close()
if os.path.exists(输出路径):
大小kb = os.path.getsize(输出路径) / 1024
print(f"✅ PDF已创建: {输出路径} ({大小kb:.1f} KB)")
return True
else:
print(f"❌ PDF创建失败")
return False
except Exception as e:
print(f"❌ 错误: {str(e)}")
return False
模式2:完整文档转换(自动爬取侧边栏)
关键要点:
- ❌ 不要猜测URL路径
- ✅ 必须真实爬取页面上的链接
- ✅ 等待JavaScript加载完成(SPA需要时间)
from playwright.sync_api import sync_playwright
import os
import time
def 提取真实导航链接(页面对象, 基础网址, 链接过滤关键词=None):
"""
从页面上真实提取所有文档链接
关键步骤:
1. 访问文档首页
2. 等待JavaScript加载(10-15秒)
3. 提取所有标签
4. 过滤出文档链接
参数:
页面对象: Playwright页面对象
基础网址: 文档首页URL
链接过滤关键词: 可选的URL过滤关键词列表,如 ['/docs/', '/api/']
"""
print("正在访问文档首页...")
页面对象.goto(基础网址, timeout=60000)
print("等待JavaScript加载完成(15秒)...")
time.sleep(15) # 重要!SPA需要时间渲染
print("提取所有文档链接...")
所有链接 = 页面对象.evaluate("""
() => {
// 获取页面上所有链接
const links = Array.from(document.querySelectorAll('a'));
return links.map(a => ({
text: a.textContent.trim(),
href: a.href,
className: a.className
})).filter(l => l.text && l.href);
}
""")
# 自动推断过滤关键词(如果未提供)
if 链接过滤关键词 is None:
from urllib.parse import urlparse
基础路径 = urlparse(基础网址).path
链接过滤关键词 = [基础路径] if 基础路径 else ['/docs/', '/api/', '/guide/']
# 过滤文档链接
文档链接 = []
已见url = set()
# 常见的要跳过的标题(通用列表)
跳过标题 = ['Logo', 'Docs', 'API', 'Guide', 'Documentation',
'Skip to', 'Menu', 'Toggle', 'Search', 'GitHub',
'文档', '指南', '菜单', '搜索', '跳转']
for 链接 in 所有链接:
url = 链接['href']
标题 = 链接['text']
# 检查URL是否匹配任一过滤关键词
url匹配 = any(关键词 in url for 关键词 in 链接过滤关键词)
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [jamesxjz](https://github.com/jamesxjz)
- **Source:** [jamesxjz/html-to-pdf-agent-skill](https://github.com/jamesxjz/html-to-pdf-agent-skill)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.