先给结论
使用Selenium进行网页抓取的流程是:打开浏览器页面,等待目标记录完成渲染,在每个记录容器内提取字段,最后写入CSV。本文以Quotes to Scrape的JavaScript练习页为例,演示单页抓取、显式等待、翻页、去重、无头模式和运行状态检查。
Selenium适合需要浏览器渲染或交互的页面。如果授权的HTTP响应或官方API已经包含所需数据,直接使用HTTP客户端或API通常更简单。先验证单页字段,再增加翻页。运行报告中,last_page表示正常到达末页,page_limit表示达到设定上限,error表示运行失败;CSV行数只能验证导出数量,不能证明覆盖了网站全部数据。
1.安装Python、Selenium和Chrome
准备Python3.10或更高版本、桌面版Chrome和独立虚拟环境即可开始。现代Selenium通常会通过Selenium Manager自动处理驱动。
Windows PowerShell:
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install selenium==4.51.0
macOS或Linux:
python3 -m venv .venv
.venv/bin/python -m pip install selenium==4.51.0
命令直接调用虚拟环境中的Python,不必先激活环境。若Windows不识别py但python --version可用,将第一条命令改为python -m venv .venv。首次启动可能需要联网下载兼容驱动;受限网络、系统不支持或PATH中已有冲突驱动时,仍需按环境单独配置。
把代码保存为scrape_quotes.py,不要命名为selenium.py,否则会遮蔽已安装的Selenium包。
2.运行第一个单页抓取脚本
先让脚本稳定导出当前页,再处理翻页。下面的脚本使用Selenium和Python标准库,等待可读取的报价卡片,逐条提取文本、作者和标签,并写入CSV。
import csv
from pathlib import Path
from selenium import webdriver
from selenium.common.exceptions import (NoSuchElementException,
StaleElementReferenceException, WebDriverException)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def read_records(driver):
try:
cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
if not cards:
return False
records = []
for card in cards:
quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
tags = [tag.text for tag in card.find_elements(By.CSS_SELECTOR, ".tag")]
if not quote or not author:
return False
records.append({"quote": quote, "author": author,
"tags": " | ".join(tags)})
return records
except (NoSuchElementException, StaleElementReferenceException):
return False
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(30)
driver.get("https://quotes.toscrape.com/js/")
records = WebDriverWait(driver, 15).until(read_records)
for record in records:
record["source_url"] = driver.current_url
output = Path("quotes.csv").resolve()
with output.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file,
fieldnames=["quote", "author", "tags", "source_url"])
writer.writeheader()
writer.writerows(records)
print(f"Page: {driver.current_url}")
print(f"Saved {len(records)} records to {output}")
finally:
try:
driver.quit()
except WebDriverException:
pass
在脚本所在目录运行:
.\.venv\Scripts\python.exe scrape_quotes.py
.venv/bin/python scrape_quotes.py
Chrome会打开并自动关闭。终端应打印页面URL、保存数量和quotes.csv绝对路径。脚本只抓当前页;失败时会抛出异常,不会打印成功消息。旧CSV可能仍然存在,判断结果时还要检查文件更新时间。

3.选择正确的选择器并等待渲染完成
选择器应描述真实的记录结构,等待条件应描述“数据已可读”,不能用固定睡眠时间猜测页面何时完成。
先检查一条记录
打开Quotes to Scrape JavaScript练习页,右键报价并选择“检查”。每条记录位于.quote容器,正文是.text,作者是.author,标签是.tag。先找出所有记录容器,再在容器内部找字段,可以避免不同记录的字段错位。复制很长的绝对XPath会绑定页面偶然的嵌套层级,优先使用CSS选择器、稳定属性或明确关系。

driver.get()只按浏览器的页面加载策略等待,不代表后续JavaScript更新已经完成。根据Selenium等待文档,WebDriverWait(driver,15).until(read_records)会反复调用提取函数,直到找到记录且必填字段非空;缺元素或元素变旧时继续重试。不要混用隐式等待和显式等待。
这个条件适合示例页,不是所有网站的完整性证明。分批加载的页面应等待已知结果数、加载指示器或业务状态;presence_of_all_elements_located只保证当前匹配存在,不保证后续结果到齐。
读取字段并写入CSV
使用.text读取可见文字,使用get_attribute("href")读取链接地址。标签用|合并到一个字段。标准库CSV写入器可处理逗号、引号和字段内换行;newline=""遵循Python CSV文档的建议,utf-8-sig有助于表格软件识别Unicode。统计解析后的记录,不要按物理文本行计数。

