Selenium 实战复盘:脚本为什么总是随机失败

简介: Selenium是主流浏览器自动化工具,可真实模拟用户操作,专攻JavaScript渲染、登录态交互等动态页面场景。入门易,但写稳难——关键在理解页面状态,善用显式等待、Page Object等工程实践。

这事儿我有发言权。

Selenium 我从读研时就开始用。后来做新闻源采集、视频元数据,再到现在跑跨境电商页面,前前后后写过不少浏览器自动化脚本。它给我的感觉一直很矛盾:入门特别快,写稳特别难。

打开网页,输入关键词,点击按钮。半小时就会了。

结果呢?同一段代码,上午跑得好好的,下午突然提示元素不存在。你加了三秒等待,它下一次偏偏第四秒才出来。这能不烦?

先搞明白 Selenium 在干什么

Selenium 是一套浏览器自动化工具。它不是在后台简单下载网页源码,而是在控制一个真实浏览器。你平时能在 Chrome 里完成的动作,它基本都能模拟,比如打开页面、填写表单、点击按钮、切换窗口、上传文件、滚动页面和保存截图。

这也是 Selenium 和普通 HTTP 请求最大的差别。

如果目标页面的内容直接存在于 HTML 中,或者网站提供了稳定的公开 API,我一般不会先用 Selenium。启动一个浏览器要占内存,页面渲染还要花时间。为了读取一段静态文字,把整个 Chrome 拉起来,我图啥呢?

Selenium 真正适合的是动态页面。比如内容需要执行 JavaScript 才出现,搜索结果必须点击后加载,或者任务要在登录状态下完成一整套操作。到了这种场景,直接请求处理起来可能更绕,浏览器自动化反而省事。

第一次运行,别先折腾驱动

Python 项目建议放在独立虚拟环境里。安装过程很简单:

python -m venv .venv

Windows 激活环境:

.venv\Scripts\activate

macOS 或 Linux 激活环境:

source .venv/bin/activate

接着安装 Selenium:

pip install -U selenium

现代版本通常可以通过 Selenium Manager 处理常见浏览器驱动。以前那种先查 Chrome 版本,再找对应 ChromeDriver,下载完还要配置路径的流程,现在大多数情况下不用手动做了。

先跑一段最小代码:

from selenium import webdriver

driver = webdriver.Chrome()

try:
    driver.get("https://example.com")
    print(driver.title)
finally:
    driver.quit()

浏览器能打开,终端能输出网页标题,基础环境就没问题。

这里的 finally 别省。脚本中途报错时,它仍然会关闭浏览器。否则调试十几次,后台留下一排 Chrome 进程,内存一路往上走。那场面我见过。

离谱。

调试阶段我一般保留浏览器界面。至少脚本停住时,我能直接看到是按钮没出现,还是弹窗把页面挡住了。等流程跑稳以后,再切到无头模式:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")

driver = webdriver.Chrome(options=options)

无头模式也要设置窗口尺寸。很多网站用了响应式布局,窗口太窄时,桌面导航会折叠成手机菜单。页面布局一变,原来的按钮可能直接消失。代码本身没改,定位却失效了,这就很尴尬。

元素定位别追求长,追求稳

浏览器打开以后,Selenium 得先找到页面上的元素,才能点击或者输入。

常用定位方式没几种:

from selenium.webdriver.common.by import By

driver.find_element(By.ID, "username")
driver.find_element(By.NAME, "keyword")
driver.find_element(By.CSS_SELECTOR, "button.submit")
driver.find_element(By.XPATH, "//button[contains(text(), '查询')]")

我自己最常用的是 id 和 CSS Selector。如果前端提供了 data-testid 之类的测试属性,会更省心:

search_input = driver.find_element(
    By.CSS_SELECTOR,
    'input[data-testid="search-input"]'
)

search_input.send_keys("Selenium")

XPath 当然可以用。需要按文字、相邻关系或者复杂层级找元素时,它很方便。问题是,很多人会直接从浏览器开发者工具里复制完整 XPath:

/html/body/div[2]/div/div[3]/form/input

看上去非常精确,对吧?

前端只要多套一层 div,整条路径就废了。

