Playwright 元素定位:一个 width>50 过滤器把 42px 的输入框删掉了

我的浏览器自动化脚本挂在一个很蠢的地方:

lua 复制代码
tag input not found while adding 'Python'

"没找到输入框。"

但我刚截过图。输入框明明在页面上,我甚至能量出它的位置。

这中间我花了两小时,反复改选择器。最后发现:它一直找得到。是我自己在找到之后,又把它从候选列表里删掉了。

下面是我撞上的三道假信号。每一道都长得像"我在仔细检查",实际上都是在丢掉正确的答案。

假信号一:我把几何位置当成了身份

我需要在发布面板里定位标签输入框。我的做法是:找出所有可能的输入框,取最靠上的那个。

python 复制代码
ins = [...document.querySelectorAll('input.byte-select__input')]
ins.sort((a, b) => a.r.y - b.r.y)
const el = ins[0].el;      // "最靠上的那个"

第一次它成功了。 第一个字段(添加标签)就在最上面。

然后它悄悄坏了。

因为加完一个标签之后,DOM 里的顺序变了。而"最靠上"这个假设不再成立 ------ 我的后续输入落到另一个字段里去了。

具体后果是:我要往「标签」里填的三个词,第二、三个跑进了「创作话题」。而创作话题是可选字段 ------ 界面上完全正常,没有任何报错,提交按钮就在下面等着我按。

再往前一步,我就把一串没人看的字符串带进了正式发布。

"最靠上"不是身份,是一次侥幸。

规则

锚定结构,不要锚定坐标。

位置和顺序是会变的状态。能不能定位到一个控件,应该取决于它在结构里的位置,而不是它在屏幕上的位置:

python 复制代码
# 不要这样
ins.sort(by_y); el = ins[0]

# 要这样:找到那个 label 是"添加标签:"的容器,再取它里面的输入框
for (const fi of document.querySelectorAll('.form-item')) {
    const lab = fi.querySelector('.label');
    if (lab.innerText.trim() !== '添加标签:') continue;
    return fi.querySelector('input');     // 结构关系,不受顺序影响
}

结构是稳定的,位置是流动的。

假信号二:我的过滤器把目标删掉了

上面那个方案里,我加了一条看起来很负责的过滤:

python 复制代码
.filter(o => o.r.width > 50 && o.r.height > 10)

这条过滤删掉了我要找的东西。

那个输入框空的时候只有 42 像素宽(它随输入内容增长)。

42 < 50,所以它被排除了。然后脚本非常诚实、非常自信地报告:

css 复制代码
tag input not found

我盯着这句话看了很久,因为这句话是真的------在它自己那个已经过滤过的世界里,确实找不到。

而"过滤器存在"这件事,是我写的,我却忘了。

我以为我在观察,其实我在筛选。

规则

过滤器不是观察,是决定。查不到东西时,先怀疑过滤器,再怀疑事实。

那条规则最后是怎么被用上的

我列了一张表,把所有候选的原始属性打出来,不做任何过滤:

ini 复制代码
.byte-select__input   w=42   h=24   y=271   closest('.tag-input')=true
.byte-select__input   w=0    h=0    y=0     closest('.tag-input')=false
.byte-select__input   w=42   h=24   y=530   closest('.tag-input')=false

一眼就看到了那个 42。 而在此之前,我"知道"宽度过滤是合理的,所以我从来没去看它到底删掉了什么。

看到原始数据之后,答案通常不需要推理。

假信号三:类名存在 ≠ 元素可见

我需要判断"发布面板是不是开着"。我写的检查是:

javascript 复制代码
!!document.querySelector('.publish-popup')

它在两种完全不同的状态下都返回 true。

因为 .publish-popup 这个类名在"发布"按钮上也有:

元素 类名 尺寸
发布按钮 publish-popup publish-popup with-padding 62 × 32
发布面板 publish-popup publish-popup with-padding **active** 560 × 799

同一个类名,一个是按钮,一个是面板。我的检查永远通过。

于是我的脚本以为面板开着,去操作里面那些 0×0 的元素,然后"元素不可见"。

规则

问"它是不是可见、有没有尺寸",不要问"这个类名存不存在"。

python 复制代码
# 不要这样
ok = document.querySelector('.publish-popup') !== null

