1. 项目概述与核心价值

最近在做一个数据归档的项目,需要从几个付费知识平台批量下载一些文档资料。手动操作?光是想到要一个个登录、点击、复制粘贴、再整理格式,头就大了。而且这些平台通常都有反爬机制,简单的 requests 库请求很容易被识别和拦截。这时候,一个能模拟真人操作、能处理复杂交互的自动化工具就成了刚需。Selenium,这个在自动化测试领域大名鼎鼎的框架,恰恰是解决这类问题的利器。它不仅能驱动浏览器真实地打开网页、点击按钮、输入文字,还能执行JavaScript,完美应对需要登录、有动态加载内容的付费站点。

这个项目的目标很明确:写一个Python脚本,用Selenium自动化登录付费文档平台,定位并抓取目标文档的正文内容,然后将这些内容规整地保存到Word文档中。这不仅仅是“抓取”,更是一个“采集-清洗-格式化输出”的完整流程。对于需要做行业报告、竞品分析、资料汇编的朋友来说,掌握这套方法能极大提升效率。当然,我们必须时刻牢记网络礼仪和法律法规,抓取的内容仅用于个人学习研究,绝不用于商业牟利或对目标服务器造成恶意压力。接下来,我就把这次实战中的思路、踩过的坑和最终成型的代码,毫无保留地分享出来。

2. 技术选型与工具准备

2.1 为什么是Selenium?

在Python爬虫生态里, requests + BeautifulSoup 是经典组合,但对于现代Web应用却常常力不从心。很多网站的内容是通过JavaScript动态渲染的,直接拿到的HTML源码里空空如也。付费平台更是会设置登录墙、验证码、点击劫持等反爬手段。

Selenium的核心优势在于它通过WebDriver协议直接控制浏览器。你看到的就是浏览器真实渲染的页面,所有的JS动态加载、Ajax异步请求、CSS样式都已经执行完毕。这意味着你可以像真人一样操作:找到“登录”按钮的 xpath 并点击,在输入框里填入账号密码,等待页面跳转,再找到文档的容器元素。它处理的是“结果”,而不是“请求”,从而绕开了很多针对HTTP请求层的反爬策略。

当然,Selenium也有代价:资源消耗大(要启动一个完整的浏览器进程),速度相对较慢。但对于抓取需要复杂交互、频次不高、但准确性要求极高的付费文档场景,它是目前最稳定、最可靠的方案之一。

2.2 核心工具链搭建

工欲善其事,必先利其器。下面是我们需要用到的核心库和工具,我会说明每个的选择理由和安装要点。

  1. Selenium库 :通过 pip install selenium 安装。这是我们的主引擎。
  2. WebDriver :这是Selenium控制浏览器的桥梁。你需要根据你使用的浏览器版本下载对应的驱动。
    • ChromeDriver :最主流的选择,更新快,社区支持好。务必去 ChromeDriver官网 下载与你的Chrome浏览器 版本号完全一致 的驱动。将下载的 chromedriver.exe 放在项目目录下,或者将其路径添加到系统的环境变量 PATH 中。
    • GeckoDriver :用于Firefox。
    • 我个人推荐Chrome,因为其开发者工具(F12)对于元素定位和调试来说是最友好的。
  3. python-docx库 :用于创建和编辑Word文档。 pip install python-docx 。这个库提供了非常直观的API来添加段落、标题、表格和图片,能让我们轻松地将爬取的文本结构化地输出到 .docx 文件中。
  4. 辅助库 time selenium.webdriver.support.ui.WebDriverWait 用于等待页面加载; re (正则表达式)用于文本清洗。

注意:WebDriver的版本匹配是新手最容易出错的地方。如果版本不匹配,Selenium会报错无法启动浏览器。一个快速检查的方法是:打开Chrome,在地址栏输入 chrome://version/ ,查看第一行的“Google Chrome”版本号,然后下载对应版本的驱动。

2.3 环境配置与初步测试

