动态渲染页面:Selenium 入门

requests 抓动态页面只能拿到一具空壳,这时才轮到 Selenium:它真的启动浏览器,等 JavaScript 把 DOM 渲染完,再读渲染后的结果。本章讲清什么时候值得付这个代价、启动选项怎么写,以及为什么浏览器应该是最后手段。

什么时候必须开浏览器

判断标准只有一条:目标数据是否只能由页面里的 JavaScript 产生,且找不到等价的接口。

场景特征结论
单页应用(SPA)首屏 HTML 只是挂载节点,列表由前端组件渲染接口不可用时必须用
异步分片加载数据由 XHR/fetch 分页拉取,DOM 里查不到先找接口,找不到才用
需要交互点「加载更多」、滚动到底、输入关键词必须用
接口带签名保护请求参数动态变化,没有官方开放接口只按页面正常流程低频操作

签名保护这一类要格外克制:先找官方开放接口、RSS 或数据导出功能,都没有时才用浏览器按页面正常流程低频操作自己的账号,不做参数逆向。采集前先看 robots.txt 与站点服务条款,绕过登录、付费墙、验证码等访问控制永远不在范围内。

反例:能用接口就不要开浏览器

浏览器是最后手段,不是第一选择:先抓包看请求,能复现出等价的只读接口就写接口采集,纯静态 HTML 用 requests 加解析库即可。

维度HTTP 接口浏览器自动化
单次耗时几十到几百毫秒1 秒以上,冷启动更久
内存占用几 MB每个实例数百 MB 起
并发能力单机几十到几百并发单机十几个实例就已吃满
行为特征与普通请求一致指纹、轨迹、时序都更像机器人

只有数据确实由 JS 计算、且没有可复现接口时才切到浏览器;调一次浏览器够接口跑十次以上,这个差距决定了采集规模的上限。

安装与启动选项

pip install selenium 装的是包,驱动交给 Selenium Manager:4.6 起启动时会自动检测浏览器版本、下载匹配的驱动并缓存,一般不需要手动下载 chromedriver,也不用把驱动路径写进 PATH。

选项通过 webdriver.Chrome(options=options) 传入,容器里跑无头模式的必备三项是 --headless=new--no-sandbox--disable-dev-shm-usage,少了第三项页面一复杂就随机崩溃;需要固定视口时加 --window-size,需要声明身份时加 --user-agent

等待:只用显式等待

time.sleep 是猜时间,WebDriverWait 是等条件;猜时间要么白等,要么不够。

条件等到什么适用场景
presence_of_element_located元素进入 DOM,不要求可见读取属性或 .text 之前的兜底
visibility_of_element_located元素在 DOM 且宽高大于 0读取可见文本、截图
element_to_be_clickable元素可见且可交互点击、输入之前
text_to_be_present_in_element元素文本包含指定片段等异步填充的标题或数字
invisibility_of_element_located元素消失或不可见等加载遮罩、骨架屏消失
wait = WebDriverWait(driver, 15, poll_frequency=0.5)   # 最长等 15 秒,每 0.5 秒轮询一次
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "#list .item")))

不要和隐式等待混用:driver.implicitly_wait(10) 对每一次元素查找都生效,叠加显式等待后最坏情况是「隐式等 10 秒没找到,显式再等 15 秒」,定位失败时也难判断是页面没渲染还是选择器写错。统一用显式等待,让超时异常带着选择器信息抛出来。

定位与滚动

定位优先级是 By.ID 高于 By.CSS_SELECTORBy.CSS_SELECTOR 高于 By.XPATH:ID 最稳定,CSS 选择器可读性好、基本够用,XPath 只在需要按文本匹配或向上溯源时用,它对结构调整极其敏感。

find_element 找不到会抛异常,find_elements 返回列表、找不到时是空列表,判断弹窗或列表是否存在直接判长度即可。要让页面继续加载得自己发起动作:driver.execute_script("window.scrollTo(0, document.body.scrollHeight)") 触发懒加载,element.click() 点「加载更多」,之后仍要用显式等待确认新内容真的出现。

完整可运行示例

import time

from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

TARGET_URL = "https://example.com/list"   # 只采集条款允许抓取的目标
PAGE_INTERVAL, MAX_PAGES = 3.0, 3         # 翻页间隔与页数上限,给目标站点留余量


def collect(driver, wait: WebDriverWait) -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    driver.get(TARGET_URL)
    for page in range(1, MAX_PAGES + 1):
        wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#list .item")))
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")   # 触发懒加载
        soup = BeautifulSoup(driver.page_source, "html.parser")   # 渲染后的 DOM 交给解析库
        rows += [{"page": page, "title": n.select_one(".title").get_text(strip=True)}
                 for n in soup.select("#list .item")]
        if not driver.find_elements(By.CSS_SELECTOR, "#next:not([disabled])"):
            break   # 没有下一页就结束
        driver.find_element(By.CSS_SELECTOR, "#next").click()
        time.sleep(PAGE_INTERVAL)   # 主动降速,不要把目标站点打满
    return rows


def main() -> None:
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")            # 无头模式
    options.add_argument("--no-sandbox")              # 容器内常需要
    options.add_argument("--disable-dev-shm-usage")   # 避免 /dev/shm 过小导致崩溃
    driver = webdriver.Chrome(options=options)
    try:
        rows = collect(driver, WebDriverWait(driver, 15, poll_frequency=0.5))
        print(f"共采集 {len(rows)} 条", rows[:3])
    except TimeoutException as exc:
        print(f"等待超时,先检查选择器再怀疑网络:{exc}")
    finally:
        driver.quit()   # 必须退出,否则浏览器与驱动进程会一直残留


if __name__ == "__main__":
    main()

driver.page_source 返回当前 DOM 的 HTML 字符串,数据此时已经在里面,交给 BeautifulSoup 解析比逐个 find_element 取值更快也更好写;无头模式省的是界面开销,不是内存,所以务必用 try/finally 保证 driver.quit() 被执行。

常见坑与调试

现象常见原因处理方式
元素可见却定位不到页面在 iframe 或 shadow DOM 内driver.switch_to.frame(...),必要时展开 shadow root
超时但截图内容齐全选择器命中了不可见的占位元素换用 visibility_of_element_located
跑一段时间机器变卡忘了 driver.quit(),进程与临时目录堆积try/finally 保证退出
无头模式下行为与手动不一致视口过小、UA 被识别为无头显式设置 --window-size 与 UA

调试点只有一个原则:让失败现场可见。先去掉 --headless=new 用有界面模式手跑一遍,再加 driver.save_screenshot("debug.png")print(driver.current_url),确认页面到底停在哪一步。

小结:先确认没有可复现的接口,再考虑用 Selenium 打开浏览器;用显式等待替代猜时间,用 page_source 加解析库处理渲染结果,用 try/finally 保证浏览器退出,频率与合规底线始终先于采集效率。

笔记加载中…