我做跨境页面时尤其不爱依赖显示文字。同一个按钮,在英文环境叫 Search,切到其他语言以后文案就变了。脚本如果按按钮文字定位,地区一换就集体罢工。关键还是找稳定的业务属性,而不是赌页面永远不改。

为什么加了 sleep 还是会报错

9 月 17 日早上 9 点 12 分,我刚把工位上的黑咖啡打开,组里同事发来一段代码:

driver.get(url)
time.sleep(5)

button = driver.find_element(By.ID, "submit")
button.click()

他说已经等了五秒,为什么偶尔还是找不到按钮?

答案很简单。因为时间过去五秒,不等于按钮已经可以点击。

页面一秒加载完,脚本白等四秒。网络慢一点,按钮第六秒才出现,五秒结束后照样报错。把等待时间改成十秒,只是让脚本在大多数时候多等五秒,并没有解决判断条件的问题。

更合适的做法是显式等待:

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

wait = WebDriverWait(driver, 10)

button = wait.until(
    EC.element_to_be_clickable(
        (By.ID, "submit")
    )
)

button.click()

这段代码等的不是固定时长,而是“按钮已经可以点击”。按钮两秒后准备好,脚本两秒后继续;按钮八秒后准备好,脚本就等八秒。

如果只是要求元素进入页面结构,可以使用 presence_of_element_located。如果必须看得见,再用 visibility_of_element_located。遇到加载遮罩或者弹窗,也可以等它消失:

wait.until(
    EC.invisibility_of_element_located(
        (By.CSS_SELECTOR, ".loading-mask")
    )
)

time.sleep() 不是完全不能出现。调试动画、观察执行过程或者控制操作间隔时,偶尔用一下没问题。真正坑的是把它当成页面状态判断。

页面有没有准备好,应该问页面。

问时钟干什么?

一段完整表单代码应该怎么写

下面这段示例会打开测试表单,填写文字和密码,选择下拉项,勾选复选框,提交以后再验证结果:

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

options = webdriver.ChromeOptions()
options.add_argument("--window-size=1440,1000")

driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 10)

try:
    driver.get(
        "https://www.selenium.dev/selenium/web/web-form.html"
    )

    text_input = wait.until(
        EC.visibility_of_element_located(
            (By.NAME, "my-text")
        )
    )
    text_input.send_keys("Selenium 自动化测试")

    driver.find_element(
        By.NAME,
        "my-password"
    ).send_keys("demo-password")

    select_element = driver.find_element(
        By.NAME,
        "my-select"
    )
    Select(select_element).select_by_visible_text("Two")

    checkbox = driver.find_element(
        By.ID,
        "my-check-1"
    )
    if not checkbox.is_selected():
        checkbox.click()

    driver.find_element(
        By.CSS_SELECTOR,
        "button"
    ).click()

    message = wait.until(
        EC.visibility_of_element_located(
            (By.ID, "message")
        )
    )

    assert message.text == "Received!"
    driver.save_screenshot("form-result.png")

finally:
    driver.quit()

这已经是一段完整的浏览器自动化流程。浏览器启动后先进入页面,等输入框可见,再完成填写和提交。提交以后不盲等,而是等结果消息出现。确认文案符合预期后截图,无论中途是否报错,最终都关闭浏览器。

刚入门不用急着引入很多框架。先把这段代码看明白,再换成自己的页面。学习速度反而更快。

元素明明在页面上,为什么找不到

这种情况先别急着换选择器。

如果元素位于 iframe 中,Selenium 默认在主页面里查找,当然找不到。需要先切进去:

wait.until(
    EC.frame_to_be_available_and_switch_to_it(
        (By.CSS_SELECTOR, "iframe.payment-frame")
    )
)

driver.find_element(
    By.ID,
    "card-number"
).send_keys("1234")

操作完成后,再回到主页面:

driver.switch_to.default_content()

新标签页也是同一个道理。点击链接以后,浏览器虽然打开了新页面,Selenium 当前控制的却可能还是原窗口:

original_window = driver.current_window_handle
old_windows = set(driver.window_handles)

driver.find_element(
    By.LINK_TEXT,
    "查看详情"
).click()

