Selenium 驱动真实浏览器。这就是它作为爬虫存在的全部意义:当页面在初始 HTML 加载后通过 JavaScript 动态构建内容时,普通 HTTP 请求只会返回一个空壳,而浏览器会运行脚本并呈现完整的 DOM。代价是您现在需要为每个页面运行 Chrome,这比获取 HTML 更慢、更消耗资源。本指南基于当前工具链(Selenium 4)构建一个可运行的 Selenium 爬虫,然后说明什么情况下自己驱动浏览器变得不划算。

版本差异在这里很重要。大多数 Selenium 教程仍在讲授 Selenium 3 的模式:find_element_by_id、手动下载 ChromeDriver 二进制文件、executable_path 参数。所有这些都已被移除或弃用。Selenium 4 用单一的 By 定位器 API 替换了所有查找辅助方法,Selenium Manager(自 4.6 版起内置)现在可以自动解析与您已安装浏览器匹配的驱动,因此大多数指南开头的驱动下载步骤已经消失。以下所有内容均针对当前设置编写。

您需要什么

三样东西:Python 3.8 或更高版本、已安装的 Google Chrome,以及 Selenium 包。就这些。您不需要下载 ChromeDriver,也不需要在当前安装上使用 webdriver-manager,因为 Selenium Manager 会为您处理驱动。

bash
# Selenium 4.6+ ships Selenium Manager, which resolves
# the matching driver for your installed Chrome.
pip install selenium
新项目无需 webdriver-manager

如果您在旧版指南中见过 webdriver-manager,现在已不再需要它。它解决的问题如今已由 Selenium Manager 原生处理。仅在已经依赖它的遗留代码中保留;对于任何新项目,单独安装 selenium 就足够了。

启动无头 Chrome

Selenium 4 通过传递给驱动的 Options 对象配置浏览器。对于抓取,您几乎总是需要无头模式(无可见窗口),加上真实的窗口尺寸和用户代理,因为某些网站在视口很小或用户代理明显是自动化工具时会有不同的行为。

python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-gpu")
options.add_argument(
    "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
)

# No driver path needed. Selenium Manager resolves it.
driver = webdriver.Chrome(options=options)

如果您确实需要固定特定驱动二进制文件,现在通过 Service 对象来实现(webdriver.Chrome(service=Service("/path/to/chromedriver"), options=options)),而不是已删除的 executable_path 关键字。对大多数人来说,让 Selenium Manager 自行处理是正确的选择。

打开页面并定位元素

有了驱动,driver.get(url) 会导航页面并阻塞直到初始加载完成。然后您用 By API 查找元素。这是与 Selenium 3 的最大变化:每个 find_element_by_* 辅助方法都已消失,替换为 find_element(By.X, value)

python
from selenium.webdriver.common.by import By

driver.get("https://quotes.toscrape.com/")

# One element, then many.
title = driver.find_element(By.TAG_NAME, "h1").text
quotes = driver.find_elements(By.CLASS_NAME, "quote")

for q in quotes:
    text = q.find_element(By.CLASS_NAME, "text").text
    author = q.find_element(By.CLASS_NAME, "author").text
    print(author, "-", text)

值得掌握的定位器有 By.IDBy.CLASS_NAMEBy.CSS_SELECTORBy.XPATH。CSS 选择器能满足大多数需求且易于阅读;只有在需要按文本内容匹配或向上遍历到父元素(这是 CSS 无法做到的)时才用 XPath。注意 find_element 在没有匹配时会抛出异常,而 find_elements 返回空列表,因此遍历复数形式并检查长度比将单次查找包装在 try/except 中更好。

等待动态内容

以下是大多数 Selenium 初学者爬虫失败的原因:在 get() 返回后立即调用 find_element。在 JavaScript 渲染的页面上,您想要的元素可能尚未存在,而您得到的是 NoSuchElementException,尽管手动查看时页面加载正常。解决方法是显式等待,它会轮询直到条件为真或超时。

不要使用 time.sleep()。固定的休眠在页面快速时浪费时间,在页面缓慢时则会失败;显式等待会在元素就绪时立即返回,只有在元素真正从未出现时才会超时。

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

