Python自动化实战:Selenium与WeasyPrint实现付费网页文档转PDF
1. 项目缘起与核心挑战最近在帮朋友整理一份行业资料发现一个挺有意思的需求某个专业文档网站上有不少高质量的付费文档但网站本身只提供在线阅读没有直接的PDF下载按钮。朋友需要把这些文档整理成PDF方便离线阅读和归档。这让我想起了以前用Python爬虫处理类似问题的经历于是决定把整个思路和实现过程梳理出来。这个需求的核心其实是在模拟一个“完美用户”的操作登录账号、浏览文档、然后“打印”成PDF。听起来简单但实际操作中会遇到不少坑比如付费墙的绕过当然我们讨论的是在已购买权限下的合法获取、动态加载的内容抓取、以及如何将网页完美地转换为排版清晰的PDF文件。这不仅仅是写几行requests代码那么简单它涉及到对现代Web应用交互逻辑的理解以及对浏览器自动化工具的熟练运用。我选择用Python来实现主要是因为其生态库丰富从网络请求到浏览器控制再到PDF处理都有成熟的解决方案。整个过程会用到selenium进行浏览器自动化来应对JavaScript渲染用pdfkit或WeasyPrint进行网页到PDF的转换中间还会涉及一些等待策略、反爬应对和排版优化的小技巧。下面我就把从环境搭建到最终成功下载保存PDF的完整流程以及我踩过的那些坑毫无保留地分享出来。2. 技术选型与工具链搭建工欲善其事必先利其器。面对一个需要登录、内容动态加载且需要高质量PDF输出的网站单一的技术栈很难搞定。我们需要一套组合工具。2.1 为什么是Selenium而不是单纯的Requests很多Python爬虫教程都是从requests和BeautifulSoup开始的它们对于静态页面是利器。但对于我们这个项目目标网站的核心内容文档正文极有可能是通过JavaScript动态加载的。直接使用requests获取到的HTML很可能只是一个空壳框架真正的文档内容并不在里面。这时候我们需要一个能执行JavaScript的“浏览器”。Selenium就是一个浏览器自动化工具。它可以驱动真实的浏览器如Chrome、Firefox访问网页等待页面完全加载包括所有JS执行完毕然后再获取完整的DOM内容。这就完美解决了动态内容加载的问题。此外像登录过程中的验证码如果是简单图形验证码、点击翻页等交互操作Selenium模拟起来也比单纯的HTTP请求库要直观和稳定得多。2.2 PDF生成工具的抉择pdfkit vs. WeasyPrint当我们用Selenium拿到了完整的网页HTML后下一步就是将其转为PDF。这里有两个主流选择pdfkit 这实际上是wkhtmltopdf命令行工具的Python封装。wkhtmltopdf渲染引擎基于Qt WebKit对CSS的支持很好生成质量高是我很长一段时间内的首选。WeasyPrint 一个纯Python的库旨在将HTML/CSS文档转换为PDF。它的优势是无需依赖外部二进制程序部署更干净并且对现代CSS标准如Flexbox, Grid的支持理论上更好。我最初选择了pdfkit但在实际项目中遇到了一个棘手问题某些使用了特定CSS字体或复杂布局的页面用pdfkit转换时会出现排版错乱或字体缺失。后来切换到WeasyPrint问题得到了解决并且因为它是Python原生库在跨平台部署时少了很多环境依赖的麻烦。因此在本教程中我将以WeasyPrint作为核心PDF生成工具。2.3 完整的工具链清单基于以上分析我们的工具链如下浏览器驱动与自动化seleniumwebdriver-manager(用于自动管理浏览器驱动) Chrome浏览器。PDF生成weasyprint。辅助工具time(用于强制等待)、os(用于文件操作)。安装命令非常简单pip install selenium webdriver-manager weasyprintwebdriver-manager是个神器它会自动下载匹配你本地Chrome版本的chromedriver省去了手动查找和配置环境变量的步骤。3. 实战步骤分解从登录到保存PDF理论说完我们进入实战环节。我会假设目标网站的结构是需要登录文档页面有一个主要的容器来显示内容并且可能有多页。3.1 初始化浏览器并实现登录首先我们需要初始化一个Selenium控制的Chrome浏览器。为了使其更接近真实用户并减少被反爬机制识别的风险我们通常会添加一些选项。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time # 配置Chrome选项 options webdriver.ChromeOptions() # 禁用自动化控制提示避免网站检测 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 可选项无头模式不显示浏览器界面调试时可注释掉 # options.add_argument(--headless) # 可选项禁用GPU在某些环境下更稳定 options.add_argument(--disable-gpu) # 防止网站检测到webdriver属性 options.add_argument(--disable-blink-featuresAutomationControlled) # 使用webdriver-manager自动获取并配置驱动 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) # 访问登录页面 login_url https://目标网站.com/login # 请替换为实际登录地址 driver.get(login_url) time.sleep(2) # 等待页面加载可配合显式等待优化 # 定位登录表单元素并输入信息 # 你需要通过浏览器开发者工具F12查看登录页面的HTML结构找到用户名和密码输入框的id或name try: username_input driver.find_element(By.ID, username) # 也可能是 By.NAME, By.CSS_SELECTOR 等 password_input driver.find_element(By.ID, password) username_input.send_keys(你的账号) password_input.send_keys(你的密码) # 找到登录按钮并点击 login_button driver.find_element(By.CSS_SELECTOR, button[typesubmit]) login_button.click() # 等待登录成功通常可以等待某个登录后才会出现的元素比如用户头像 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, user-avatar)) ) print(登录成功) except Exception as e: print(f登录过程出现错误{e}) driver.quit()注意 上面的代码是模板By.ID、By.CSS_SELECTOR后面的选择器需要你根据目标网站的实际HTML结构进行修改。使用浏览器的“检查”功能右键点击输入框或按钮选择“Copy” - “Copy selector”或“Copy XPath”可以快速获取。3.2 导航至目标文档页面并获取完整内容登录成功后我们就可以访问付费文档了。这里的关键是确保文档内容包括所有通过JS加载的部分已经完全渲染在页面中。# 假设文档页面的URL是已知的或者可以从某个列表页获取 doc_url https://目标网站.com/doc/12345 # 替换为实际文档地址 driver.get(doc_url) # 核心等待文档内容区域加载完成 # 你需要找到包含文档正文的HTML容器的选择器比如一个div的id是article-content content_selector #article-content # 这是一个CSS选择器示例 try: # 显式等待最多等15秒直到内容容器出现在DOM中且可见 content_element WebDriverWait(driver, 15).until( EC.visibility_of_element_located((By.CSS_SELECTOR, content_selector)) ) print(文档主要内容已加载。) # 进一步如果文档有分页可能需要模拟点击“下一页”或滚动加载 # 这里以点击“下一页”按钮为例直到没有下一页 while True: # 假设下一页按钮的CSS选择器是 .next-page next_button driver.find_elements(By.CSS_SELECTOR, .next-page) if not next_button or disabled in next_button[0].get_attribute(class): print(已到达文档末尾。) break # 点击下一页 next_button[0].click() time.sleep(1.5) # 等待新内容加载可根据网络情况调整 # 等待新加载的内容也出现在容器内 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, f{content_selector}:last-child)) ) except Exception as e: print(f加载文档内容时出错{e})3.3 将网页内容转换为PDF当所有内容都加载完毕后我们有两种思路来生成PDF获取整个页面的HTML 直接driver.page_source。但这样会包含页头、页脚、侧边栏等无关内容。只获取内容容器的HTML 使用content_element.get_attribute(outerHTML)。这通常更干净。我强烈推荐第二种方法因为我们可以只转换我们需要的核心内容并对它进行样式优化。from weasyprint import HTML, CSS import os # 获取纯净的内容HTML content_html content_element.get_attribute(outerHTML) # 构建一个完整的HTML文档字符串可以在这里注入自定义CSS来优化PDF样式 # 例如确保字体、强制分页等 full_html_template f !DOCTYPE html html head meta charsetutf-8 style /* 这里可以添加全局样式确保PDF打印效果 */ body {{ font-family: SimSun, STSong, serif; /* 指定中文字体确保中文显示 */ font-size: 14px; line-height: 1.6; margin: 0; padding: 20px; }} img {{ max-width: 100%; /* 防止图片溢出 */ height: auto; }} pre, code {{ background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }} /* 强制在每个h2标题前分页 */ h2 {{ page-break-before: always; }} /* 避免在段落中间分页 */ p {{ page-break-inside: avoid; }} /style /head body {content_html} /body /html # 指定输出PDF路径 output_pdf_path os.path.join(os.getcwd(), downloaded_document.pdf) # 使用WeasyPrint生成PDF # 注意HTML类可以接受字符串 try: html_obj HTML(stringfull_html_template, base_urldoc_url) # base_url用于解析相对路径的图片、CSS html_obj.write_pdf(output_pdf_path) print(fPDF已成功生成并保存至{output_pdf_path}) except Exception as e: print(f生成PDF时出错{e}) finally: # 关闭浏览器 driver.quit()3.4 处理复杂情况图片、特殊样式与登录态维持在实际操作中你可能会遇到更多问题图片加载问题 如果文档图片是延迟加载lazy load的可能需要滚动到图片位置才能触发加载。可以在获取HTML前用JavaScript滚动页面driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)并配合短暂等待。CSS样式丢失 原网页的样式可能通过link标签引入。WeasyPrint的base_url参数能帮助解析相对路径但如果是复杂的网络字体或绝对路径可能需要将相关CSS文件下载到本地或直接在style标签中重写关键样式。登录态Cookie/Session Selenium驱动的浏览器和手动打开的浏览器一样登录后Cookie会保留。只要在同一个driver实例内跳转页面登录态就会保持。无需手动处理Cookie。4. 关键问题排查与优化经验即使按照上述步骤操作你也可能遇到各种意想不到的问题。下面分享几个我踩过的坑和解决方案。4.1 内容加载不全或等待失效这是最常见的问题。WebDriverWait配合EC.visibility_of_element_located并不总是万能特别是对于复杂SPA单页应用。对策一更精准的等待条件。不要只等一个容器出现可以等待容器内的某个特定元素如第一段文字出现。WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, “#article-content p”)) )对策二结合强制等待与JavaScript检测。有时需要给复杂的JS渲染留出时间。time.sleep(3) # 或者用JS检查文档是否已准备就绪 is_ready driver.execute_script(return document.readyState complete;)对策三滚动触发加载。对于无限滚动或点击加载更多的页面需要在循环中不断滚动到底部或点击按钮直到没有新内容。4.2 PDF排版混乱字体缺失WeasyPrint对CSS的支持很好但网页中的样式可能非常复杂且冲突。对策一简化并重写核心样式。就像上面的代码示例在模板的style标签里用更强制性的CSS规则覆盖原网页样式。例如!important规则有时是必要的body { font-family: ‘SimSun’, serif !important; }。对策二指定中文字体路径。如果目标系统没有中文字体PDF中的中文会显示为方框。解决方案是将字体文件如.ttf路径明确告诉WeasyPrint。from weasyprint.text.fonts import FontConfiguration font_config FontConfiguration() css CSS(string‘font-face { font-family: “MyFont”; src: url(“/path/to/your/font.ttf”); }’, font_configfont_config) html_obj.write_pdf(output_pdf_path, stylesheets[css], font_configfont_config)对策三分页控制。使用CSS的page-break-before、page-break-after、page-break-inside属性精细控制PDF的分页位置避免表格或图片被截断。4.3 被网站识别为自动化脚本一些网站会检测navigator.webdriver等属性来识别Selenium。对策一使用undetected-chromedriver。这是一个专门用于绕过检测的库可以视为Selenium的增强版。安装pip install undetected-chromedriver。使用方式与Selenium类似但初始化更简单。import undetected_chromedriver as uc driver uc.Chrome() driver.get(login_url) # ... 后续操作与Selenium一致对策二谨慎使用无头模式。无头模式--headless更容易被一些高级反爬系统识别。在调试阶段建议先使用有界面模式确保核心流程跑通。对策三模拟人类操作。在关键操作如点击、输入之间加入随机延迟time.sleep(random.uniform(1, 3))并模拟鼠标移动轨迹虽然Selenium原生支持有限但可通过ActionChains做一些简单模拟。5. 完整代码整合与扩展思路将以上所有步骤整合一个健壮性更高的脚本框架如下import time import os import random from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from weasyprint import HTML, CSS def download_paid_doc_to_pdf(login_url, doc_url, username, password, content_selector, output_filename): 将指定付费文档下载为PDF Args: login_url: 网站登录地址 doc_url: 目标文档地址 username: 登录用户名 password: 登录密码 content_selector: 文档正文容器的CSS选择器 output_filename: 输出PDF文件名 # 1. 初始化浏览器这里使用标准Selenium可替换为undetected_chromedriver options webdriver.ChromeOptions() options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) options.add_argument(--disable-blink-featuresAutomationControlled) # options.add_argument(--headless) # 调试阶段建议关闭 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) try: # 2. 登录 driver.get(login_url) time.sleep(random.uniform(2, 4)) # 请根据实际网站修改以下选择器 driver.find_element(By.NAME, account).send_keys(username) driver.find_element(By.NAME, password).send_keys(password) driver.find_element(By.XPATH, //button[contains(text(), “登录”)]).click() # 等待登录成功标志 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CLASS_NAME, “user-menu”)) ) print(“登录成功。”) time.sleep(random.uniform(1, 2)) # 3. 访问文档并获取内容 driver.get(doc_url) # 等待内容区域加载 content_element WebDriverWait(driver, 20).until( EC.visibility_of_element_located((By.CSS_SELECTOR, content_selector)) ) print(“文档主体加载完成。”) # 模拟滚动或翻页以加载全部内容根据网站结构调整 last_height driver.execute_script(“return document.body.scrollHeight”) while True: driver.execute_script(“window.scrollTo(0, document.body.scrollHeight);”) time.sleep(random.uniform(1.5, 2.5)) new_height driver.execute_script(“return document.body.scrollHeight”) if new_height last_height: break last_height new_height # 再次确保内容已完全渲染 time.sleep(2) content_html content_element.get_attribute(‘outerHTML’) # 4. 生成PDF html_template f””” !DOCTYPE html html headmeta charset”utf-8”style body {{ font-family: ‘SimSun’, ‘STSong’, serif; font-size: 15px; line-height: 1.8; padding: 2cm; }} h1, h2, h3 {{ page-break-after: avoid; }} pre, code {{ background: #f8f8f8; padding: 10px; border-radius: 5px; overflow-x: auto; }} img {{ max-width: 100%; height: auto; display: block; margin: 10px auto; }} table {{ border-collapse: collapse; width: 100%; margin: 15px 0; }} th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }} /style/head body{content_html}/body /html ””” pdf_path os.path.join(os.getcwd(), output_filename) html_obj HTML(stringhtml_template, base_urldoc_url) html_obj.write_pdf(pdf_path) print(f”PDF已成功保存{pdf_path}”) except Exception as e: print(f”流程执行出错{e}”) # 可以在这里截图方便调试 driver.save_screenshot(‘error_screenshot.png’) finally: driver.quit() # 使用示例 if __name__ “__main__”: # 这些参数需要你根据目标网站实际情况填写 MY_LOGIN_URL “https://example.com/login” MY_DOC_URL “https://example.com/doc/123” USERNAME “your_username” PASSWORD “your_password” CONTENT_SELECTOR “.document-content” # 使用浏览器的检查工具确定 OUTPUT_FILE “我的付费文档.pdf” download_paid_doc_to_pdf(MY_LOGIN_URL, MY_DOC_URL, USERNAME, PASSWORD, CONTENT_SELECTOR, OUTPUT_FILE)扩展思路批量下载 将上述函数封装好然后读取一个包含多个文档URL的列表循环调用即可。注意在循环中加入合理的延迟如time.sleep(random.uniform(5, 10))避免请求过于频繁触发风控。内容清洗 在构建HTML模板前可以使用BeautifulSoup对content_html进行进一步处理比如移除广告、无关链接、脚本标签等让生成的PDF更纯净。元数据添加WeasyPrint允许在生成PDF时添加元数据如作者、标题、主题等。html_obj.write_pdf(pdf_path, metadata{ ‘title’: ‘你的文档标题’, ‘author’: ‘文档作者’, ‘subject’: ‘文档主题’, })错误重试与日志 对于不稳定的网络环境可以添加重试机制如tenacity库。同时将关键步骤和错误信息记录到日志文件中便于后期排查。整个流程的核心在于对目标网站结构的精准分析以及应对各种反爬和渲染问题的耐心调试。每个网站都是独特的没有一套代码能通吃所有情况。但掌握了Selenium的等待策略、元素定位和WeasyPrint的样式控制你就具备了解决这类问题的通用能力。记住始终在合法合规和尊重版权的前提下使用这些技术仅用于已获得访问权限的个人内容备份与管理。