返回博客
指南
Robert SfichiLast updated on Apr 29, 20265 min read

使用 Selenium 进行网络抓取:Python 分步教程

使用 Selenium 进行网络抓取:Python 分步教程
简而言之:Selenium 允许您通过 Python 代码驱动真实浏览器,从而抓取大量使用 JavaScript 的网站。本教程将带您逐步了解每个阶段:安装 Selenium、配置 Chrome、定位和操作元素、处理等待和分页、导出干净的数据,以及通过代理、Selenium Grid 和基于 API 的替代方案来扩展您的抓取工具。

Selenium 是一个浏览器自动化框架,可通过代码控制真实的浏览器实例(Chrome、Firefox、Edge 等)。虽然它最初是为测试 Web 应用程序而构建的,但如今已成为基于 Selenium 进行 Web 爬取的最广泛使用的工具之一,特别是在那些需要通过 JavaScript 渲染所需内容的网站上。

如果您曾尝试使用 requestsBeautifulSoup 尝试过抓取单页应用或无限滚动信息流,你应该已经知道这个问题:下载的 HTML 只是一个空壳。实际数据是在 JavaScript 运行后加载的,而普通的 HTTP 客户端永远不会执行那个 JavaScript。Selenium 通过启动完整的浏览器来解决这个问题,它会像人类访客一样加载页面,然后让你通过编程方式访问生成的 DOM。

本教程涵盖了 Python 中 Selenium 网页抓取的每个实用步骤:环境配置、元素定位策略、等待动态内容、滚动、分页、数据导出、代理集成以及性能调优。完成本教程后,您将拥有一个可运行的端到端抓取工具,并能清晰了解在何种情况下 Selenium 是正确的选择,以及何时应选用更轻量级的替代方案。

什么是 Selenium 以及为何将其用于网页抓取?

