很多值得采集的数据从不出现在原始 HTML 中。商品网格、评论区、无限滚动信息流和仪表盘组件,都在 JavaScript 于浏览器中运行之后才出现,因此普通的 HTTP 请求返回给你的是一个骨架,有趣的部分全部缺失。经典的解法是在真实浏览器中渲染页面,然后解析完成的标记。在 Python 中最常见的组合正是 Selenium 加 BeautifulSoup。

本指南将展示如何使用 Selenium 和 BeautifulSoup 抓取动态内容:Selenium 驱动无头 Chrome 实例执行 JavaScript、等待元素出现、滚动触发懒加载并处理点击交互,而 BeautifulSoup 则将渲染后的 page_source 解析为干净的结构化数据。我们将构建一个小型、可运行的示例,然后诚实地审视这套方案在哪里变得昂贵,以及在哪些情况下服务端渲染 API 是更轻量的选择。

为什么普通请求会错过动态内容

当页面是服务端渲染时,你下载的 HTML 已经包含了数据。当页面是客户端渲染时,服务器发送一个近乎空白的骨架加上一个 JavaScript 包;浏览器运行该 JavaScript,回调 API,然后将真实内容注入 DOM。像 requests 这样的库只能看到第一个响应,所以它永远看不到 JavaScript 之后添加的内容。

这就是动态爬取要解决的全部问题:你需要某样东西在读取 DOM 之前真正执行页面的 JavaScript。Selenium 通过自动化真实浏览器来实现这一点。一旦浏览器渲染完成,得到的 HTML 就只是普通的 HTML,而 BeautifulSoup 是从中提取字段的快速、符合人体工程学的方式。两个工具分工明确:Selenium 处理交互和渲染,BeautifulSoup 处理提取。

先渲染,再解析

BeautifulSoup 不执行 JavaScript。它自身只解析你传给它的 HTML,因此用客户端渲染页面的原始响应喂给它,你得到的是同样空的骨架,如同普通请求返回的结果一样。渲染步骤必须先发生,无论是通过 Selenium 的本地浏览器,还是返回完整 HTML 的渲染 API。

搭建环境

你需要 Python 3.8 或更高版本以及 pip。创建虚拟环境以隔离依赖,然后安装 Selenium 和 BeautifulSoup。

bash
python -m venv scraper_env
source scraper_env/bin/activate

pip install selenium beautifulsoup4

在 Windows 上,用 scraper_env\Scripts\activate 代替 source 那行来激活。你无需手动下载驱动二进制文件:自 Selenium 4.10 起,Selenium Manager 会在你首次启动 Chrome 时自动解析并下载匹配的 ChromeDriver,因此只需一个当前版本的 Chrome 安装和 selenium 包即可开始。

步骤 1:启动无头 Chrome WebDriver

首先配置 Chrome 以无头模式运行,即没有可见窗口。无头模式更快,是服务器上的理想选择,但在开发阶段以有界面模式运行可以让调试选择器更加直观。几个额外的标志使浏览器在容器中更加稳定,并减少简单反爬检测的暴露面。

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

def build_driver():
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--window-size=1920,1080")
    return webdriver.Chrome(options=options)

固定窗口大小比看起来更重要:许多网站在不同视口宽度下渲染不同的布局,因此固定尺寸可以使选择器在多次运行中保持稳定。调试时,去掉 --headless=new 这行并实时观察页面加载过程。

步骤 2:导航并使用显式等待元素

动态爬取中最常见的错误是在内容到达之前就读取 DOM。固定的 time.sleep() 是错误的解决方案:太短会漏掉数据,太长会让每次运行都很慢。正确的工具是显式等待,它会轮询页面直到特定条件为真(或超时触发),条件满足时立即返回。Selenium 通过 WebDriverWait 配合 expected_conditions 来实现这一点。

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

def load_page(driver, url, wait_for):
    driver.get(url)
    WebDriverWait(driver, 15).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, wait_for))
    )

这里 load_page 导航到 URL,并阻塞直到至少一个匹配 wait_for 的元素出现,最多等待 15 秒。当你还需要确保元素已绘制(而不只是出现在 DOM 中)时,使用 visibility_of_element_located;在点击某个元素之前,使用 element_to_be_clickable。将等待绑定到你真正需要的元素,是让 Selenium 运行既快速又可靠的关键。

步骤 3:滚动以触发懒加载

许多信息流和商品网格只在你滚动时才加载更多条目。要捕获所有内容,你必须自行驱动滚动,然后等待新一批内容渲染完成,再继续滚动。模式是一个循环:滚动到底部,等待,测量页面高度,当高度不再增长时停止。

python
import time

def scroll_to_bottom(driver, pause=2.0, max_rounds=10):
    last_height = driver.execute_script("return document.body.scrollHeight")
    for _ in range(max_rounds):
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(pause)
        new_height = driver.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            break
        last_height = new_height

