三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python自动化实战:Selenium与WeasyPrint实现付费网页文档转PDF

Python自动化实战:Selenium与WeasyPrint实现付费网页文档转PDF

1. 项目缘起与核心挑战

最近在帮朋友整理一份行业资料,发现一个挺有意思的需求:某个专业文档网站上有不少高质量的付费文档,但网站本身只提供在线阅读,没有直接的PDF下载按钮。朋友需要把这些文档整理成PDF,方便离线阅读和归档。这让我想起了以前用Python爬虫处理类似问题的经历,于是决定把整个思路和实现过程梳理出来。

这个需求的核心,其实是在模拟一个“完美用户”的操作:登录账号、浏览文档、然后“打印”成PDF。听起来简单,但实际操作中会遇到不少坑,比如付费墙的绕过(当然,我们讨论的是在已购买权限下的合法获取)、动态加载的内容抓取、以及如何将网页完美地转换为排版清晰的PDF文件。这不仅仅是写几行requests代码那么简单,它涉及到对现代Web应用交互逻辑的理解,以及对浏览器自动化工具的熟练运用。

我选择用Python来实现,主要是因为其生态库丰富,从网络请求到浏览器控制,再到PDF处理,都有成熟的解决方案。整个过程会用到selenium进行浏览器自动化来应对JavaScript渲染,用pdfkitWeasyPrint进行网页到PDF的转换,中间还会涉及一些等待策略、反爬应对和排版优化的小技巧。下面,我就把从环境搭建到最终成功下载保存PDF的完整流程,以及我踩过的那些坑,毫无保留地分享出来。

2. 技术选型与工具链搭建

工欲善其事,必先利其器。面对一个需要登录、内容动态加载且需要高质量PDF输出的网站,单一的技术栈很难搞定。我们需要一套组合工具。

2.1 为什么是Selenium,而不是单纯的Requests?

很多Python爬虫教程都是从requestsBeautifulSoup开始的,它们对于静态页面是利器。但对于我们这个项目,目标网站的核心内容(文档正文)极有可能是通过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 完整的工具链清单

基于以上分析,我们的工具链如下:

  • 浏览器驱动与自动化selenium+webdriver-manager(用于自动管理浏览器驱动) + Chrome浏览器。
  • PDF生成weasyprint
  • 辅助工具time(用于强制等待)、os(用于文件操作)。

安装命令非常简单:

pip install selenium webdriver-manager weasyprint

webdriver-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-features=AutomationControlled') # 使用webdriver-manager自动获取并配置驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) # 访问登录页面 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[type="submit"]') 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.IDBy.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:

  1. 获取整个页面的HTML: 直接driver.page_source。但这样会包含页头、页脚、侧边栏等无关内容。
  2. 只获取内容容器的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 charset="utf-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(string=full_html_template, base_url=doc_url) # base_url用于解析相对路径的图片、CSS html_obj.write_pdf(output_pdf_path) print(f"PDF已成功生成并保存至:{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>标签引入。WeasyPrintbase_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_config=font_config) html_obj.write_pdf(output_pdf_path, stylesheets=[css], font_config=font_config)
  • 对策三:分页控制。使用CSS的page-break-beforepage-break-afterpage-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-features=AutomationControlled') # options.add_argument('--headless') # 调试阶段建议关闭 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) 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> <head><meta 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(string=html_template, base_url=doc_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)

扩展思路:

  1. 批量下载: 将上述函数封装好,然后读取一个包含多个文档URL的列表,循环调用即可。注意在循环中加入合理的延迟(如time.sleep(random.uniform(5, 10))),避免请求过于频繁触发风控。
  2. 内容清洗: 在构建HTML模板前,可以使用BeautifulSoupcontent_html进行进一步处理,比如移除广告、无关链接、脚本标签等,让生成的PDF更纯净。
  3. 元数据添加WeasyPrint允许在生成PDF时添加元数据,如作者、标题、主题等。
    html_obj.write_pdf(pdf_path, metadata={ ‘title’: ‘你的文档标题’, ‘author’: ‘文档作者’, ‘subject’: ‘文档主题’, })
  4. 错误重试与日志: 对于不稳定的网络环境,可以添加重试机制(如tenacity库)。同时,将关键步骤和错误信息记录到日志文件中,便于后期排查。

整个流程的核心在于对目标网站结构的精准分析,以及应对各种反爬和渲染问题的耐心调试。每个网站都是独特的,没有一套代码能通吃所有情况。但掌握了Selenium的等待策略、元素定位和WeasyPrint的样式控制,你就具备了解决这类问题的通用能力。记住,始终在合法合规和尊重版权的前提下使用这些技术,仅用于已获得访问权限的个人内容备份与管理。

← 返回列表