Selenium 最初于 2004 年作为测试框架问世,此后历经多次重大版本迭代。如今,Selenium WebDriver 通过 W3C WebDriver 协议与浏览器通信,该标准化 API 已被所有主流浏览器厂商原生支持。 您使用 Python(或 Java、JavaScript、Ruby、C# 及其他多种语言)编写指令,WebDriver 会将这些指令转换为真实浏览器会话中的操作。浏览器会渲染 HTML、执行 JavaScript、应用 CSS 并发起网络请求,其行为与真人操作键盘时完全一致。

正是这一完整的渲染流程,使得 Selenium 在数据抓取方面极具价值。传统的 HTTP 库(如 requests 仅能获取服务器返回的原始 HTML 文档。如果页面依赖客户端 JavaScript 来填充产品列表、加载评论或组装仪表盘,您将无法获取这些数据。而 Selenium 则会等待 JavaScript 执行完毕,并为您提供完全渲染后的 DOM。

Selenium 还支持所有主流浏览器(Chrome、Firefox、Edge、Opera、Safari),并兼容多种操作系统。当目标网站的行为因浏览器引擎不同而有所差异时,这种跨浏览器支持就显得尤为重要。而且,由于 Selenium 模拟了真实用户的行为(点击、输入、滚动),它能够处理交互式工作流,而静态爬虫根本无法做到这一点。

其代价是资源消耗。运行完整浏览器需要消耗实际的 CPU 和内存,且速度明显慢于发送轻量级的 HTTP 请求。 Selenium 最初是作为测试工具设计的,因此其部分抽象机制(如 WebDriver 协议的开销、缺乏内置的自动等待功能)会给纯粹的爬取场景带来阻碍。尽管如此,其庞大的社区、详尽的文档以及广泛的语言支持,使其成为任何想要涉足基于浏览器的数据提取的人最稳妥的入门选择之一。本教程后续部分将探讨性能考量及替代方案。

何时应(或不应)选择 Selenium 作为爬取工具

当您需要抓取的页面需要执行 JavaScript 才能呈现内容时,Selenium 是最佳选择。例如:使用 React、Angular 或 Vue 构建的单页应用;需要登录表单才能访问的网站;带有无限滚动功能的页面;以及在初始页面加载后通过 AJAX 调用加载数据的仪表盘。

对于大规模爬取静态 HTML 页面,它并非最佳选择。对于此类任务, requests 搭配 BeautifulSoup 或 Scrapy 之类的框架会更快、更省内存,且更易于扩展。Scrapy 与 Selenium 的对比归根结底在于你是否真的需要浏览器。当你需要具备内置自动等待功能的现代异步 API 时,像 Playwright 和 Puppeteer 这样的新型浏览器自动化库也值得考虑。

以下是快速决策清单:

  • 静态 HTML,无需 JS:使用 requests + BeautifulSoup。
  • 大规模爬取静态页面:使用 Scrapy。
  • JS渲染页面,多语言团队:使用Selenium。
  • JS渲染页面,仅限Python/JS,且需要现代API:考虑使用Playwright

一条实用的经验法则:从能正常工作的最轻量级工具开始。如果 requests 能获取所需数据,就停在这里。如果内容是 JavaScript 渲染的,且你希望获得广泛的语言支持以及成熟的生态系统,使用 Selenium 进行网页抓取是一个稳妥的折中方案。

先决条件与环境配置

在编写任何爬取代码之前,你需要安装三样东西:Python 3.8 或更高版本、Selenium 包,以及与你的浏览器版本匹配的浏览器驱动程序。

安装 Python 和 Selenium

打开终端并确认 Python 版本:

python --version

然后通过 pip 安装 Selenium:

pip install selenium

从 4.6 版本开始,Selenium 自带了一个名为 selenium-manager 的内置工具,它能自动为您下载并配置正确的浏览器驱动程序。 在早期版本中,您需要手动下载 ChromeDriver(或适用于 Firefox 的 GeckoDriver),并将其添加到系统 PATH 环境变量中。如果您使用的是 Selenium 4.6 或更高版本,在大多数情况下可以省去手动管理驱动程序的步骤,但若遇到启动错误,仍建议核对 Chrome 与 ChromeDriver 的版本是否一致。

验证安装

编写一个简短的测试脚本以确认一切正常:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://example.com")
print(driver.title)  # Should print "Example Domain"
driver.quit()

如果 Chrome 窗口打开、加载页面并将标题输出到控制台,说明您的环境已准备就绪。否则,请检查 Chrome 是否已安装,以及您的 Selenium 版本是否与计算机上的 Chrome 主版本号一致。

虚拟环境

建议在虚拟环境中工作,以避免 Selenium 及其依赖项与其他项目发生冲突:

python -m venv scraper-env
source scraper-env/bin/activate   # On Windows: scraper-env\Scripts\activate
pip install selenium beautifulsoup4 pandas

当 Selenium 获取渲染后的页面源代码后,BeautifulSoup 会负责快速解析 HTML,而 pandas 则能轻松完成数据清洗和 CSV 导出。这两者虽非必需,但在实际使用 Selenium Python 进行网页抓取的工作流中频繁出现,因此从一开始就安装它们可以节省时间。

至此,您的环境已准备就绪。下一步是配置 Chrome,使其在抓取过程中按您期望的方式运行。

配置 Chrome 选项以进行抓取

Selenium 会以默认设置启动 Chrome,但这些默认设置并不适合爬取。 ChromeOptions 类允许您在浏览器会话启动前进行自定义配置。

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")          # Run without a visible window
options.add_argument("--disable-gpu")           # Prevents GPU-related issues in headless
options.add_argument("--window-size=1920,1080") # Consistent viewport for element visibility
options.add_argument("--no-sandbox")            # Required in some CI/Docker environments
options.add_argument("--disable-dev-shm-usage") # Avoids shared memory issues in containers
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                     "AppleWebKit/537.36 (KHTML, like Gecko) "
                     "Chrome/125.0.0.0 Safari/537.36")

driver = webdriver.Chrome(options=options)

无头模式可在不显示图形界面的情况下运行 Chrome。这能降低资源消耗并加快执行速度,当你在服务器或 CI 管道中运行爬虫时,这一点尤为重要。 --headless=new 标志取代了旧版 --headless 标志,并提供了与有头模式更佳的功能对等性。关于无头浏览器的更多背景及其适用场景,一份关于无头浏览器架构的专门入门指南将是一个有用的参考。

设置自定义用户代理字符串有助于让您的请求混入常规浏览器流量中,而非暴露其来自自动化工具。结合使用逼真的窗口尺寸,可确保响应式布局呈现出您期望解析的页面桌面版本。

其他有用的标志包括 --disable-extensions (跳过加载会增加启动时间的扩展程序), --disable-infobars (隐藏“Chrome 正在被控制”横幅),以及 --disable-notifications (屏蔽可能遮挡元素的弹出窗口)。这些标志共同构建了一个精简的浏览器配置文件,其优化重点在于数据提取而非交互式浏览。

启动浏览器并访问 URL

配置好选项后,启动浏览器并访问页面只需两行代码:

driver = webdriver.Chrome(options=options)
driver.get("https://books.toscrape.com/")

driver.get() 阻塞直到浏览器触发 load 事件触发,这意味着初始的 HTML、CSS 和同步 JavaScript 已加载完毕。它不会等待在 load 事件之后触发的 AJAX 请求,因此您可能仍需显式等待动态注入的内容(详见下文的等待部分)。

导航完成后可立即使用的几个有用属性:

print(driver.title)        # Page <title> text
print(driver.current_url)  # Final URL after any redirects

操作完成后,请务必调用 driver.quit() ,无论是通过 finally 块中,还是通过将 Selenium 用作上下文管理器。让浏览器进程持续运行会导致内存快速泄漏,尤其是在循环中。一个简单的模式如下:

try:
    driver = webdriver.Chrome(options=options)
    driver.get("https://example.com")
    # ... scraping logic ...
finally:
    driver.quit()

这可确保即使代码中途抛出异常,浏览器也能正常关闭。尽早熟悉此类资源管理,可避免生产环境中孤立的 Chrome 进程消耗服务器内存。在开发机上,请定期检查任务管理器,确认没有过期的 chromechromedriver 进程。

定位页面上的元素

查找元素是任何 Selenium 爬取教程的核心。Selenium 提供了两种主要方法: find_element() (返回第一个匹配项) 和 find_elements() (返回所有匹配项的列表),二者均支持 By 定位策略。

常见的定位策略

from selenium.webdriver.common.by import By

# By ID (fastest, most reliable when available)
driver.find_element(By.ID, "search-input")

# By class name
driver.find_elements(By.CLASS_NAME, "product-card")

# By CSS selector
driver.find_element(By.CSS_SELECTOR, "div.results > a.item-link")

# By XPath
driver.find_element(By.XPATH, "//table[@id='data']//tr")

# By tag name
driver.find_elements(By.TAG_NAME, "tr")

# By name attribute
driver.find_element(By.NAME, "email")

基于 ID 的定位器通常速度最快,因为浏览器无需遍历 DOM 树即可直接定位到元素。如果目标元素没有 ID,对于大多数用例而言,CSS 选择器是次优选择:它们简洁、易读且支持广泛。

XPath 与 CSS 选择器

当需要向上遍历 DOM(父节点轴)、根据文本内容匹配或使用复杂谓词时,XPath 表现尤为出色。例如, //div[contains(text(), 'Price')] 在 XPath 中很容易实现,但在 CSS 中没有直接的等效写法。对于像 div.card > h3.title。当 CSS 无法表达所需的关联关系时,请使用 XPath;其他情况则坚持使用 CSS。如果您正在为复杂的页面结构构建选择器,深入了解 XPath 与 CSS 选择器之间的权衡取舍非常值得。

find_element 与 find_elements

find_element() 若无匹配项,则抛出 NoSuchElementException 异常,而 find_elements() 则返回一个空列表。在抓取数据时, find_elements() 通常更安全,因为您可以在处理前检查列表长度,从而避免因元素缺失而需要使用 try/except 代码块。

cards = driver.find_elements(By.CSS_SELECTOR, ".product-card")
if cards:
    for card in cards:
        title = card.find_element(By.CSS_SELECTOR, "h3").text
        price = card.find_element(By.CSS_SELECTOR, ".price").text
        print(title, price)

请注意,你可以将 find_element 调用(不仅限于驱动程序)。这将搜索范围限定在该元素的子树中,当遍历卡片或表格行等重复结构时,这种方式既更快又更精确。

Selenium 查找元素的网页抓取工作流本质上是:确定定位策略,在浏览器的开发者工具控制台中进行测试,然后将其编码到 Python 脚本中。Chrome 开发者工具允许你使用 $$() ,并使用 $x() ,这大大加快了选择器的开发速度。

与页面元素交互

一旦定位到某个元素,Selenium 便提供了一系列方法,让你能够像人类用户一样对其进行操作。WebElement 对象提供了点击、输入、读取文本以及获取属性值等操作。

点击、输入和读取

from selenium.webdriver.common.keys import Keys

# Click a button
driver.find_element(By.ID, "load-more").click()

# Type into an input field
search_box = driver.find_element(By.NAME, "q")
search_box.clear()
search_box.send_keys("web scraping with selenium")
search_box.send_keys(Keys.RETURN)

# Read text content
heading = driver.find_element(By.TAG_NAME, "h1").text

# Read an attribute value
link = driver.find_element(By.CSS_SELECTOR, "a.detail-link")
href = link.get_attribute("href")

`text` .text 属性返回元素的可见文本,而 .get_attribute() 方法则用于读取任何 HTML 属性(如 href、src、data-* 等)。这两个方法是您在元素级别进行数据提取的主要工具。

下拉菜单的操作

针对 <select> 元素,Selenium 提供了一个便捷类:

from selenium.webdriver.support.ui import Select

dropdown = Select(driver.find_element(By.ID, "sort-by"))
dropdown.select_by_visible_text("Price: Low to High")

您还可以通过值(select_by_value("price_asc"))或基于零的索引(select_by_index(2))进行选择。现代 Web 应用程序越来越多地使用自定义下拉组件,而非原生 <select> 元素,此时你需要先点击下拉触发器,再点击目标选项,将其作为独立的 find_elementclick 调用。

动作链

API ActionChains API 允许您组合复杂的交互操作,例如悬停、拖放和右键点击:

from selenium.webdriver.common.action_chains import ActionChains

menu = driver.find_element(By.ID, "mega-menu")
ActionChains(driver).move_to_element(menu).perform()

这对于抓取巨型菜单、工具提示以及其他仅在悬停时显示的元素非常有用。您可以在调用 .perform() 之前串联多个操作,使其按顺序执行。

这些交互方法均会等待元素出现在 DOM 中,但不一定要求其可见或可点击。将其与显式等待(下文将介绍)结合使用,可避免在异步加载内容的页面上出现竞争条件。

等待策略:隐式、显式和流畅等待

动态页面会异步加载内容,而 Selenium 网页抓取中最常见的错误之一,就是试图定位尚未存在于 DOM 中的元素。硬编码 time.sleep(5) 在技术上可行,但在加载速度快的页面上会浪费时间,而在加载速度慢的页面上则会失败。Selenium 提供了三种更智能的替代方案。

隐式等待

隐式等待会指示驱动程序在抛出异常前轮询 DOM 指定秒数 NoSuchElementException:

driver.implicitly_wait(10)  # Applies globally to all find_element calls

这种方式设置简单,但会应用于每次查找,这可能掩盖性能问题,并在元素确实不存在时拖慢爬虫速度。此外,您无法自定义条件:它仅检查元素是否存在,而不检查其可见性或可点击性。

显式等待

显式等待是 Selenium 爬虫教程中推荐的最佳实践方法。您需指定一个条件和超时时间,驱动程序将持续轮询直至条件满足:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 15).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".results-container"))
)