安装好库和驱动后,写一个最简单的脚本测试环境是否通畅。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# 指定chromedriver的路径,如果已加入PATH则不需要
service = Service(executable_path='./chromedriver') # Windows可能是'./chromedriver.exe'
driver = webdriver.Chrome(service=service)

# 打开一个测试页面
driver.get("https://www.baidu.com")
print(driver.title) # 应该打印出“百度一下,你就知道”

# 等待几秒观察
import time
time.sleep(3)

# 关闭浏览器
driver.quit()

如果这段代码能成功打开百度页面并打印出标题,那么恭喜你,Selenium环境配置成功。如果报错,请检查:1)驱动路径是否正确;2)驱动版本是否与浏览器匹配;3)是否有其他Chrome进程未关闭。

3. 自动化抓取策略与核心代码解析

3.1 模拟登录:跨越第一道门槛

付费文档的第一道关卡就是登录。我们的脚本需要自动完成这个动作。

策略分析 :首先手动用浏览器打开目标网站,完成一次登录。然后利用浏览器的开发者工具(F12),在“元素”(Elements)面板里,找到用户名和密码输入框,以及登录按钮。查看它们的HTML属性,如 id name class ,或者更稳定的 xpath

实操代码示例

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def login(driver, username, password):
    """
    模拟登录函数
    """
    # 打开登录页
    login_url = "https://目标网站.com/login"
    driver.get(login_url)

    # 等待登录表单加载完成
    wait = WebDriverWait(driver, 10)
    # 假设用户名输入框的id是‘username’
    username_input = wait.until(EC.presence_of_element_located((By.ID, "username")))
    password_input = driver.find_element(By.ID, "password") # 密码框
    submit_button = driver.find_element(By.XPATH, "//button[@type='submit']") # 登录按钮

    # 输入凭据并提交
    username_input.send_keys(username)
    password_input.send_keys(password)
    submit_button.click()

    # 等待登录成功后的页面跳转,例如等待某个登录后才出现的元素
    try:
        wait.until(EC.presence_of_element_located((By.CLASS_NAME, "user-avatar")))
        print("登录成功!")
        return True
    except Exception as e:
        print("登录可能失败:", e)
        # 这里可以截图保存,方便调试
        driver.save_screenshot('login_failed.png')
        return False

关键点与避坑

  • 等待是金 :网络有延迟,页面加载需要时间。直接查找元素很可能因为元素还未加载而抛出 NoSuchElementException 。必须使用 WebDriverWait 配合 expected_conditions 进行显式等待。这是Selenium自动化稳定性的基石。
  • 选择定位器 :优先使用 id name ,因为它们通常是唯一的。其次是 class name css selector xpath 功能最强大但也最脆弱,因为页面结构微调就可能导致 xpath 失效。尽量使用相对路径和属性组合,避免使用绝对路径。
  • 验证登录 :提交后不能假设成功。一定要通过等待某个登录后特有的元素(如用户头像、用户名显示区域)的出现来判断登录状态。
  • 验证码处理 :如果遇到验证码,自动化难度剧增。可以考虑:1)识别简单的图形验证码(使用 pytesseract 等OCR库,但成功率不高);2)寻找接口漏洞(不推荐);3)手动处理一次并将 cookies 保存下来后续使用(针对固定会话)。对于严肃的付费平台,通常验证码是标配,这可能意味着完全自动化此步骤不可行,需要半自动化介入。

3.2 定位与抓取文档内容

登录成功后,我们需要导航到目标文档页面,并提取出核心的正文内容。

策略分析 :同样使用开发者工具分析文档页面的结构。正文通常包裹在一个具有特定 id class 的容器 div 中。我们的目标是获取这个容器内的所有文本,并尽可能保留段落结构。

实操代码示例

