系列 · 影刀 × Agent 开发实战 第 2 篇 / 共 2 篇 教程

自动化实战:CSS 定位怎么写才稳定?

从稳定属性、属性匹配、范围控制到影刀里的文字定位、唯一性验证和排错路线,整理网页自动化中更可维护的 CSS 定位方法。

作者:黄撑 更新于 2026-07-13

刚开始学自动化,CSS 定位很容易被讲成“背几个语法”:#id.class[name="xxx"]。但真实项目里,真正难的不是写出一个能用的选择器,而是写出一个稳定、可读、出错后好排查的选择器。

这篇文章不再停留在 CSS 入门。目标是让实习生能从“会写选择器”,进一步走到“知道为什么这样写、什么时候不用这样写、怎么验证它是不是可靠”。

CSS SELECTOR稳定定位不是把路径写长,而是按层级缩小目标

先命中元素,再提高匹配精度,最后把范围限定到弹窗、表格行或业务卡片里,并验证结果是否唯一。

01基础命中button[data-action="submit"]

先找到大致目标,避免坐标点击。

02精准匹配[class*="comments--"]

用等于、包含、开头和结尾匹配处理动态属性。

03范围控制.dialog .confirm

先锁定弹窗、表格行或卡片,再找里面的按钮。

04唯一验证len(elements) == 1

不是“能找到”就结束,而是确认只找到目标元素。

本文默认场景是网页自动化,尤其是影刀编码版里常见的 browser.find_by_css()browser.find_all_by_css()browser.find_by_xpath()。如果只是临时点一次页面,写得能用就行;如果要交给 RPA 长期跑,就必须把定位当成可维护代码来写。

先把定位分成三层能力

CSS 定位可以先分成三层,不要一上来就陷入语法细节。

层级解决什么问题典型写法
基础命中通过标签、id、class、属性找到元素#submit.primaryinput[name="keyword"]
精准匹配对属性做等于、包含、开头、结尾等匹配[data-id^="order-"][class*="comments--"]
范围控制先限定弹窗、表格行、卡片,再找内部按钮.dialog .confirmtr[data-id="1"] button

入门阶段只要知道第一层。真正做自动化项目,至少要掌握第二层和第三层。

先快速过一遍基础:这些够你开始写定位

基础选择器不需要背很多,先记住它们分别适合什么场景。

写法例子适合什么时候用
标签buttoninput只做大范围候选,不建议单独点击
id#submit-orderid 稳定且页面唯一
class.primary.button.primaryclass 有业务含义,不是随机样式名
属性input[name="username"]自动化里最常用,优先看 namedata-*aria-label
组合input.form-input[name="keyword"]单个条件不唯一时,再加必要条件
范围.dialog .confirm页面有多个同名按钮时,先限定弹窗、表格行或卡片

一个登录输入框可能长这样:

<input
  id="username"
  class="login-input"
  name="username"
  placeholder="请输入用户名"
  type="text"
/>

如果 name="username" 稳定,就优先写:

input[name="username"]

不优先写:

input[placeholder="请输入用户名"]

原因很简单:name 更像程序字段,placeholder 更像给用户看的提示文案,后者更容易被产品文案调整影响。

基础部分先记住一句话:优先找能表达功能的稳定属性,不要一上来复制浏览器生成的完整路径。

属性匹配:从入门到中等水平的关键

很多页面没有干净的 id,也没有专门给自动化准备的 data-testid。这时只会写 [name="xxx"] 还不够,需要掌握属性匹配。

写法含义适合场景
[attr="value"]属性值完全等于精确匹配 nametypedata-action
[attr^="value"]属性值以某段开头id 或 class 有固定前缀
[attr$="value"]属性值以某段结尾文件名、链接、状态后缀
[attr*="value"]属性值包含某段文本动态 class 有稳定片段
[attr~="value"]属性值按空格拆分后包含某个词class 这类空格分隔属性
`[attr=“value”]`属性值等于 value 或以 value- 开头

可以先按这个判断:

真实场景优先写法例子
属性完全稳定精确匹配[data-action="submit"]
前缀稳定,后面变化开头匹配[id^="order-"]
后缀稳定结尾匹配[href$=".xlsx"]
中间有稳定片段包含匹配[class*="comments--"]
class 是多个独立词空格词匹配[class~="primary"]