常见条件包括:

  • presence_of_element_located: 元素存在于 DOM 中(可能处于隐藏状态)
  • visibility_of_element_located: 元素既存在又可见
  • element_to_be_clickable: 元素可见且处于启用状态
  • text_to_be_present_in_element: 元素包含特定文本
  • staleness_of: 元素已不再属于 DOM(在导航之后特别有用)

这些选项让您能够精准等待所需条件,绝不冗余,从而在不牺牲可靠性的前提下保持爬虫的高效运行。

流畅等待

流畅等待是一种经过额外调优的显式等待:您可以设置轮询间隔,并指定轮询期间忽略哪些异常。

from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException

wait = WebDriverWait(
    driver, timeout=20, poll_frequency=0.5,
    ignored_exceptions=[NoSuchElementException]
)
element = wait.until(
    EC.presence_of_element_located((By.ID, "dynamic-table"))
)

当默认的 500 毫秒轮询间隔对于受速率限制或加载缓慢的目标而言过于频繁时,请使用流畅等待。在大多数使用 Selenium 进行网页抓取的场景中,标准的显式等待 WebDriverWait 已能满足需求。关键要点在于:始终优先使用 WebDriverWait 而非 time.sleep()。它既更快,也更可靠。

执行 JavaScript 和滚动技巧

某些交互操作通过直接执行 JavaScript 会更简单(或仅能通过此方式实现)。Selenium 的 execute_script() 方法可在浏览器环境中运行任意 JavaScript 代码片段,并将结果返回给您的 Python 代码。

基本脚本执行