4.翻页、去重并判断运行结果
单页数据确认无误后再翻页。每次点击Next都要验证旧元素已失效、URL已变化,并单独记录停止原因。
将下面代码保存为scrape_quotes_pages.py。它包含单页提取、翻页、去重、CSV导出和run-status.json报告:
import argparse, csv, json
from pathlib import Path
from urllib.parse import urljoin
from selenium import webdriver
from selenium.common.exceptions import (NoSuchElementException,
StaleElementReferenceException, WebDriverException)
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
START_URL = "https://quotes.toscrape.com/js/"
FIELDS = ["quote", "author", "tags", "source_url"]
def read_records(driver):
try:
cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
if not cards: return False
records = []
for card in cards:
quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
tags = [x.text for x in card.find_elements(By.CSS_SELECTOR, ".tag")]
if not quote or not author: return False
records.append({"quote": quote, "author": author,
"tags": " | ".join(tags)})
return records
except (NoSuchElementException, StaleElementReferenceException):
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--pages", type=int, default=1)
parser.add_argument("--headless", action="store_true")
args = parser.parse_args()
if args.pages < 1: parser.error("--pages must be at least 1")
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
if args.headless: options.add_argument("--headless=new")
driver = None; rows, seen, visited = [], set(), set(); raw_count = 0
status, detail = "error", "Browser did not start"
try:
driver = webdriver.Chrome(options=options); driver.set_page_load_timeout(30)
wait = WebDriverWait(driver, 15); driver.get(START_URL)
for page_number in range(1, args.pages + 1):
current_url = driver.current_url
if current_url in visited: raise ValueError("Repeated page URL; pagination stopped")
batch = wait.until(read_records); visited.add(current_url); raw_count += len(batch)
for record in batch:
key = (record["quote"], record["author"])
if key not in seen:
seen.add(key); rows.append({**record, "source_url": current_url})
print(f"Page {page_number}: {len(batch)} records | {current_url}")
links = driver.find_elements(By.CSS_SELECTOR, ".pager .next a")
if not links:
status, detail = "last_page", "No Next link on the processed page"; break
if page_number == args.pages:
status, detail = "page_limit", "More pages exist; requested limit reached"; break
expected_url = urljoin(current_url, links[0].get_attribute("href"))
old_card = driver.find_element(By.CSS_SELECTOR, ".quote")
links[0].click(); wait.until(EC.staleness_of(old_card)); wait.until(EC.url_to_be(expected_url))
except (WebDriverException, ValueError) as exc:
status = "error"; detail = f"{type(exc).__name__}: {str(exc).splitlines()[0]}"
if driver is not None:
try: driver.save_screenshot("failure.png")
except WebDriverException: pass
finally:
if driver is not None:
try: driver.quit()
except WebDriverException: pass
output = Path("quotes-pages.csv").resolve()
with output.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=FIELDS); writer.writeheader(); writer.writerows(rows)
report = {"status": status, "detail": detail, "pages_processed": len(visited),
"raw_records": raw_count, "unique_records": len(rows),
"duplicates_removed": raw_count - len(rows), "csv": str(output)}
Path("run-status.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report, indent=2))
if status == "error": raise SystemExit(1)
if __name__ == "__main__": main()
运行20页:
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20
脚本遇到.pager .next a后,先保存目标URL和旧页面的.quote元素,点击后等待旧元素失效并确认URL匹配,再读取新页面。只等待同一个.quote选择器可能读到旧页面元素。单页应用若不替换文档,应改为等待页码变化或记录ID变化。
如何确认Selenium抓取正常结束?
先读run-status.json,再使用CSV:
| 状态 | 含义 | 下一步 |
|---|---|---|
last_page |
当前页没有Next链接,正常到达分页终点 | 检查选择器和页面结构是否正确 |
page_limit |
还有下一页,但达到请求上限 | 提高--pages或接受当前范围 |
error |
浏览器启动、提取或导航失败 | 处理错误后重新运行;CSV可能只是部分结果 |
报告还会给出已处理页数、原始记录数、去重后记录数和删除的重复数。Windows下可立即检查退出码和解析后的CSV行数:
Write-Output "Exit code: $LASTEXITCODE"
Write-Output "CSV records: $(@(Import-Csv .\quotes-pages.csv).Count)"
退出码为0只表示脚本没有报告错误,page_limit也会返回0;仍应核对状态和字段。CSV数量与unique_records一致只能证明导出一致,不能证明每个字段或全部网站记录都正确。

5.把脚本改造成自己的抓取器
更换URL时必须同时检查记录选择器、字段、去重键和翻页条件。旧的.quote选择器不会自动适配新网站。
- 起始URL: 把
START_URL改为获准访问的列表页,并确认重定向后的地址。 - 记录容器: 用重复商品卡片、表格行或文章条目替换
.quote,核对匹配数量。 - 字段与去重: 同步修改提取逻辑、
FIELDS和唯一键;真正可选的字段不要设为必填等待条件。 - 翻页: 替换
.pager .next a,并选择能证明新批次已加载的信号。
“加载更多”按钮应等待新增记录ID;无限滚动应在每轮滚动后等待新ID,并设置最大时长或迭代次数。若没有明确结束标记,应记录为不确定停止。虚拟列表可能从DOM移除旧记录,不能只在最后一次性提取。iframe需要先切换frame;Shadow DOM和Canvas内容也需要单独的提取方案。
6.排查空结果、重复页面和驱动错误
- 空结果或反复超时: 确认当前URL、实时DOM、选择器和frame上下文。增加超时时间无法修复匹配不到任何元素的选择器。
StaleElementReferenceException: 节点已被替换或文档发生变化。更新后重新定位元素。- 页面重复: 检查真实Next URL或页码指示器。保留重复URL保护,调查原因,不要无限静默去重。
- Chrome无法启动: 区分驱动下载、浏览器启动、PATH冲突和系统限制。浏览器代理不会自动解决Selenium Manager的下载网络问题。遇到元素失效、超时等异常时,先查看终端中的异常类型和出错步骤。
- CSV缺行: 读取
run-status.json并检查CSV更新时间。若Excel占用文件导致PermissionError,关闭文件、确认目录可写后再运行。 - 拒绝访问或出现CAPTCHA: 停止自动重试,检查授权、站点条款和可用API。被拦截的页面不是普通空列表。
7.使用无头模式
先在可见浏览器中确认结果,再使用无头模式部署。无头Chrome仍然运行浏览器,只是不显示窗口。
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20 --headless
.venv/bin/python scrape_quotes_pages.py --pages 20 --headless
脚本会在提取或导航错误后尝试保存failure.png;启动失败可能没有截图。检查截图时间,避免误用旧文件。无头模式只是隐藏浏览器窗口,运行方式仍是Chrome自动化。
8.为Selenium抓取选择合适的代理
从练习页面转向价格监测、地区内容核对等实际项目后,代理的作用是让浏览器通过指定网络出口访问页面。选择代理时,先明确两个问题:需要哪个地区的访问结果,以及同一次任务是否需要保持固定IP。
| 抓取需求 | 选择重点 |
|---|---|
| 比较不同地区的商品价格或页面内容 | 目标地区是否可选,浏览器出口是否与任务要求一致 |
| 分别采集多个独立页面或批次 | 是否需要按批次更换出口,以及如何控制请求频率 |
| 连续翻页或完成依赖会话的操作 | 同一任务期间保持出口稳定,避免中途切换IP影响页面状态 |
| 定期通过同一出口执行监测 | 固定IP的使用周期和维护方式 |
如果项目需要灵活安排出口或按批次采集,可以先查看Socks5.IO的动态住宅代理,按目标地区和会话需求选择配置;如果更看重长期使用同一出口,可以查看静态住宅代理。选定产品后,再核对可用地区、会话时长和认证方式。
准备接入时,按照Selenium代理IP配置指南完成浏览器配置,具体接入参数可查阅Selenium代理集成文档。先用单页脚本确认出口IP和地区,再执行连续翻页,检查会话是否稳定。Chrome代理认证需要使用受支持的配置方式,不能简单把username:password拼进--proxy-server参数。
已有明确采集需求,可以从对应的Socks5.IO产品页选择代理,再用本文的单页脚本完成接入验证。 确认字段、出口和翻页结果都符合预期后,再扩大采集范围。