wait.until(
    lambda d: len(d.window_handles) > len(old_windows)
)

new_window = (
    set(driver.window_handles) - old_windows
).pop()

driver.switch_to.window(new_window)
print(driver.title)

driver.close()
driver.switch_to.window(original_window)

很多所谓的“元素定位失败”,根本不是定位表达式写错了,而是查找上下文错了。人已经去了新页面,脚本还留在旧页面,这能找到才奇怪。

页面更新以后,别继续拿着旧元素

动态页面局部刷新后,原来保存的元素对象可能会失效。这时常见的报错是:

StaleElementReferenceException

我第一次遇到时,对着 CSS Selector 改了半天。后来才反应过来,选择器没问题,是页面已经变了,我还在操作更新前的对象。

例如先获取一批卡片,再逐个点进去:

cards = driver.find_elements(
    By.CSS_SELECTOR,
    ".result-card"
)

进入详情页再返回后,列表可能已经重新渲染。原来的 cards 不一定还能继续用。更稳的做法是每轮重新定位:

cards_locator = (By.CSS_SELECTOR, ".result-card")

count = len(
    wait.until(
        EC.presence_of_all_elements_located(cards_locator)
    )
)

for index in range(count):
    cards = wait.until(
        EC.presence_of_all_elements_located(cards_locator)
    )

    cards[index].click()

    # 在这里处理详情页
    print(driver.title)

    driver.back()

    wait.until(
        EC.presence_of_all_elements_located(cards_locator)
    )

页面状态变了,就重新找元素。

别跟旧对象谈感情。

项目写大以后怎么整理

脚本只有几十行时,把定位、操作和断言放在一起没什么问题。页面一多,同一个按钮选择器可能散落在十几个文件中。前端改一次页面,测试代码跟着全线报错。

这时可以引入 Page Object,把页面细节封装起来:

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


class LoginPage:
    URL = "https://example.com/login"

    USERNAME = (By.ID, "username")
    PASSWORD = (By.ID, "password")
    SUBMIT = (
        By.CSS_SELECTOR,
        "button[type='submit']"
    )

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def open(self):
        self.driver.get(self.URL)
        return self

    def login(self, username, password):
        self.wait.until(
            EC.visibility_of_element_located(
                self.USERNAME
            )
        ).send_keys(username)

        self.driver.find_element(
            *self.PASSWORD
        ).send_keys(password)

        self.driver.find_element(
            *self.SUBMIT
        ).click()

调用时只描述业务动作:

page = LoginPage(driver).open()
page.login("demo-user", "demo-password")

这样一来,页面改了选择器,只需要修改 LoginPage。测试用例不必知道按钮用了什么 CSS,也不用到处复制等待代码。

不过别刚写三十行脚本,就拆出十二个类。先跑通,再把重复部分整理出来。为了看起来专业而搭一大堆空架子,也挺折磨人的。

Selenium 能用来做网页采集吗

可以。特别是页面必须执行 JavaScript、经过点击或者登录后才显示内容时,Selenium 很实用。

问题是,工具能操作浏览器,不代表所有内容都可以随便获取。

我们组在自动化任务上线前,会先确认目标数据是否允许访问,操作会不会触发订单、评论或其他写入行为,是否涉及个人敏感信息,以及访问频率是否合理。验证码、登录保护和平台风控是安全边界,不是拿来练手的题目。

如果只是正常测试自己的系统,这些问题比较好控制。涉及第三方页面,就要先看平台规则和授权范围。脚本跑通只是技术问题,能不能跑则是另一回事。

真正需要掌握的不是 API

Selenium 的方法名随用随查就行。

真正决定脚本稳不稳的,是你能不能看懂页面状态。按钮还没出现,就等它出现;按钮被遮住,就等遮挡层消失;页面开了新窗口,就切换过去;页面重新渲染,就重新定位元素。

9 月 18 日晚上 10 点 40 分,我把初稿发给我家那位看。他扫完后问:“你是不是漏了一句最重要的?”

我问哪句。

他说:“成功一次,不叫稳定。”

行吧。

后端说这话,确实没毛病。一段 Selenium 脚本连续运行几十次,遇到加载波动和页面更新也能正确处理,才有资格交给定时任务。