wait = WebDriverWait(driver, 10)

# Wait until at least one .quote is present in the DOM.
wait.until(
    EC.presence_of_element_located((By.CLASS_NAME, "quote"))
)

# Now it is safe to read the rendered content.
quotes = driver.find_elements(By.CLASS_NAME, "quote")

当您只需要元素在 DOM 中时使用 presence_of_element_located,当您即将读取文字或点击时使用 visibility_of_element_locatedelement_to_be_clickable。定位器以元组形式传递,这是个容易出错的地方:是 ((By.CLASS_NAME, "quote")),一个元组参数,而不是两个独立参数。

处理分页

大多数真实目标将数据分散在多个页面上。模式是一个循环:抓取当前页,找到下一页控件,点击它,等待新内容,重复直到控件消失。陷阱在于页面对象在导航后会过时,因此每次迭代都要重新查找下一页按钮,而不是持有对它的引用。

python
from selenium.common.exceptions import NoSuchElementException

all_quotes = []

while True:
    wait.until(
        EC.presence_of_element_located((By.CLASS_NAME, "quote"))
    )
    for q in driver.find_elements(By.CLASS_NAME, "quote"):
        all_quotes.append(q.find_element(By.CLASS_NAME, "text").text)

    try:
        next_btn = driver.find_element(By.CSS_SELECTOR, "li.next a")
    except NoSuchElementException:
        break  # last page reached
    next_btn.click()

print("scraped", len(all_quotes), "quotes")
driver.quit()

完成后务必调用 driver.quit()。它会关闭浏览器和驱动进程;driver.close() 只关闭当前窗口,驱动进程继续运行,在循环中会迅速导致 Chrome 进程泄漏。对于无限滚动页面(而非下一页按钮),等效做法是在循环中执行 driver.execute_script("window.scrollTo(0, document.body.scrollHeight)"),并等待条目数量停止增长。

通过代理路由 Selenium

从单个 IP 大量抓取任何目标,您都会遭遇速率限制或封锁。通过代理服务器路由浏览器可以将请求分散到不同地址,避免单个地址触发限制。最简单的方式是将代理作为 Chrome 参数传入。

python
options = Options()
options.add_argument("--headless=new")
options.add_argument("--proxy-server=http://proxy.example.com:8080")

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

这适用于无需身份验证的代理。问题是:Chrome 的 --proxy-server 标志不接受 URL 中的用户名和密码,因此需要凭据的代理需要一个注入认证头的浏览器扩展,或者一个持有凭据的本地中继。这是直接驱动 Chrome 的粗糙边缘之一。通过令牌认证的轮换端点可以避开这个问题:您将 Selenium 指向一个主机,轮换和信任在其背后处理。管理自己的代理池和使用托管端点之间的权衡在反向连接代理与 Crawling API 中有详细介绍,如果您的目标防御严密,数据中心代理与住宅代理解释了您实际需要哪种 IP 类型。

什么时候浏览器是错误的工具

Selenium 在页面真正需要浏览器时才是正确的选择:大量客户端渲染、需要交互才能访问的内容,或依赖点击和输入的工作流程。如果只是出于习惯而使用它,则是错误的工具。如果数据在初始 HTML 中,requests 加解析器的速度快了一个数量级,且更轻量。一旦目标用严密的反机器人防御进行反击,原始的无头浏览器无论如何都会被识别指纹并被封锁,您最终需要手动重建轮换、重试和隐身逻辑。

方式 渲染 JS 速度/成本 最适合
requests + 解析器 最快,最轻量 初始 HTML 中有数据,无 JS
Selenium 是(真实浏览器) 慢,消耗资源 JS 渲染、点击、表单、登录
托管 Crawling API 是(服务器端) 单次请求,无基础设施 防御严密的目标、大规模、无需浏览器机群

第三行是大多数生产级抓取的落点。托管 Crawling API 在服务器端渲染页面、轮换 IP、在遭遇封锁时重试,并通过单次请求返回完整 HTML,因此您无需自己运行或强化浏览器机群以应对指纹识别。将 Selenium 保留用于真正的交互工作,将高量、易被封锁的工作交给 API。

Crawlbase Crawling API