page_height = driver.execute_script("return document.body.scrollHeight;")
print(f"Total page height: {page_height}px")

您可以将 Python 对象作为参数传递给脚本,并在 JavaScript 字符串中通过 arguments[0], arguments[1]等在 JavaScript 字符串中引用它们。返回值会自动转换为对应的 Python 类型(字典、列表、字符串、数字)。

滚动至页面底部

在网页抓取中,最常见的用例是无限滚动。以下是一个适用于 Selenium 抓取动态网站场景的可靠循环模式:

import time

last_height = driver.execute_script("return document.body.scrollHeight")

while True:
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(2)  # Allow content to load
    new_height = driver.execute_script("return document.body.scrollHeight")
    if new_height == last_height:
        break
    last_height = new_height

该循环会比较每次滚动前后的滚动高度。当高度停止变化时,说明没有新内容加载,循环即退出。对于持续加载内容的页面,您可能需要添加最大迭代次数限制,以避免陷入无限循环。

滚动至特定元素

有时您需要将特定元素拉入视口(例如延迟加载的图片或“加载更多”按钮):

element = driver.find_element(By.ID, "footer-section")
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth'});", element)

触发隐藏操作

JavaScript 执行还可用于点击被覆盖层遮挡的元素、移除干扰截图的固定头部,或提取嵌入在 JavaScript 变量中的数据:

data = driver.execute_script("return window.__INITIAL_STATE__;")

如果网站将结构化数据存储在全局 JS 对象中(在 React 和 Next.js 应用中很常见),直接获取数据比解析渲染后的 DOM 更快。通过 execute_script,无需任何 HTML 解析即可立即进行处理。

用于调试的截图

当爬虫无提示失败或返回意外结果时,失败瞬间的页面状态截图将极具价值。

driver.save_screenshot("debug_screenshot.png")

您还可以捕获特定元素:

element = driver.find_element(By.ID, "captcha-container")
element.screenshot("captcha_element.png")

在错误处理块中保存截图,以便在出现问题时直观检查浏览器当时渲染的内容。在无头模式下,这一点尤为重要,因为没有可视窗口可供查看。将截图与 driver.current_urldriver.page_source[:500] 日志记录相结合,可获得完整的调试快照。

对于长时间运行的爬取管道,建议在关键检查点(登录后、分页后、数据提取前)保存带时间戳的截图,以便精确定位运行偏离预期路径的位置。

跨页分页处理

大多数网站将数据分散在多个页面上,而生产级别的爬虫需要自动跟随这些分页链接。常见的模式有两种:URL 参数分页和基于点击的分页。

URL 参数分页

如果网站使用类似 ?page=2,您可以直接构造 URL:

all_results = []
for page_num in range(1, 50):
    driver.get(f"https://example.com/listings?page={page_num}")
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, ".listing-card"))
    )
    items = driver.find_elements(By.CSS_SELECTOR, ".listing-card")
    if not items:
        break  # No results means we passed the last page
    for item in items:
        all_results.append({
            "title": item.find_element(By.CSS_SELECTOR, "h2").text,
            "price": item.find_element(By.CSS_SELECTOR, ".price").text,
        })

if not items: break 检查是最简单的末页检测方法:当页面返回零条结果时,即停止抓取。另一种方法是检查当前页码是否超过分页控件中显示的总页数,或者查找“下一页”链接,并在该链接消失时停止抓取。

基于点击的分页

某些网站会在您点击“下一页”按钮时通过 JavaScript 加载下一页:

from selenium.common.exceptions import NoSuchElementException

all_results = []
while True:
    items = driver.find_elements(By.CSS_SELECTOR, ".listing-card")
    for item in items:
        all_results.append(item.find_element(By.CSS_SELECTOR, "h2").text)

    try:
        next_btn = driver.find_element(By.CSS_SELECTOR, "a.next-page")
        if "disabled" in next_btn.get_attribute("class"):
            break
        next_btn.click()
        WebDriverWait(driver, 10).until(EC.staleness_of(items[0]))
    except NoSuchElementException:
        break

staleness_of 该条件会等待旧元素从 DOM 中移除,以此确认新页面已加载完毕。通过检查“下一页”按钮是否带有“disabled”类,可优雅地处理最后一个页面。

这两种模式都需要明确的终止条件。若无此条件,您的爬虫要么陷入无限循环,要么在最后一页崩溃。请务必测试分页逻辑的边界情况:在最后一个页面会发生什么?

从多个 URL 抓取数据

当目标数据分布在不同的详情页面上时(例如,一个产品列表链接到各个产品页面),通常需要分两步进行抓取:先从列表中收集 URL,然后访问每个 URL。

# Pass 1: Collect detail URLs from the listing page
driver.get("https://example.com/catalog")
links = driver.find_elements(By.CSS_SELECTOR, "a.product-link")
detail_urls = [link.get_attribute("href") for link in links]

# Pass 2: Visit each detail URL and extract data
products = []
for url in detail_urls:
    try:
        driver.get(url)
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".product-detail"))
        )
        products.append({
            "name": driver.find_element(By.CSS_SELECTOR, "h1.product-name").text,
            "description": driver.find_element(By.CSS_SELECTOR, ".description").text,
            "url": url,
        })
    except Exception as e:
        print(f"Skipped {url}: {e}")
        continue

driver.quit()

整个过程中应复用同一个 WebDriver 实例。为每个 URL 启动新浏览器会浪费启动时间和内存。将内部循环包裹在 try/except 块中,可确保单个页面故障不会导致整个抓取任务崩溃。

这种主从模式是 Python Selenium 网站抓取工作流中最实用的方案之一。它与上一节介绍的分页循环配合使用,可爬取整个产品目录。首先收集分页列表中所有商品详情页的 URL,然后在第二轮中遍历这些详情页。

