
本文详解如何在 python 中稳定、批量地将多个网页保存为 pdf 文件,重点解决 pyqt5 多页并发导出时进程提前退出、部分文件丢失的问题,并提供可扩展的面向对象实现方案。
本文详解如何在 python 中稳定、批量地将多个网页保存为 pdf 文件,重点解决 pyqt5 多页并发导出时进程提前退出、部分文件丢失的问题,并提供可扩展的面向对象实现方案。
在使用 PyQt5 的 QWebEnginePage 批量导出网页为 PDF 时,常见陷阱是:过早调用 app.quit() 或 QApplication.exit(),导致尚未完成加载或打印的页面被强制中止,最终仅生成部分 PDF 文件。根本原因在于 pdfPrintingFinished 信号触发时机异步且不可控,而原代码对每个页面都绑定 QApplication.exit(),造成首个完成的页面就终结整个事件循环。
✅ 正确做法:统一管理生命周期,按需退出
推荐采用面向对象封装 + 中央调度器的方式,避免全局状态污染和竞态问题。以下是一个健壮、可复用的 WebPdfPrinter 类实现:
import sys
from pathlib import Path
from PyQt5.QtCore import QUrl, QPageLayout, QPageSize
from PyQt5.QtWebEngineWidgets import QWebEnginePage
from PyQt5.QtWidgets import QApplication
class WebPdfPrinter(QApplication):
def __init__(self, url_path_pairs):
super().__init__(sys.argv)
# 全局页面布局(A4 纵向)
self.pageLayout = QPageLayout()
self.pageLayout.setPageSize(QPageSize(QPageSize.A4Extra))
self.pageLayout.setOrientation(QPageLayout.Portrait)
# 存储待处理的 (page, filepath) 映射
self.pending = {} # {url: (page, path)}
for url, path in url_path_pairs:
if not Path(path).exists():
page = QWebEnginePage()
self.pending[url] = (page, path)
# 绑定加载完成回调(注意:lambda 捕获 url 需显式绑定)
page.loadFinished.connect(
lambda ok, u=url: self._on_load_finished(ok, u)
)
page.load(QUrl(url))
def _on_load_finished(self, ok, url):
if not ok:
print(f"⚠️ 页面加载失败:{url}")
self._mark_as_done(url, success=False)
return
page, path = self.pending[url]
try:
page.printToPdf(path, self.pageLayout)
# 绑定打印完成回调(同样注意闭包捕获)
page.pdfPrintingFinished.connect(
lambda p, success, u=url: self._on_print_finished(success, u)
)
except Exception as e:
print(f"❌ PDF 打印异常 ({url}): {e}")
self._mark_as_done(url, success=False)
def _on_print_finished(self, success, url):
self._mark_as_done(url, success)
def _mark_as_done(self, url, success):
if url not in self.pending:
return
page, path = self.pending.pop(url)
page.deleteLater() # 及时释放资源
if success:
print(f"✅ PDF 已保存:{path}")
else:
print(f"❌ PDF 生成失败:{path}")
# 所有任务完成,退出应用
if not self.pending:
print("? 所有任务完成,正在退出...")
self.quit()? 使用示例(整合你的爬取逻辑)
import requests
from bs4 import BeautifulSoup
# 1. 获取目标 URL 列表(保持你原有的解析逻辑)
catalogue_url = "https://debis.deu.edu.tr/ders-katalog/2023-2024/tr/"
department_url = "bolum_1129_tr.html"
reqs = requests.get(catalogue_url + department_url)
soup = BeautifulSoup(reqs.content, 'html.parser')
urls = soup.find_all('a')
hrefs = [a.get('href') for a in urls]
try:
start_idx = hrefs.index("../havuz/1129tr.pdf")
end_idx = hrefs.index("bolum_1129_tr.html")
except ValueError:
raise RuntimeError("未找到起始或结束锚点,请检查 HTML 结构")
# 2. 构建 (url, filepath) 列表
url_list = []
for i in range(start_idx + 1, end_idx):
course_url = catalogue_url + urls[i].get('href')
filepath = f"File({i}).pdf"
url_list.append((course_url, filepath))
# 3. 启动批量 PDF 打印器
if url_list:
app = WebPdfPrinter(url_list)
sys.exit(app.exec())
else:
print("ℹ️ 无新页面需要导出。")⚠️ 关键注意事项
- 必须使用 QWebEnginePage 而非 QWebEngineView:后者用于 GUI 显示,而前者轻量、无界面,更适合后台批量处理。
- 避免在 lambda 中直接引用循环变量:Python 闭包会捕获变量名而非值,务必用 u=url 显式绑定(如 lambda ok, u=url:),否则所有回调可能共享最后一个 url。
- 及时清理资源:调用 page.deleteLater() 防止内存泄漏;QWebEnginePage 对象需显式销毁。
- 错误处理不可省略:网络超时、JS 渲染失败、PDF 写入权限不足等均可能导致 loadFinished 返回 False 或 pdfPrintingFinished 中 success=False,应记录日志并继续执行其余任务。
- 环境依赖:确保已安装 PyQt5 和 PyQtWebEngine(pip install PyQt5 PyQtWebEngine),且系统具备 Qt WebEngine 运行时支持(Windows 用户建议使用官方二进制包而非 conda)。
该方案彻底规避了“多页面竞争退出”的缺陷,通过中央状态管理确保每个页面完整经历「加载 → 渲染 → 打印 → 回调 → 清理」全流程,稳定支持数十甚至上百个网页的批量 PDF 导出。


