# 要这样
for (const el of document.querySelectorAll('.tag-input input')) {
    const r = el.getBoundingClientRect();
    if (r.width > 0 && r.height > 0) return true;    // 真的渲染出来了
}

类名匹配会同时命中"容器"和"里面随便什么东西"。渲染尺寸不会撒谎。

让我脱困的那一行打印

三道假信号我都改完之后,我以为好了。还是不行。

rust 复制代码
+ tag 'Python'  (matched option 'Python')
  tag chips: 1 -> 1        ← 加了,但没加进去

日志说"匹配到了选项",芯片数量说"没变化"。两个信号打架。

这时候我做了一件本该更早做的事:不打印我自己的推断,直接打印那个字段的真实值。

python 复制代码
typed = page.evaluate("() => document.querySelector('.tag-input input').value")
print(f"input value after typing: {typed!r}")

输出:

css 复制代码
input value after typing: ''

空的。 我敲进去的所有字符,一个都没进去。

这句话让整个问题变得确定:焦点从来没有落到那个输入框上。

原因还是那个 42 像素 ------ 我为了"保险"给点击点加了个最小边距:

python 复制代码
w = max(real_width, 60)          # 我以为这更安全
click(x + w / 2, y + h / 2)      # 结果点到了输入框右边外面

我以为的安全边距,把我自己推出了目标。

改成用 Playwright 的 locator 点击(它自己处理滚动和居中)之后,一次就对了。

规则

当"动作报告成功"和"状态说没成功"冲突时,相信状态。

我的日志说 matched option 'Python' ------ 那是我的判断 。 value = '' 是事实。

日志记录的是我以为发生了什么;状态记录的是发生了什么。 两者冲突时,只有一个能作为下一步的依据。

而且这条还有个推论:要打印原始状态,不要打印你对状态的理解。

python 复制代码
print(f"clicked the tag option")          # 我的推断 ------ 可能错
print(f"input value: {value!r}")          # 事实 ------ 不会骗人

我前面两小时都在打印第一种。

把三道假信号放在一起看

假信号 我以为我在做的 实际发生的
几何位置当身份 精确定位 一次侥幸,状态一变就失效
宽度过滤 排除干扰项 把目标删掉了
类名存在性 检查状态 命中了同名按钮,永远为真

三个都是"看起来更严谨"的操作。

它们不是粗心,恰恰相反 ------ 我在"仔细地"排序、"谨慎地"过滤、"规范地"用类名判断。每一个都让代码看起来更专业,同时悄悄地把正确答案排除在外。

严谨的动作如果不检查自己的前提,就只是精致的错误。

如果你也在写浏览器自动化

这三条是我现在会先做的:

  1. 定位用结构关系,不用坐标或顺序。 找到那个有明确文本的标签/容器,再取它里面的元素。
  2. 任何过滤器都要能被单独关掉验证一次。 尤其是有阈值的(width > N、y > M)。先打印未过滤的原始列表,再决定过什么。
  3. 判断元素状态用 getBoundingClientRect() 的尺寸,不用类名。 类名是给人看的,尺寸是给渲染引擎算的。

还有一条和工作流有关的:

  1. 动作之后立刻读回状态。 不要"填了就当作填上了"。填完之后把那个字段的值打出来 ------ 一个空字符串能省你两小时。

本文所有代码片段、报错文本与输出均为实际运行记录,问题已修复,规则已应用到后续脚本中。

相关推荐
larance1 小时前
[菜鸟教程] 机器学习教程十课-Python 机器学习应用
人工智能·python·机器学习
栖凤1 小时前
Java 的 try-catch 在 Agent 里失效了,我用了 5 个模式才兜住
java·前端·python
Yyyyyy~1 小时前
【python】函数
python
金銀銅鐵1 小时前
[Java] 借助GUI展示class文件的版本号
后端·python·ai编程
外收内放1 小时前
Python基础语法练习题(62原始版本及其优化版本)
开发语言·python
棉猴1 小时前
玩游戏学Python4-字符串
python·游戏·字符串·pygame·玩游戏
东来也_八门2 小时前
我用麦当劳开放的 MCP,把那个最冷门的营养接口做成了真实配餐求解器
python
AI日报派送佬2 小时前
Ultralytics YOLO 模型训练技巧与最佳实践
人工智能·python·深度学习·yolo·机器学习·计算机视觉
zhaowangji2 小时前
mujoco仿真(机械臂推动正方体到指定位置)
人工智能·python