对于庞大的 URL 列表,建议在请求之间添加短暂延迟(0.5 至 2 秒),以遵守网站的速率限制,并降低触发反机器人防御机制的风险。您可以稍微随机化延迟时间,使请求模式更难被预测。

提取和解析 HTML 表格

表格数据是使用 Selenium 进行网页抓取时结构最清晰(因此也最容易处理)的目标之一。以下是一种适用于任何标准 HTML 表格的通用模式:

table = driver.find_element(By.CSS_SELECTOR, "table#stats")

# Extract headers
headers = [th.text for th in table.find_elements(By.CSS_SELECTOR, "thead th")]

# Extract rows
rows = []
for tr in table.find_elements(By.CSS_SELECTOR, "tbody tr"):
    cells = [td.text for td in tr.find_elements(By.TAG_NAME, "td")]
    rows.append(dict(zip(headers, cells)))

这将生成一个字典列表,其中每个键是列标题,每个值是对应的单元格文本。这种结构清晰且可预测,可直接接入您的导出管道。

如果表格较大,或者你计划进行进一步分析,将数据交由 pandas 处理会更高效:

import pandas as pd

html = driver.page_source
tables = pd.read_html(html, attrs={"id": "stats"})
df = tables[0]

pd.read_html 比手动遍历元素更能优雅地处理 colspan、rowspan 和嵌套表格。使用 Selenium 驱动的 page_source ,向其提供包含 JavaScript 注入数据的完整渲染 HTML。

请注意那些在滚动时动态加载行数据的表格。在这种情况下,您需要在提取行数据前滚动表格容器(而非仅滚动页面)以触发延迟加载。部分金融和分析网站采用虚拟化表格,仅渲染可见行,这要求您逐步滚动以捕获完整数据集。

结合 Selenium 与 BeautifulSoup 实现更快的解析

Selenium 在页面渲染方面表现出色,但其元素查找方法相对较慢,因为每次调用都会跨越 WebDriver 协议边界。一个常见的性能优化技巧是让 Selenium 负责渲染,然后将生成的 HTML 传递给 BeautifulSoup 进行解析。

from bs4 import BeautifulSoup

driver.get("https://example.com/products")
WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".product-grid"))
)

soup = BeautifulSoup(driver.page_source, "html.parser")
cards = soup.select(".product-card")

for card in cards:
    title = card.select_one("h3").get_text(strip=True)
    price = card.select_one(".price").get_text(strip=True)
    print(title, price)

这种 Selenium-BeautifulSoup 网页抓取模式兼顾了两者的优势:Selenium 执行 JavaScript 以确保 DOM 完整,而 BeautifulSoup 则在内存中解析静态 HTML 快照,无需任何网络往返。在包含数百个元素的页面上,速度差异尤为明显。

工作流很简单:使用 Selenium 进行导航和 JavaScript 执行,并在获取到 page_source。这将 WebDriver 的调用次数从数百次减少到一次。如果您是 BeautifulSoup 的新手,关于使用 BeautifulSoup 提取和解析网页数据的指南将是本教程的有用补充。

将抓取的数据导出为 CSV 和 JSON

保存在 Python 列表中的原始数据仅在脚本运行期间有用。您几乎总是希望将其持久化。

CSV 导出

import csv