能打开浏览器,只是开始。

相关文章
|
1天前
|
运维 监控 安全
阿里云轻量应用服务器控制台在哪?控制台支持哪些功能?
阿里云轻量应用服务器控制台是可视化管理平台,支持远程连接、重装系统、重置密码、云监控、防火墙配置等全流程运维操作,PC端与阿里云App均可便捷访问。阿里云轻量应用服务器官网:https://t.aliyun.com/U/dwftch
|
1天前
|
存储 SQL 缓存
一条查询从50毫秒变340毫秒|分片键选错之后我才看懂架构选型
从一次架构改造后查询变慢切入,按"写入点几个、数据切没切"分开三种架构形态,拆解缓存融合与多数派复制机制,对比各自的瓶颈位置,指出热点两条路线都解决不了,给出带量化指标的选型四步法。
|
1天前
|
运维 监控 安全
阿里云轻量应用服务器控制台在哪?入口链接找到了
阿里云轻量应用服务器控制台是统一管理后台,支持远程连接、重装系统、重置密码、云监控、防火墙配置等一站式运维操作,PC端或阿里云App均可快速访问。阿里云轻量应用服务器官网:https://t.aliyun.com/U/dwftch
|
1天前
|
数据采集 运维 监控
自动化包装生产线设备可视化管理方案
该工厂自动化包装产线通过“PLC+伺服+HMI”实现智能控制,现开展数字化改造:部署工业物联网平台,实现PLC数据上云,支持远程监控、故障告警、参数调控、OEE分析及MES/ERP系统对接,全面提升生产效率与管理水平。(239字)
|
1天前
|
人工智能 监控 安全
AI 智能体(AI Agent)的开发与上线
AI智能体是能自主感知、规划、调用工具并持续执行任务的智能系统。本文详解其六大阶段:场景定义与架构设计、技术栈选型、核心模块研发、测试评估与安全、部署上线及持续运维,涵盖单/多智能体架构、主流框架、RAG、安全护栏与数据飞轮等关键实践。(239字)
|
1天前
|
存储 人工智能 安全
千问办公企业版和个人版如何选择?阿里QwenWork版本如何收费?
阿里云千问办公(QwenWork)提供个人版(含免费版)与企业版:个人版免费试用送2000积分,标准/高级版折后78/158元/月;企业版按席位计费,标准版198元/月/席,旗舰版298元/月/席(100席起),支持VPC部署与统一管控。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
1天前
|
供应链 安全 BI
数据安全到底怎么做?权限、脱敏、水印、防泄漏、审计全讲明白
企业数据安全不止于权限管控,需构建覆盖“访问—使用—导出—流转—追溯”全链路的闭环体系:精准权限(资源/数据/字段/操作四层)、动态脱敏、可溯源水印、智能防泄漏(行为+出口双控)及实时审计。五维协同,方能在保障业务可用前提下守住安全边界。(239字)
|
1天前
|
人工智能 安全 数据挖掘
千问办公企业版怎么样?收费价格、核心能力、VPC网络、安全管控详细指南
千问办公企业版是面向组织协作的AI办公平台,含标准版(198元/席/月)与旗舰版(298元/席/月,100席起购),支持多模态生成、钉钉深度集成及企业级管控功能,提供席位订阅与积分包增购。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
1天前
|
运维 安全 数据安全/隐私保护
拒绝“裸奔”!企业终端文件加密与多场景防泄密硬核盘点
本文深度解析终端文件加密技术体系,涵盖手动加解密与配额管控、加密属性管理、内外发包安全交付、离线/个人模式、批量处理与邮件解密、移动端防护等七大模块,强调策略精细化与审批审计能力,助力企业在安全与效率间取得平衡。(239字)
|
1月前
|
缓存 开发工具 数据安全/隐私保护
Windows PowerShell 设置 HTTP 代理:系统级配置完整教程
Windows代理无“总开关”!浏览器、PowerShell、后台服务各走一套配置:WinINET(用户级)、WinHTTP(系统级)、环境变量(跨平台工具)。本文详解三者 PowerShell 配置、验证与恢复,助你精准穿透代理迷雾。

热门文章

最新文章