def fetch_document_content(driver, doc_url):
    """
    抓取指定URL文档的正文内容
    """
    driver.get(doc_url)
    # 等待文档主体内容加载。这里假设正文在一个id为‘content’的div里
    wait = WebDriverWait(driver, 15)
    # 有些内容可能是滚动加载的,这里我们假设初始加载已包含全部文本
    content_element = wait.until(EC.presence_of_element_located((By.ID, "content")))

    # 获取元素内的所有文本
    full_text = content_element.text
    print(f"抓取到文本长度:{len(full_text)}")

    # 但是,.text属性会丢失所有的段落格式,变成一个巨大的字符串。
    # 为了保留段落结构,我们可以获取内部的所有<p>标签
    paragraphs = content_element.find_elements(By.TAG_NAME, "p")
    paragraph_list = [p.text for p in paragraphs if p.text.strip()] # 过滤空段落

    # 如果没有<p>标签,可以尝试按换行符分割
    if not paragraph_list:
        print("未找到<p>标签,尝试按换行分割。")
        # .text 中的换行符可能是 \n
        paragraph_list = [line for line in full_text.split('\n') if line.strip()]

    return paragraph_list, full_text

关键点与避坑

  • .text vs .get_attribute(‘innerHTML’) .text 获取的是渲染后的纯文本,去掉了所有HTML标签,但合并了空格和换行,段落信息可能丢失。 .get_attribute(‘innerHTML’) 获取的是原始的HTML字符串,包含标签,便于解析结构,但也包含大量噪音。需要根据页面实际情况选择,或结合使用。
  • 动态加载与滚动 :很多现代网站为了性能,会采用“无限滚动”或“分页加载”。如果发现抓取的内容不完整,可能需要模拟滚动操作。可以使用 driver.execute_script(“window.scrollTo(0, document.body.scrollHeight);”) 来滚动到底部,触发更多内容加载,然后重复抓取。
  • 反爬应对 :即使使用Selenium,一些网站也能检测到自动化行为(例如,WebDriver会有一些特定的JavaScript变量)。可以尝试添加一些选项来“隐藏”自动化特征:
    from selenium.webdriver.chrome.options import Options
    options = Options()
    options.add_argument(“--disable-blink-features=AutomationControlled”)
    options.add_experimental_option(“excludeSwitches”, [“enable-automation”])
    options.add_experimental_option(‘useAutomationExtension’, False)
    driver = webdriver.Chrome(service=service, options=options)
    
    此外,在操作间随机加入 time.sleep(random.uniform(1, 3)) 可以模拟人类操作的不确定性,降低被封风险。但核心原则依然是:友好、低频。

3.3 内容清洗与格式化

抓取到的原始文本通常包含多余的空白符、广告词、无关的页眉页脚信息等,需要清洗。同时,我们要为生成结构化的Word文档做准备。

清洗策略

  1. 去除首尾空白 :使用 str.strip()
  2. 合并多余空行 :将连续多个换行符替换为一个或两个。
  3. 去除特定广告词 :如果页面有固定的广告语,可以用 str.replace() 将其替换为空。
  4. 提取核心标题 :通常可以从 <h1> 标签或页面 title 中提取,作为Word文档的标题。

格式化思路 :我们将清洗后的段落列表,视为Word文档中的自然段落。每个列表项就是一个 Paragraph 对象。

4. 使用python-docx生成结构化Word文档

抓取和清洗后的数据是半成品, python-docx 库能帮我们将其包装成专业的Word文档。

4.1 创建文档与设置基础样式

from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH

def create_word_document(paragraph_list, doc_title, output_filename='output.docx'):
    """
    将段落列表生成为Word文档
    """
    # 1. 创建文档对象
    doc = Document()

    # 2. 添加文档标题
    title = doc.add_heading(level=0) # 0级标题是最大的Title样式
    title_run = title.add_run(doc_title)
    title_run.font.size = Pt(22)
    title_run.font.bold = True
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # 3. 添加一个空行作为间隔
    doc.add_paragraph()

    # 4. 遍历段落列表,添加正文
    for para_text in paragraph_list:
        # 每个原始段落都作为Word的一个新段落
        p = doc.add_paragraph()
        # 设置段落格式:首行缩进2字符(约0.75厘米)
        p.paragraph_format.first_line_indent = Inches(0.75)
        # 设置行距为1.5倍
        p.paragraph_format.line_spacing = 1.5
        # 添加文本
        run = p.add_run(para_text)
        # 设置正文字体
        run.font.name = ‘宋体’
        run.font.size = Pt(12)

    # 5. 保存文档
    doc.save(output_filename)
    print(f"文档已成功保存至:{output_filename}")