fieldnames = ["title", "price", "url"]
with open("products.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(all_results)

JSON 导出

import json

with open("products.json", "w", encoding="utf-8") as f:
    json.dump(all_results, f, indent=2, ensure_ascii=False)

Pandas 快捷方式

如果您已使用 pandas,一条代码即可处理这两种格式:

import pandas as pd

df = pd.DataFrame(all_results)
df.to_csv("products.csv", index=False)
df.to_json("products.json", orient="records", indent=2)

当下游工具(如电子表格、SQL导入)期望接收扁平化的表格数据时,请选择 CSV 格式。当数据为嵌套结构,或需要保留数组和对象等数据类型时,请选择 JSON 格式。对于非常大的数据集,建议采用增量写入行数据的方式,而非先将所有数据累积在内存中。该 csv.DictWriter 方法天然支持增量写入,因为您可以在每行处理后进行刷新。

清理和验证抓取的数据

从浏览器抓取的数据极少能直接用于分析。重复行、缺失字段和格式不一致是常态,而非例外。

import pandas as pd

df = pd.DataFrame(all_results)

# Remove exact duplicate rows
df.drop_duplicates(inplace=True)

# Drop rows where critical fields are missing
df.dropna(subset=["title", "price"], inplace=True)

# Normalize price strings to floats
df["price"] = (df["price"]
               .str.replace(r"[^0-9.]", "", regex=True)
               .astype(float))

# Strip extra whitespace from text fields
df["title"] = df["title"].str.strip()

这一清理步骤弥合了原始抓取输出与真正适用于分析、报告或导入数据库的数据之间的差距。在导出前运行这些检查可及早发现问题,而非在后续处理中才发现数据错误。

值得添加的常见验证检查包括:确认 URL 格式正确、数值字段在预期范围内(例如产品价格为 $0.00 可能是解析错误),以及必填文本字段不是伪装成有效值的空字符串。构建一个在每次抓取会话后运行的小型验证函数,将使您的数据处理管道随着时间的推移变得更加健壮。

结合 Selenium 使用代理以避免被封禁

如果从单一 IP 地址发送过多请求,目标网站最终会封禁您。代理服务器可将流量分散到不同 IP 上,从而降低被封禁的风险,并能访问受地理限制的内容。

手动代理配置

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://123.45.67.89:8080")

driver = webdriver.Chrome(options=options)
driver.get("https://httpbin.org/ip")
print(driver.find_element(By.TAG_NAME, "body").text)
driver.quit()

此方法适用于单个代理,但手动轮换代理池(针对每个 IP 启动新的驱动程序)既繁琐又低效。数据中心代理虽然成本较低,但更容易被网站检测到;而住宅代理通过真实的消费者 IP 路由流量,因此更难与真实访客区分开来。

需认证的代理

许多代理服务商要求用户名/密码认证。Chrome 默认不支持通过命令行参数进行代理认证,因此需要采用变通方案。一种常见方法是使用轻量级浏览器扩展程序,该程序会自动将凭据注入代理握手过程中。另一种选择是运行本地代理(如 mitmproxy),由其添加认证头部信息并将其转发至远程代理。

何时使用托管代理服务

维护自己的代理池、处理轮换逻辑以及应对被封禁的 IP,这些运维负担会随着规模扩大而增加。托管代理服务为您提供一个单一入口点,在后台自动处理轮换、认证和 IP 健康状态。这对于生产级别的 Selenium 代理网络爬虫尤为有用,因为您需要数百个分布在不同地理区域的轮换 IP。

关于避免被封禁的建议,网络爬虫指南中关于规避IP封禁的策略同样适用于基于Selenium的配置。关键实践包括:在轮换IP的同时轮换用户代理、在请求间添加随机延迟,以及遵守 robots.txt 指令。

检测并规避蜜罐陷阱

蜜罐是一种隐藏的 HTML 元素,对人类访客不可见,但对机器人可见。如果您的爬虫与之交互(点击隐藏链接、填写隐藏表单字段),网站会将您的会话标记为自动化操作,并可能立即封禁您。

最常见的模式是使用 display:nonevisibility:hidden:

hidden_inputs = driver.find_elements(
    By.CSS_SELECTOR, "input[style*='display:none'], input[style*='visibility:hidden']"
)

for inp in hidden_inputs:
    print(f"Honeypot detected: name={inp.get_attribute('name')}")

针对蜜罐的防御性编码规则:

  • 切勿盲目遍历所有表单字段并进行填写。请先检查其可见性。
  • element.is_displayed() 来处理任何未明确指定的元素。
  • 如果链接的 opacity: 0 属性,或通过负坐标定位在屏幕外,请跳过该链接。
  • 某些蜜罐使用 CSS 类而非内联样式。若怀疑网站使用外部 CSS 隐藏陷阱元素,请使用 execute_script 检查计算后的样式。

当您的爬虫需要填写表单(搜索输入框、登录字段、联系表单)时,识别蜜罐就显得尤为重要。在每次交互前进行简单的可见性检查,不仅开销极小,还能避免一类原本难以调试的检测问题。

性能优化技巧

Selenium 驱动的是完整的浏览器,因此任何能减少浏览器工作量的优化,都会直接转化为更快、更经济的抓取运行。

以无头模式运行。移除 GUI 层可显著缩短启动时间并降低内存占用。在 Chrome 设置中使用 --headless=new 在 Chrome 选项中启用。无头浏览器指南对此有深入详尽的说明。

屏蔽非必要资源。图片、字体和 CSS 文件会增加加载时间却不提供有效数据。您可以使用 Chrome DevTools Protocol 命令来屏蔽它们:

driver.execute_cdp_cmd("Network.setBlockedURLs", {
    "urls": ["*.jpg", "*.png", "*.gif", "*.svg", "*.woff2", "*.css"]
})
driver.execute_cdp_cmd("Network.enable", {})

优先使用快速定位器。基于 ID 的查找速度最快。遍历大型子树的复杂 XPath 表达式会消耗更多资源。在有选择的情况下,请使用 By.ID 或简短的 CSS 选择器。

优化等待策略。过长的隐式等待会拖慢每个元素的查找速度。请结合 WebDriverWait 并根据具体页面的行为设置超时,而非采用一刀切的 30 秒默认值。

尽量减少浏览器重启。尽可能在不同页面间复用同一个驱动程序实例。每次 webdriver.Chrome() 调用都会生成一个完整的浏览器进程。

禁用不需要的功能。--disable-extensions, --disable-infobars等标志,并 --blink-settings=imagesEnabled=false 可进一步降低浏览器开销。

页面加载策略。设置 options.page_load_strategy = "eager" 可停止等待图片和样式表加载完成。DOM 能更早进入交互状态,从而可更早开始数据提取。

这些优化效果具有叠加效应。与默认配置相比,综合应用所有这些优化措施可将 Selenium 无头抓取任务的运行时间缩短 50% 或更多。

常见挑战及解决方法

即使配置得当,您仍会遇到问题。以下是最常见的问题及其解决方案。

挑战

原因

解决方案

NoSuchElementException

元素尚未加载

使用 WebDriverWait 并指定适当的预期条件

元素引用已过期

定位元素后 DOM 已发生变化

在任何页面导航或 AJAX 刷新后重新定位元素

验证码(CAPTCHA)拦截

网站检测到自动化流量

降低请求频率、轮换用户代理、使用住宅代理

数据不一致

页面布局因产品/类别而异

添加防御性检查,包括 find_elements 并在访问前测试列表长度

长时间运行中的内存泄漏

浏览器在浏览数百个页面后会累积状态

定期重启驱动程序(每 N 页一次)或清除 Cookie/缓存

执行缓慢

完整的浏览器渲染开销

应用上一节中的优化建议

错误处理模式:将爬取循环包裹在 try/except 语句中,捕获 WebDriverException 作为广泛的备用方案。记录 URL、保存屏幕截图,并继续处理下一个项目,而不是导致整个任务崩溃。

from selenium.common.exceptions import WebDriverException

for url in urls:
    try:
        driver.get(url)
        # ... extraction logic ...
    except WebDriverException as e:
        driver.save_screenshot(f"error_{url.split('/')[-1]}.png")
        print(f"Failed on {url}: {e}")
        continue

如果某个通常能正常运行的网站出现间歇性失败,请检查该网站是否会根据地理位置、用户代理或时间段提供不同的内容。在失败时记录完整的页面源代码(除了截图之外)有助于诊断此类环境问题。若要使用 Selenium 绕过 Cloudflare 及类似的防护服务,需要采用超出基本配置范围的额外技术。

使用 Selenium Grid 实现扩展

单个 Selenium 实例每次仅限运行一个浏览器。当您需要并行抓取数千个页面时,Selenium Grid 会将浏览器会话分布到多台机器上。

Grid的工作原理

Selenium Grid 采用中心-节点(hub-and-node)架构。中心(hub)作为中央服务器,接收 WebDriver 请求并将其路由至可用节点,每个节点运行一个或多个浏览器实例。您只需将脚本指向中心 URL,而非本地驱动程序:

from selenium import webdriver

options = webdriver.ChromeOptions()
driver = webdriver.Remote(
    command_executor="http://grid-hub:4444/wd/hub",
    options=options
)

您可以使用 Docker 运行中心和节点,这使得扩展变得非常简单:

docker run -d -p 4444:4444 selenium/hub
docker run -d --link selenium-hub:hub selenium/node-chrome

实际注意事项

运行数十个并行浏览器会话会消耗大量 CPU 和内存。请监控节点并设置最大会话数上限,以防止系统过载。在分布式环境中,由于截图和日志位于远程机器上,调试也会变得更加困难。请使用 Grid 的内置视频录制功能或集中式日志记录来保持可视性。

对于需要中等并行度(5 到 20 个并发会话)的 Selenium Grid 网络爬虫工作负载,Selenium Grid 是一个可靠的选择。超过这个范围,管理节点、处理故障以及监控资源使用情况的运维负担会迅速增加。对于不希望管理 Grid 基础设施的团队,云托管的浏览器服务或基于 API 的爬虫工具可以作为托管服务,提供相同的并行执行模型。

Selenium、Playwright 与 Puppeteer:快速对比

Selenium 并非唯一的浏览器自动化工具。Playwright 和 Puppeteer 是广受欢迎的现代替代方案,各具优势。

功能

Selenium

Playwright

Puppeteer

支持的编程语言

Python、Java、C#、JS、Ruby

Python、Java、.NET、JS/TS

仅限 JavaScript/TypeScript

浏览器引擎

Chrome、Firefox、Edge、Safari

Chromium、Firefox、WebKit

仅限 Chromium

自动等待

手动(显式/隐式等待)

操作上的内置自动等待

手动(waitForSelector)

速度

较慢(WebDriver协议)

更快(CDP / 浏览器通道)

更快(CDP)

并行执行

通过 Selenium Grid

原生浏览器上下文

通过页面/浏览器上下文

社区

规模最大、最成熟

发展迅速

规模庞大(以Chromium为核心)

若您需要多语言支持、跨浏览器测试兼容性,或团队已熟悉 Selenium API,请选择 Selenium。若您希望通过单一库获得内置自动等待、现代异步模式及多浏览器支持,请选择 Playwright。若您的技术栈仅限 JavaScript 且仅需 Chromium 支持,请选择 Puppeteer。

这三种工具均可处理基于浏览器的数据抓取,但对于无需 Selenium 广泛语言生态系统的新项目而言,Playwright 内置的自动等待功能以及支持并行处理的原生浏览器上下文使其更具优势。若需深入了解 Playwright 的数据抓取能力,Playwright 网络抓取相关资源提供了详尽的对比分析。

无需浏览器渲染 JavaScript:基于 API 的替代方案

运行完整浏览器虽可行,但在 CPU、内存及工程维护方面成本较高。若您的目标仅是获取一个 JavaScript 密集型页面的渲染后 HTML,基于 API 的方法将高效得多。

托管式抓取 API 接受一个 URL 并返回完全渲染的 HTML(甚至可能是预解析的 JSON)。在后台,它们会处理浏览器渲染、代理轮换、验证码破解和重试逻辑。您只需发送一个 HTTP 请求,即可获得干净的数据,这意味着您的抓取代码可以简单到仅需一个 requests.get() 调用一样简单。

这种模式在以下场景中表现尤为出色:

  • 您需要进行大规模抓取,且不愿管理 Selenium Grid 基础设施。
  • 反机器人系统非常严密,需要住宅代理轮换以及浏览器指纹管理。
  • 您希望将解析逻辑与渲染层完全解耦。
  • 您的团队缺乏跨环境维护浏览器和驱动程序版本的 DevOps 能力。

其取舍在于控制权。使用 Selenium,您可以点击按钮、填写表单并执行多步骤工作流。基于 API 的渲染器通常处理单页抓取。对于复杂的交互式抓取(登录流程、多步骤向导),Selenium 或类似的浏览器自动化工具仍是更合适的选择。

混合方案在实践中效果良好:开发阶段在本地使用 Selenium 来理解页面结构并构建选择器,然后在生产环境中切换到基于 API 的服务,以避免大规模运行浏览器带来的运维开销。这既能在原型设计阶段提供 Selenium 网页抓取的灵活性,又能在生产环境中提供托管服务的可靠性。无论哪种方式,您都保持相同的解析代码;仅数据获取层发生变化。

完整示例:基于 Selenium 的端到端网页抓取项目

让我们将所有内容整合成一个可运行的脚本。本示例从一个分页演示网站抓取图书标题和价格,对数据进行清洗,并导出为 CSV 文件。

import csv
import time
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

# --- Setup ---
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=options)

