教程

如何使用Selenium进行Python网页抓取:CSV导出与翻页验证

学习如何使用Selenium进行Python网页抓取:安装Selenium、等待JavaScript渲染的数据、导出CSV、验证翻页结果,并定位失败原因。

Gideon ReedGideon Reed13 分钟阅读

先给结论

使用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:

powershellpowershell
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install selenium==4.51.0

macOS或Linux:

bashbash
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。

pythonpython
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

在脚本所在目录运行:

powershellpowershell
.\.venv\Scripts\python.exe scrape_quotes.py
bashbash
.venv/bin/python scrape_quotes.py

Chrome会打开并自动关闭。终端应打印页面URL、保存数量和quotes.csv绝对路径。脚本只抓当前页;失败时会抛出异常,不会打印成功消息。旧CSV可能仍然存在,判断结果时还要检查文件更新时间。

Windows PowerShell显示Selenium Python抓取脚本将10条报价保存为CSV

3.选择正确的选择器并等待渲染完成

选择器应描述真实的记录结构,等待条件应描述“数据已可读”,不能用固定睡眠时间猜测页面何时完成。

先检查一条记录

打开Quotes to Scrape JavaScript练习页,右键报价并选择“检查”。每条记录位于.quote容器,正文是.text,作者是.author,标签是.tag。先找出所有记录容器,再在容器内部找字段,可以避免不同记录的字段错位。复制很长的绝对XPath会绑定页面偶然的嵌套层级,优先使用CSS选择器、稳定属性或明确关系。

Chrome DevTools显示报价、正文、作者和标签元素

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。统计解析后的记录,不要按物理文本行计数。

CSV输出显示quote、author、tags和source_url四列

4.翻页、去重并判断运行结果

单页数据确认无误后再翻页。每次点击Next都要验证旧元素已失效、URL已变化,并单独记录停止原因。

将下面代码保存为scrape_quotes_pages.py。它包含单页提取、翻页、去重、CSV导出和run-status.json报告:

pythonpython
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页:

powershellpowershell
.\.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行数:

powershellpowershell
Write-Output "Exit code: $LASTEXITCODE"
Write-Output "CSV records: $(@(Import-Csv .\quotes-pages.csv).Count)"

退出码为0只表示脚本没有报告错误,page_limit也会返回0;仍应核对状态和字段。CSV数量与unique_records一致只能证明导出一致,不能证明每个字段或全部网站记录都正确。

无头Selenium运行10页并显示last_page、退出码0和100条CSV记录

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仍然运行浏览器,只是不显示窗口。

powershellpowershell
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20 --headless
bashbash
.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产品页选择代理,再用本文的单页脚本完成接入验证。 确认字段、出口和翻页结果都符合预期后,再扩大采集范围。

常见问题

安装Selenium,用webdriver.Chrome()启动Chrome,打开目标页,等待必需记录可读后,在记录容器内提取字段,再用Python的csv模块保存。多页抓取时,先验证翻页完成,再读取下一批数据。

最新文章

  • 酒店数据采集教程:用Python校验房价,配置Socks5.IO代理并导出CSV

  • Python股票数据爬虫教程:抓取股价、校验行情与配置SOCKS5代理

  • Discord怎么解绑Xbox?电脑、手机操作与语音故障排查

全球代理 IP 网络示意

立即接入全球代理 IP 网络,让每一次连接更快更稳

开始免费试用