4.2 高级格式与元素添加

python-docx 的功能远不止于此,你可以轻松地:

  • 添加子标题 :使用 add_heading(‘二级标题’, level=2)
  • 插入表格 :如果抓取到了表格数据,可以用 add_table(rows=, cols=) 创建,并填充数据。
  • 插入图片 :如果文档中有需要保存的图片,Selenium可以找到图片元素并获取其 src 属性,然后使用 requests 库下载,最后用 doc.add_picture(‘image_path’) 插入。
  • 设置页眉页脚 :通过 section = doc.sections[0] 获取节,然后操作 section.header section.footer
  • 添加分页符 doc.add_page_break()

5. 完整流程集成与异常处理

将上述模块组合起来,形成一个健壮的、可处理异常的主流程。

import time
from selenium.common.exceptions import TimeoutException, NoSuchElementException

def main():
    # 初始化浏览器驱动(带上隐藏自动化特征的选项)
    chrome_options = webdriver.ChromeOptions()
    chrome_options.add_argument(“--disable-blink-features=AutomationControlled”)
    prefs = {“profile.managed_default_content_settings.images”: 2} # 可选:不加载图片加速
    chrome_options.add_experimental_option(“prefs”, prefs)

    service = Service(‘./chromedriver’)
    driver = webdriver.Chrome(service=service, options=chrome_options)
    driver.implicitly_wait(10) # 设置隐式等待,为所有find_element操作设置默认等待时间

    try:
        # 1. 登录
        if not login(driver, “your_username”, “your_password”):
            print(“登录失败,程序终止。”)
            return

        # 给一点时间让会话稳定
        time.sleep(2)

        # 2. 目标文档URL列表
        doc_urls = [
            “https://目标网站.com/doc/123”,
            “https://目标网站.com/doc/456”,
            # ... 更多文档
        ]

        all_docs_data = [] # 用来存储每个文档的(标题, 段落列表)

        for i, url in enumerate(doc_urls):
            print(f”正在处理第{i+1}个文档:{url}“)
            try:
                # 3. 抓取内容
                paragraphs, full_text = fetch_document_content(driver, url)
                # 简单从URL或页面标题提取文档标题,这里用占位符
                doc_title = f”抓取文档_{i+1}“

                all_docs_data.append((doc_title, paragraphs))

                # 4. 每抓取一个,可以立即生成一个Word,也可以合并
                create_word_document(paragraphs, doc_title, f”doc_{i+1}.docx”)

                # 5. 友好延迟,避免请求过快
                time.sleep(random.uniform(5, 10))

            except TimeoutException:
                print(f”处理 {url} 时超时,可能页面未加载成功,跳过。”)
                driver.save_screenshot(f”timeout_{i}.png”)
                continue
            except NoSuchElementException:
                print(f”处理 {url} 时未找到关键元素,页面结构可能已变化,跳过。”)
                driver.save_screenshot(f”no_element_{i}.png”)
                continue
            except Exception as e:
                print(f”处理 {url} 时发生未知错误:{e}“)
                driver.save_screenshot(f”error_{i}.png”)
                continue

        # 6. (可选)将所有文档合并到一个Word中
        if all_docs_data:
            merged_doc = Document()
            for title, paras in all_docs_data:
                merged_doc.add_heading(title, level=1)
                for p in paras:
                    merged_doc.add_paragraph(p)
                merged_doc.add_page_break() # 每个文档后分页
            merged_doc.save(“merged_documents.docx”)
            print(“所有文档已合并保存。”)

    finally:
        # 无论是否出错,最终都要关闭浏览器
        print(“任务结束,关闭浏览器。”)
        driver.quit()

if __name__ == “__main__”:
    main()