all_books = []
base_url = "https://books.toscrape.com/catalogue/page-{}.html"

# --- Scrape with Pagination ---
for page in range(1, 51):
    driver.get(base_url.format(page))
    try:
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".product_pod"))
        )
    except Exception:
        break  # No more pages

    books = driver.find_elements(By.CSS_SELECTOR, ".product_pod")
    if not books:
        break

    for book in books:
        title = book.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("title")
        price = book.find_element(By.CSS_SELECTOR, ".price_color").text
        all_books.append({"title": title, "price": price})

driver.quit()

# --- Clean ---
seen = set()
cleaned = []
for book in all_books:
    key = (book["title"], book["price"])
    if key not in seen:
        seen.add(key)
        price_str = book["price"].replace("\xa3", "").strip()
        try:
            book["price"] = float(price_str)
        except ValueError:
            continue
        cleaned.append(book)

# --- Export ---
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price"])
    writer.writeheader()
    writer.writerows(cleaned)

print(f"Scraped {len(cleaned)} books to books.csv")

该脚本演示了本教程涵盖的完整生命周期:无头 Chrome 配置、显式等待、使用 CSS 选择器定位元素、带末页检测的分页处理、去重、数据类型标准化以及 CSV 导出。您只需替换 URL 模式和 CSS 选择器,即可将其适配至任何目标网站。