当页面需要 JavaScript 但您不想运行浏览器机群时,Crawling API 在服务器端渲染页面、轮换 IP,并在遭遇封锁时重试,通过单次请求返回完整 HTML。发送带有 &javascript=true 的 URL,完全省去无头基础设施。在免费层级针对您的真实目标试用。

调用它只需一个 HTTP 请求,无需驱动、无需等待、无需代理认证流程:发送您的令牌和目标 URL,请求渲染,读取响应体。

python
import requests

resp = requests.get(
    "https://api.crawlbase.com/",
    params={
        "token": "_YOUR_TOKEN_",
        "url": "https://quotes.toscrape.com/js/",
        "javascript": "true",  # render the page server-side
    },
)
print(resp.status_code)
print(resp.text[:500])

与 Selenium 所得渲染结果相同,但没有浏览器、没有显式等待,也没有代理认证的麻烦。关于运行自己的代理池与使用拥有整个工作的端点之间更深入的比较,请参阅网络爬虫最佳代理

回顾

核心要点

  • 使用 Selenium 4 模式。By 定位器 API 替换了所有 find_element_by_* 辅助方法,Selenium Manager 自动解析驱动,因此手动下载步骤已经消失。
  • 通过 Options 配置浏览器。无头模式、真实的窗口尺寸和用户代理是抓取的基本配置。
  • 显式等待,永远不要 sleep。WebDriverWaitexpected_conditions 在内容就绪时立即返回,是修复 JS 渲染竞态条件的正确方法。
  • 导航后重新查找元素。引用在页面跳转后会过时;每次循环重新查询下一页控件,并在结束时调用 driver.quit()
  • 浏览器并非总是答案。当数据在 HTML 中时使用 requests,当目标防御严密或需要在没有浏览器机群的情况下大规模运行时使用托管 Crawling API。

常见问题

Selenium 4 还需要下载 ChromeDriver 吗?

不需要。自 Selenium 4.6 起,内置的 Selenium Manager 会检测您已安装的 Chrome 并自动下载匹配的驱动,因此只需 pip install selenium 即可。只有当您故意需要固定特定二进制文件时,才通过 Service 对象指定驱动路径;旧的 executable_path 关键字已被移除。

为什么 find_element 在页面加载正常时抛出 NoSuchElementException?

因为元素是由 JavaScript 在初始加载后渲染的,而您的代码在它存在之前就查询了 DOM。用显式等待替换即时查找:WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CLASS_NAME, "quote")))。等待会轮询直到元素出现或超时,避免了固定休眠和竞态条件。

应该用 Selenium 还是 BeautifulSoup 进行网络抓取?

当数据在页面初始 HTML 中时,使用 requests 加 BeautifulSoup 等解析器;这要快得多、轻量得多,因为它不会启动浏览器。当内容由 JavaScript 渲染或需要点击、输入、滚动或登录时,使用 Selenium。许多爬虫将两者结合:Selenium 渲染页面,然后 BeautifulSoup 解析生成的 HTML。

如何在 Selenium 中使用需要身份验证的代理?

Chrome 的 --proxy-server 参数不接受 URL 中的用户名和密码,因此需要凭据的代理需要一个注入认证头的浏览器扩展,或者一个持有凭据的本地中继。使用令牌认证的轮换端点可以避免这个问题:您将 Selenium 指向一个主机,凭据在其背后保存,而不是写在启动标志中。

Selenium 适合大规模抓取吗?

扩展性很差。每个页面都运行完整的浏览器,速度慢且内存消耗大,而且在防御严密的目标上,原始的无头浏览器仍会被识别指纹并被封锁。对于大量请求,服务器端渲染、轮换 IP 并在遭遇封锁时重试的托管 Crawling API 通常是更好的选择,将 Selenium 留给真正需要大量交互的工作。

driver.close() 和 driver.quit() 有什么区别?

driver.close() 关闭当前浏览器窗口,但驱动进程和其他任何窗口继续运行。driver.quit() 关闭所有窗口并关闭驱动进程。在抓取循环中,务必以 driver.quit() 结束,否则孤立的 Chrome 进程会堆积并耗尽内存。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量