越模糊的匹配,越要验证数量。尤其是 *=,它不是“更高级”,只是“更宽松”。

精确匹配

input[name="keyword"]
button[data-action="submit"]

这是最推荐的属性写法。只要属性稳定,精确匹配最清楚,也最不容易误伤。

开头匹配

tr[data-order-id^="202607"]

表示 data-order-id202607 开头。它适合批量找某一类元素,但不适合直接点击,因为它通常会匹配多个。

自动化里更常见的用法是先找一组候选:

rows = browser.find_all_by_css('tr[data-order-id^="202607"]', timeout=10)

然后再根据文本、状态或其它属性继续筛选。

结尾匹配

a[href$=".xlsx"]
img[src$=".png"]

这种写法适合下载链接、图片、文件类型判断。但如果页面里同类链接很多,仍然要先限定范围。

包含匹配

[class*="comments--"]

这类写法常用于前端框架生成的动态 class。

例如页面里出现:

<div class="comments--ChxC7GEN">评价区</div>

后半段 ChxC7GEN 看起来像随机字符,但 comments-- 是稳定语义,就可以用包含匹配:

div[class*="comments--"]

不过要注意:*= 是模糊匹配,范围容易变大。能用 data-*namearia-label 时,不要优先用它。

空格词匹配

button[class~="primary"]

它表示 class 列表里有一个完整的 primary 词。下面两个元素都能匹配:

<button class="button primary large">提交</button>
<button class="primary">提交</button>

但这个不会匹配:

<button class="primary-button">提交</button>

这和 [class*="primary"] 不一样。*= 只要包含文本就行,~= 要求是独立词。

关系选择器:控制范围的核心

CSS 选择器不只是“找某个元素”,还可以表达元素之间的关系。

写法含义示例
A BA 内部任意层级的 B.dialog button
A > BA 的直接子元素 B.menu > li
A + BA 后面紧挨着的 Blabel + input
A ~ BA 后面同级的所有 B.title ~ button

后代选择器:最常用

.dialog .confirm

表示在 .dialog 内部找 .confirm。弹窗、卡片、表格行、表单区域都常用这种思路。

直接子元素:避免层级过深误匹配

.menu > li

只找 .menu 下面第一层的 li,不会继续往更深层级找。

相邻兄弟:根据前后结构找元素

label[for="password"] + input

它表示找到 for="password" 的 label 后,紧挨着它的 input。

这种写法在后台表单里偶尔有用,但依赖结构,页面布局调整后可能失效。能用 name 还是优先用 name

伪类选择器:能用,但要知道风险

伪类选择器可以表达状态或位置,但自动化里不要滥用。

写法含义自动化建议
:nth-child(n)第 n 个子元素最后再用,容易受数据变化影响
:first-child第一个子元素适合固定结构,不适合动态列表
:last-child最后一个子元素适合固定结构,不适合分页表格
:not(...)排除某类元素可用于排除禁用项或隐藏项
:checked被选中的复选框或单选框适合状态判断
:disabled禁用状态适合判断按钮是否可用
:has(...)包含某个子元素需按运行环境验证,不要默认所有环境都稳定

:not() 用于排除干扰项

button:not([disabled])

表示找没有 disabled 属性的按钮。实际项目里通常还要加范围:

.order-dialog button.submit:not([disabled])

:has() 可以表达“包含关系”,但要谨慎

tr:has([data-status="待处理"]) button.process

这个选择器的意思是:找包含 data-status="待处理" 的表格行,再找这一行里的处理按钮。

它表达能力很强,但自动化项目里要注意两点:

  1. 影刀内置浏览器或实际运行环境是否支持,需要运行验证;
  2. 如果可以通过 tr[data-order-id="..."] 直接定位,就不要为了炫技使用 :has()

中等水平不是会用更复杂的语法,而是知道什么时候不用复杂语法。

自动化里的 CSS 定位四步法

写 CSS 时不要一上来复制浏览器生成的超长路径。先按下面四步判断,定位通常会更短,也更容易维护。

1
先确定区域

页面主体 / 弹窗 / 表格 / 商品卡片

2
找稳定特征

id / name / data-* / aria-label

3
组合必要条件

用尽量少的条件唯一定位

4
验证是否唯一

匹配结果是 0 个 / 1 个 / 多个