这里短暂的 sleep 是等待下一批数据获取和渲染的实用停顿;这是固定延迟难以避免的一处,因为触发条件是网络,而不是某个已知元素。用 max_rounds 限制循环,防止永不停止增长的页面导致无限运行。如果网站使用"加载更多"按钮而不是无限滚动,等效方式是找到按钮、点击它、等待新行出现,然后重复直到按钮消失。

步骤 4:将渲染后的 HTML 传给 BeautifulSoup

页面渲染完毕且完全滚动后,剩下的就是普通的解析。读取 driver.page_source,这是实时 DOM 序列化后的 HTML,将其载入 BeautifulSoup。然后就可以像处理任何静态页面一样,通过 CSS 选择器选择元素。

python
from bs4 import BeautifulSoup

def parse_items(html):
    soup = BeautifulSoup(html, "html.parser")
    items = []
    for card in soup.select("div.product-card"):
        title = card.select_one("h2.title")
        price = card.select_one("span.price")
        items.append({
            "title": title.get_text(strip=True) if title else None,
            "price": price.get_text(strip=True) if price else None,
        })
    return items

对每个字段的保护(title.get_text(...) if title else None)防止一个缺失元素导致整次运行崩溃,这值得从一开始就养成习惯,因为真实列表数据往往不一致。你也可以通过 Selenium 自己的 find_elements 来查询元素,但 BeautifulSoup 在 DOM 稳定后进行批量提取时更快,其选择器和导航 API 也更友好。

步骤 5:将各步骤串联起来

将四个步骤串联成一个脚本:构建驱动器、加载并等待、滚动、解析,然后务必退出驱动器,避免泄漏浏览器进程。

python
import json

def main():
    url = "https://example.com/products"
    driver = build_driver()
    try:
        load_page(driver, url, "div.product-card")
        scroll_to_bottom(driver)
        data = parse_items(driver.page_source)
    finally:
        driver.quit()
    print(json.dumps(data, indent=2))

if __name__ == "__main__":
    main()

try/finally 在生产环境中不是可选的。滚动循环或等待可能会抛出异常,如果 driver.quit() 从未执行,每次失败都会留下一个僵尸 Chrome 进程。长时间运行的任务会迅速耗尽内存。关于以这种方式运行浏览器的更深入介绍,请参见用于网页抓取的无头浏览器,以及更广泛的用 Python 渲染 JavaScript 的方案,请参见如何用 Python 抓取 JavaScript 页面

Selenium 方案的真实成本

这套方案有效,对于少量页面的一次性爬取来说很难被超越。在大规模场景下,成本会积累,在决定运行浏览器集群之前,值得将其明确说出来。

  • 浏览器开销。 每个页面都会启动一个完整的 Chrome 实例,带来相应的内存和 CPU 占用。十几个并发驱动器可以打满一台小型服务器,因此吞吐量受到硬件限制,而不仅仅是网络限制。
  • 不稳定的等待。 显式等待远比 sleep 更好,但当网站改变其标记或时序时,它们仍然会失效,而在本地通过的等待可能在较慢的机器上超时。等待逻辑需要持续维护。
  • 反爬检测。 无头浏览器仍然会泄露信号(驱动器指纹、缺失的请求头、数据中心 IP),现代防御系统能够识别这些信号。在一定流量规模下,你会遇到 CAPTCHA 和 IP 封锁,而任何等待都解决不了这些问题,这意味着还需要在上述所有成本之外叠加代理轮换和指纹修补。

这些不能说明 Selenium 是错误的工具;它说明 Selenium 是一个重型工具。当任务是"从服务器可靠地渲染大量页面,而不需要照料浏览器集群"时,渲染和反封锁才是难点,而这些正是托管 API 可以从你手中接管的部分。

Crawlbase Crawling API

如果你想获得渲染后的 HTML 而无需运行浏览器集群,Crawling API 会在服务端的真实浏览器中渲染页面,为你轮换住宅 IP,然后返回完整的 HTML,你用同样的 BeautifulSoup 代码解析即可。你传入 JS token 和等待选项,而不是管理驱动器、滚动循环和代理池。先在免费层用它请求一个动态页面试试。

更轻量的替代方案:服务端渲染,本地解析