本示例中的关键设计决策值得关注。我们使用 --headless=new 以提升速度。我们将等待操作封装在 try/except 中,以便优雅地处理“页面未找到”的情况。我们通过在集合中追踪已获取的书名/价格对来实现去重。并且我们在导出前将价格标准化为浮点数,以便下游工具能够进行数值排序和过滤。

在此基础上,您可以扩展此脚本,例如添加代理轮换、将结果写入数据库而非 CSV,或使用 cron 或 Airflow 等任务调度器按计划部署。您在本教程中掌握的这些模式,就像积木一样可以灵活组合。

关键要点

  • 当需要 JavaScript 渲染时,请使用 Selenium。对于静态 HTML 页面,像 requests 配合 BeautifulSoup 或 Scrapy 运行,速度更快且成本更低。
  • 始终使用显式等待,而非 time.sleep(). WebDriverWait 配合 expected_conditions 能让您的爬虫在动态页面上运行得更快、更可靠。
  • 屏蔽不必要的资源并采用无头模式运行。在无头模式下禁用图片、字体和 CSS,可在不丢失任何抓取数据的情况下将执行时间缩短一半。
  • 导出前清理数据。去重、空值处理和类型标准化能及早发现问题,并节省后续调试时间。
  • 从一开始就规划扩展性。代理轮换、Selenium Grid 以及基于 API 的渲染方案,可确保您的爬虫在目标网站加强防御时仍能正常运行。

常见问题

Selenium 适合大规模网页抓取吗?

虽然可行,但资源消耗巨大。每个浏览器实例都会消耗大量 CPU 和内存,因此运行数百个并行会话需要强大的基础设施支持。Selenium Grid 有助于在多台机器间分担负载,而无头模式则能降低单个会话的开销。对于真正的大规模任务(数百万页面),基于 API 的渲染服务或专为爬虫设计的框架通常更具成本效益。

Selenium 能否抓取需要登录认证的网站?

可以。您可以自动化整个登录流程:导航至登录页面,使用 find_element定位用户名和密码字段,使用 send_keys,并点击提交按钮。认证通过后,Selenium 会保留会话 Cookie,确保后续页面加载在整个会话期间保持认证状态。

使用 Selenium 抓取时如何处理 CAPTCHA?

验证码(CAPTCHA)的设计初衷就是阻止自动化操作,因此浏览器本身并无完美的解决方法。常见的策略包括:降低请求频率以避免触发验证码;使用住宅代理来降低被检测的风险;以及集成第三方验证码破解服务,这些服务会返回令牌,您可以通过 JavaScript 将其注入页面。

使用 Selenium 进行网页抓取是否合法?

合法性取决于管辖区域、网站的服务条款以及所收集数据的类型。在美国,最高法院hiQ 诉 LinkedIn 一案中的裁决明确指出,抓取公开数据并不必然违反《计算机欺诈与滥用法案》(CFAA)。但在欧盟,抓取个人数据可能涉及《通用数据保护条例》(GDPR)。请务必查阅目标网站的 robots.txt 及服务条款,并就商业抓取项目咨询法律顾问。

Selenium 和 BeautifulSoup 有什么区别?

它们解决的问题不同。Selenium 控制真实浏览器,能够执行 JavaScript、点击按钮并浏览交互式页面。BeautifulSoup 是一个解析器,它接收 HTML 字符串并提供方法来查询和提取数据,但无法加载页面或运行 JavaScript。许多爬虫工具会结合使用这两者:Selenium 渲染页面,然后 BeautifulSoup 解析 HTML 快照以实现更快的数据提取。

结论

使用 Selenium 进行网页抓取,使您能够从几乎任何网站中提取数据,包括静态 HTTP 库无法处理的、大量使用 JavaScript 的应用程序。在本教程中,您已经了解了如何设置环境、配置浏览器、定位和操作元素、通过显式等待处理动态内容、在结果集之间分页,以及导出干净的数据。

Selenium 的主要权衡在于资源消耗。真实的浏览器比轻量级的 HTTP 请求消耗更多的 CPU 和内存,而这种消耗在扩展时会成倍增加。对于许多爬取场景而言,切实可行的方案是:开发和原型设计阶段使用 Selenium,而在投入生产环境时,将渲染和反机器人检测的复杂工作转移给专用服务。

如果您发现自己花在管理代理、应对验证码以及维护浏览器基础设施上的时间,比编写实际的解析逻辑还要多,WebScrapingAPI 可以为您处理渲染和交付层,让您能够专注于数据本身。这种关注点的分离能让您的代码库保持整洁,并确保爬取流程的可靠性。

无论您选择何种工具,都应从最轻量且符合您用例的解决方案开始,仅在必要时增加复杂度,并始终遵守目标网站的服务条款。

关于作者
Robert Sfichi, 全栈开发工程师 @ WebScrapingAPI
Robert Sfichi全栈开发工程师

罗伯特·斯菲奇是 WebScrapingAPI 的团队成员,致力于产品开发,并协助构建可靠的解决方案,以支持该平台及其用户。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。