推荐优先级
  1. 1数据属性 / name / aria-label
  2. 2稳定且有意义的 id
  3. 3稳定且有意义的 class
  4. 4标签 + 属性组合
  5. 5nth-child,最后再考虑
常见误区△ 复制超长路径△ 只靠随机 class△ 模糊匹配不验证数量

这张图只保留方法,不再重复每一种语法。实际写代码时,可以把它当成检查顺序:先看范围,再看稳定特征,再组合,最后验证数量。

CSS、XPath、get_text() 怎么选?

这一步要提前讲,因为真实页面并不是所有元素都适合用 CSS。

有稳定属性

优先使用 CSS

没有稳定属性,但按钮文字明确

使用 XPath 按文字定位

需要先限定弹窗、表格或卡片范围,再判断文字

CSS 找候选元素,再用 get_text() 筛选

元素在 iframe、弹窗或动态加载区域里

先处理页面上下文和加载状态,再谈选择器

也就是说,CSS 是自动化里的主力定位方式,但不是唯一方式。遇到“只有文字稳定”的按钮时,硬凑 CSS 反而会把问题复杂化。

按文字定位:不要写 :contains()

标准 CSS 不能根据元素内部显示的文字进行匹配。下面这种写法并不是有效的 CSS:

button[text="提交订单"]
button:contains("提交订单")

在影刀编码版中,如果按钮文字稳定,可以直接用 XPath:

button = browser.find_by_xpath(
    '//button[normalize-space(.)="提交订单"]',
    timeout=10,
)
button.click()

如果需要先限定弹窗范围,再按文字筛选,可以先用 CSS 拿候选元素:

buttons = browser.find_all_by_css(".order-dialog button", timeout=10)

for button in buttons:
    if button.get_text().strip() == "提交订单":
        button.click()
        break
else:
    raise RuntimeError('未找到文本为“提交订单”的按钮')

这里的关键点是:CSS 负责缩小范围,文字判断发生在 Python 代码里。

怎么在浏览器里查看和验证?

第一步:打开开发者工具

在 Chrome 或 Edge 中按 F12,或者在目标元素上点击右键,选择“检查”。

第二步:观察元素特征

重点看这些内容:

  • 标签名;
  • id;
  • class;
  • name;
  • placeholder;
  • data 属性;
  • aria-label。

不要看到一大段 HTML 就慌。先问自己:有没有稳定 id?有没有能表达功能的属性?是否需要先限定一个区域?

第三步:在 Console 验证

document.querySelectorAll('input[name="username"]')

重点看返回数量:

数量说明下一步
0 个没找到,或者元素还没有加载检查语法、页面状态和 iframe
1 个通常是理想结果再确认它是不是目标元素
多个定位范围太大增加区域或稳定条件

在影刀里也可以用 find_all_by_css() 做类似验证:

elements = browser.find_all_by_css('input[name="username"]', timeout=10)

if len(elements) != 1:
    raise RuntimeError(f"定位结果不唯一,当前数量={len(elements)}")

elements[0].input("test_user")

一个好的定位不能只看“现在能找到”,还要看“以后是否容易失效”。

选择器明明对,为什么还是找不到?

这部分是新手最容易卡住的地方。很多时候不是 CSS 写错了,而是页面状态不对。

现象常见原因排查方向
Console 能找到,影刀找不到页面还没加载完成先等待页面加载、弹窗出现或列表渲染完成
CSS 写法没问题,但结果是 0 个元素在 iframe 里先确认当前操作的是不是正确 iframe 或页面对象
只在点击后才出现元素是弹窗、下拉框或异步区域先触发页面动作,再查找元素
明明看得见,代码找不到元素可能在 Shadow DOM、虚拟列表或特殊控件里需要单独探测页面结构,不要直接猜选择器
find_by_css() 报匹配多个选择器范围太大改用 find_all_by_css() 看数量,再增加范围条件
昨天能跑,今天失效id、class 或页面结构动态变化改用稳定属性、包含匹配或业务范围定位
使用 :has() 不稳定运行环境支持不确定在影刀实际运行环境验证,不能只看浏览器 Console

影刀里排查定位问题时,我通常先用 find_all_by_css() 看数量,而不是直接 find_by_css()

selector = '.order-dialog button.submit:not([disabled])'
elements = browser.find_all_by_css(selector, timeout=10)

if len(elements) == 0:
    raise RuntimeError(f"未找到元素:{selector}")