Crawling API 保留了这套工作流中你真正喜欢的那一半(BeautifulSoup 提取),去掉了令人痛苦的那一半(运行和反封锁浏览器)。你用 JavaScript token 发送 URL,API 在可信 IP 后渲染它,然后你像以前一样解析返回的 HTML。步骤 4 中的同一个 parse_items 函数无需任何修改即可使用。

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
import json

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def fetch_rendered(url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

html = fetch_rendered("https://example.com/products")
if html:
    print(json.dumps(parse_items(html), indent=2))

ajax_wait 选项告知 API 等待异步内容稳定,page_wait 在加载后保持固定的毫秒数停顿,使延迟渲染的元素在捕获前出现。如果字段返回为空,请增大 page_wait。注意消失了什么:没有驱动器生命周期、没有滚动循环、没有 try/finally 清理,也没有代理管理。渲染和 IP 轮换在服务端进行,因此一千个页面就是一千次 HTTP 调用,而不是一千次浏览器启动。

何时保留 Selenium

如果你的任务需要真正的多步交互(登录、填写并提交表单、点击向导步骤,然后读取依赖这些操作的状态),Selenium 有状态的浏览器会话才是正确工具。Crawling API 擅长"渲染这个 URL 并给我完整的 HTML"这类大批量场景。许多项目两者并用:浏览器处理少量交互式流程,API 处理批量获取。

关于其他值得对比的浏览器自动化方案,用于网页抓取的 Playwright介绍了具有类似权衡的现代 Selenium 替代品。如果你希望通过轮换 IP 路由自己的浏览器流量,而不是使用托管 API,Smart AI Proxy 以直连代理端点的形式提供住宅轮换;对于一次性任务,异步 Crawler 将渲染结果推送到回调,而不是阻塞每次请求。

回顾

核心要点

  • 动态内容需要渲染。 JavaScript 在第一个响应之后注入数据,因此在解析之前必须执行页面;普通请求返回的是一个骨架。
  • Selenium 渲染,BeautifulSoup 提取。 驱动无头 Chrome WebDriver 进行渲染和交互,然后将 driver.page_source 传给 BeautifulSoup 进行快速的基于选择器的提取。
  • 使用显式等待,而非 sleep。 绑定到你所需元素的 WebDriverWait 配合 expected_conditions,既比固定延迟更快,也更可靠。
  • 滚动以触发懒加载。 循环执行"滚动-等待-测量",直到页面高度不再增长,并设置循环上限以防无限运行。
  • Selenium 在大规模下很重。 浏览器开销、不稳定的等待和反爬封锁会不断积累;像 Crawling API 这样的渲染 API 返回带有 IP 轮换处理的完整 HTML,你的 BeautifulSoup 代码保持不变。

常见问题

为什么 BeautifulSoup 单独无法抓取动态内容?

BeautifulSoup 是一个解析器,不是浏览器:它读取你传给它的 HTML,但从不执行 JavaScript。在客户端渲染的页面上,原始 HTML 是一个空壳,因此在某样东西先渲染页面之前,BeautifulSoup 没有任何内容可以提取。这个渲染步骤正是 Selenium 在本地提供的,或者是 JS token 渲染 API 在服务端提供的,在 BeautifulSoup 运行之前。

如何等待动态元素而不是用 sleep 猜测?

使用显式等待。WebDriverWait(driver, timeout).until(EC.presence_of_element_located((By.CSS_SELECTOR, sel))) 轮询页面,在元素出现的瞬间返回,最多等待超时时间。这比固定的 time.sleep() 更快,因为它不会等待超过必要的时间,也更可靠,因为它绑定到你实际需要的元素,而不是一个猜测的持续时间。

还需要手动下载 ChromeDriver 吗?

自 Selenium 4.10 起不需要了。Selenium Manager 会在你首次启动浏览器时自动解析并下载与你已安装的 Chrome 匹配的 ChromeDriver 版本。只有在自动下载被阻止的封闭环境中,才需要手动管理驱动器,此时你需要指向你自己提供的驱动器二进制文件。

Selenium 能帮我绕过反爬系统吗?

在大规模下单靠 Selenium 不行。无头浏览器仍然会暴露信号(自动化指纹、数据中心 IP、缺失或不一致的请求头),现代防御系统能够检测到这些,因此当请求量上升时你会遇到 CAPTCHA 和 IP 封锁。缓解这些问题需要叠加代理轮换和指纹修补,这就是为什么一个同时处理渲染和 IP 轮换的托管 Crawling API 往往比加固浏览器集群的工作量更少。

何时应该使用 Crawling API 而不是 Selenium 和 BeautifulSoup?

当任务是从服务器可靠地渲染大量页面,且你不想运行或反封锁浏览器集群时,使用 Crawling API。它在服务端渲染,轮换住宅 IP,并返回完整 HTML,你现有的 BeautifulSoup 代码无需修改即可解析。当你需要真正的有状态交互(如登录、提交表单或点击多步流程)时,保留 Selenium。

我能将 BeautifulSoup 解析代码复用于 Crawling API 吗?

可以,这正是其主要吸引力所在。Crawling API 以字符串形式返回渲染后的 HTML,因此同样的 BeautifulSoup(html, "html.parser") 调用和同样的选择器无需更改即可使用。你只需替换获取 HTML 的方式:从本地浏览器的 driver.page_source 改为从 API 调用的 response["body"] 读取。

开始构建

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

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

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