6. 常见问题排查与实战心得

6.1 元素定位失败

这是最常遇到的问题,错误信息通常是 NoSuchElementException

  • 检查点1:等待是否充分? 99%的定位失败是因为元素还没加载出来。请将 find_element 操作包裹在 WebDriverWait 中。
  • 检查点2:定位器是否准确? 页面改版后, id class 可能变了。用开发者工具重新检查元素属性。尝试使用更灵活的 css selector ,如 driver.find_element(By.CSS_SELECTOR, “.content .main p”)
  • 检查点3:是否在正确的frame或window中? 如果元素位于 <iframe> 内,必须先使用 driver.switch_to.frame(frame_element_or_name) 切换到该frame下才能定位。
  • 检查点4:是否有弹窗遮挡? 突然出现的cookie同意框、广告弹窗会遮挡住目标元素。需要先定位并关闭这些弹窗。

6.2 页面加载缓慢或超时

  • 调整等待策略 WebDriverWait 的默认超时时间(上面代码中的10秒)可能不够。对于大型文档或慢速网络,可以延长到30秒甚至更长。
  • 优化浏览器设置 :通过 ChromeOptions 禁止加载图片、CSS甚至JavaScript( –blink-settings=imagesEnabled=false ),可以极大提升页面加载速度。但注意,禁止JS可能会导致页面功能不全。
  • 网络问题 :检查本地网络,或者考虑使用代理(需合规合法)。

6.3 被网站识别为爬虫

  • 添加User-Agent :虽然Selenium会使用真实的浏览器UA,但可以自定义。
    options.add_argument(‘user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 …’)
    
  • 使用更人性化的操作模式 :避免毫秒级精确的操作间隔。在关键操作(点击、翻页)之间加入随机延迟 time.sleep(random.uniform(1, 5))
  • 减少并发和频率 :严格控制爬取速度,不要在短时间内发起大量请求。最好在服务器负载较低的时段(如深夜)运行脚本。
  • 尊重robots.txt :虽然Selenium本身不遵守,但作为开发者,应该手动检查目标网站的 robots.txt 文件,避免抓取明确禁止的目录。这是基本的网络礼仪和法律风险规避。

6.4 抓取内容不完整或格式混乱

  • 处理动态加载 :对于滚动加载的内容,需要循环执行滚动和内容提取操作,直到没有新内容出现。
  • 精细化解析HTML结构 :不要只依赖 .text 。尝试使用 BeautifulSoup 解析 innerHTML ,可以更精确地提取特定 class div ,过滤掉“推荐阅读”、“相关文章”等噪音模块。
  • 处理富文本 :如果文档包含加粗、斜体、超链接, .text 会丢失这些信息。你需要解析 <strong> , <a> 等标签,并在生成Word时通过 python-docx run 对象相应地设置加粗、添加超链接属性。

6.5 个人实战心得

  1. 先手动,后自动 :在写任何代码之前,一定要手动在浏览器里完整地走一遍流程。理解每一步的交互,观察网络请求(F12 Network面板),这能帮你找到最有效的定位方式和潜在的坑。
  2. 模块化开发与增量测试 :不要试图一口气写完整个脚本。先写登录模块,测试通过;再写单个页面抓取模块,测试通过;最后整合和添加异常处理。每一步都打印日志或截图,方便调试。
  3. 保存中间状态 :在关键步骤(如登录后、抓取前)使用 driver.save_screenshot(‘step1.png’) 截图。当脚本在后台运行失败时,这些截图是唯一的“现场证据”。
  4. 准备好应对变化 :网站前端是经常变化的。你的脚本今天能跑,下个月可能就失效了。所以代码结构要清晰,定位器最好集中管理(比如放在一个配置字典里),方便后期维护更新。
  5. 伦理与法律是红线 :再次强调,这个技术用于学习研究和少量个人资料备份是合理的。切勿用于大规模商业抓取、侵犯版权、攻击服务器等非法用途。控制频率,友好爬取。技术本身无罪,但使用技术的人需要负责。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