if len(elements) > 1:
    raise RuntimeError(f"定位不唯一:{selector},数量={len(elements)}")

elements[0].click()

这样写虽然多几行,但报错信息会更清楚。后续排查时,能马上知道是“没找到”,还是“找到太多”。

实战案例:从“能找到”到“可维护”

案例一:登录输入框

<input type="text" name="username" placeholder="请输入账号">

推荐:

input[name="username"]

不优先使用:

input[placeholder="请输入账号"]

原因是 name="username" 更像程序字段,placeholder 更像给用户看的提示文案,后者更容易因为产品文案调整而变化。

案例二:弹窗确认按钮

<div class="dialog">
  <button class="cancel">取消</button>
  <button class="confirm">确认</button>
</div>

推荐:

.dialog .confirm

不要只写:

.confirm

因为页面其它区域也可能有确认按钮。自动化里经常不是元素本身难找,而是同名元素太多

案例三:表格某一行的处理按钮

<tr data-order-id="20260713001">
  <td>20260713001</td>
  <td>待处理</td>
  <td><button class="process-button">处理</button></td>
</tr>

推荐:

tr[data-order-id="20260713001"] .process-button

这个写法的重点不是 .process-button,而是前面的 tr[data-order-id="20260713001"]。先定位业务记录,再操作这一行内部按钮,这比直接找第几个按钮稳定得多。

案例四:动态 class 的评价区

<div class="comments--ChxC7GEN">评价内容</div>

如果确认 comments-- 是稳定前缀,可以写:

div[class*="comments--"]

但这类选择器必须验证数量:

areas = browser.find_all_by_css('div[class*="comments--"]', timeout=10)

if len(areas) != 1:
    raise RuntimeError(f"评价区定位不唯一,当前数量={len(areas)}")

这就是中等水平和入门水平的差别:入门只看“能不能找到”,中等水平会继续确认“是不是只找到了一个”。

案例五:把坏定位改造成好定位

很多新手会从浏览器里复制出这种选择器:

body > div:nth-child(3) > div > div:nth-child(2) > button:nth-child(2)

它的问题不是“不能用”,而是太依赖页面结构。只要页面多一个弹窗、多一层容器,或者按钮顺序变化,就可能点错。

如果页面结构是这样:

<div class="order-dialog">
  <button class="btn">取消</button>
  <button class="btn" data-action="confirm">确认</button>
</div>

更好的写法是:

.order-dialog button[data-action="confirm"]

如果没有 data-action,但弹窗范围和按钮文字稳定,就不要硬凑 CSS,可以用 CSS 加 get_text()

buttons = browser.find_all_by_css(".order-dialog button", timeout=10)

for button in buttons:
    if button.get_text().strip() == "确认":
        button.click()
        break
else:
    raise RuntimeError("未找到弹窗里的确认按钮")

这个案例要记住的不是某个固定答案,而是改造方向:少依赖页面层级,多依赖业务范围、稳定属性和可验证结果。

给实习生的练习

假设页面 HTML 如下:

<div class="login-panel">
  <input name="account" placeholder="请输入账号">
  <input name="password" type="password">
  <button class="login-button" data-action="login">登录</button>
</div>

先自己写,再展开答案:

查看参考答案
input[name="account"]
input[name="password"]
button[data-action="login"]

再加一个中等难度练习:

<div class="order-dialog">
  <div class="row" data-order-id="20260713001">
    <span class="status pending">待处理</span>
    <button class="btn btn-primary">处理</button>
  </div>
  <div class="row" data-order-id="20260713002">
    <span class="status done">已完成</span>
    <button class="btn btn-primary">查看</button>
  </div>
</div>

目标:点击订单 20260713001 这一行里的按钮。

查看参考答案
.order-dialog .row[data-order-id="20260713001"] button.btn-primary

这里分成三层:先限定弹窗 .order-dialog,再找到对应订单行 .row[data-order-id="20260713001"],最后操作这一行里的按钮。

学完后先记住这几条

1能在开发者工具中找到目标元素的 HTML
2能区分精确匹配、包含匹配、开头匹配和结尾匹配
3能用范围选择器避免点错同名按钮
4能在浏览器和影刀中验证选择器是否唯一
5知道什么时候该换 XPath 或 get_text(),而不是硬用 CSS
中等水平的 CSS 定位,不是写更多语法,而是能判断稳定性、控制范围,并验证结果。