diff --git a/doc/TASK_DEV.md b/doc/TASK_DEV.md index eff395c..093f0e7 100644 --- a/doc/TASK_DEV.md +++ b/doc/TASK_DEV.md @@ -244,18 +244,26 @@ from .generic import task # 触发 @register_task(当前唯一任务类型) `cmp_op` 取值:`等于` / `不等于` / `包含` / `不包含`(`==`/`!=`/`contains` 这类别名也认)。 不填 = 老行为(只看元素在不在)。 +**`cmp_value` 可以填多个值**(换行或 `|` 分隔,一行一个):多个之间是 **OR**—— +"这几个号都算我的",任一命中即命中;否定式(不等于/不包含)则是"**一个都不许出现**"。 +⚠ **逗号不是分隔符**("抖音号:123,456" 这种文本本身带逗号,拆了就永远匹配不上)。 + 例子(抓元素拿到的是 `android.widget.TextView text:抖音号:35377983067 id:com.ss.android.ugc.aweme:id/506`): ```json {"type": "if_el", "params": { "selector_type": "resourceId", "selector_value": "com.ss.android.ugc.aweme:id/506", - "cmp_op": "包含", "cmp_value": "35377983067", "timeout": 3, - "then": [{"type": "stop_self", "params": {"reason": "登录的不是目标抖音号"}}], + "cmp_op": "包含", + "cmp_value": "35377983067\n35377983068", // 多个号,任一命中即可 + "timeout": 3, + "then": [{"type": "stop_self", "params": {"reason": "登录的不是我的号"}}], "else": []}} ``` -要判断"**不是**这个号"就用 `不等于`(`cmp_op=不等于` 时"界面上是别的号"会命中,正好用来抓 -"号被换/掉登录")。 +要判断"**不是**我的号"就用 `不等于`("界面上是别的号"会命中,正好用来抓"号被换/掉登录")。 + +日志会写清"采集到什么、几个候选、命中的是哪个值",例如: +`采集到 '抖音号:35377983068' 包含 2 个候选值 → 符合,命中『35377983068』`。 **两条必须知道的规则**(都为了不误判): diff --git a/static/admin/editor.js b/static/admin/editor.js index 4557ce3..780bdc9 100644 --- a/static/admin/editor.js +++ b/static/admin/editor.js @@ -431,9 +431,11 @@ var _stepEditor={ [['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']] .map(function(o){return '';}).join('')+ ''+ - '
'+ - '
'; + '
'+ + ''+ + '
一行一个(也认 | 分隔):任意一个命中就算命中——多个号的场景把号都列上。'+ + '逗号不当分隔符,因为要比对的文本本身常带逗号
'; h+=_selRowHtml(p, (dyn?'':'
'+ '
'+ @@ -447,6 +449,7 @@ var _stepEditor={ +'
文本比对(可选):填了就把找到的那个元素的文本和"比对的值"比,' +'比中了才算命中。例:元素文本是 抖音号:35377983067,' +'想判断"这台登录的是不是这个号",选包含、值填 35377983067 即可(要整段一模一样才用"等于")。' + +'多个值一行一个,任一命中即可——"这几个号都算我的"。' +'
⚠ 元素没找到时一律算未命中(走"未找到时"分支)——"没读到"不会被当成"和我设的不一样"。')+'
', true); } // 共享:触发概率(所有步骤通用,0~100) diff --git a/tasks/generic/task.py b/tasks/generic/task.py index 69bbe51..016ccb4 100644 --- a/tasks/generic/task.py +++ b/tasks/generic/task.py @@ -145,26 +145,62 @@ _CMP_ALIASES = { CMP_OPS = tuple(_CMP_ALIASES) -def _text_matches(got, op, want): - """元素文本比对:got(采集到的)对 want(用户设的)做 op 判断。 - - op 见 `_CMP_ALIASES`(等于/不等于/包含/不包含)。认不出的 op 一律返回 False - ——条件判断"看不懂就算不命中",比猜一个更容易排查。 - """ - got = (got or "").strip() - want = (want or "").strip() +def _canon_op(op): + """运算符规范化成中文那个(认别名);认不出返回 None。""" key = (op or "").strip().lower() for canon, aliases in _CMP_ALIASES.items(): if key in [a.lower() for a in aliases]: - if canon == "等于": - return got == want - if canon == "不等于": - return got != want - if canon == "包含": - # 想匹配空串等于"永远命中",那是误配,当不命中处理 - return bool(want) and want in got - return bool(want) and want not in got - return False + return canon + return None + + +def split_cmp_values(raw): + """把"比对的值"拆成多个候选(**任一命中即可**,即 OR 语义)。 + + 分隔符只有两个:**换行** 和 `|`。 + **刻意不拆英文/中文逗号**——要比对的文本本身就常常带逗号 + (如 `抖音号:123,456`),拆了就永远匹配不上。界面上是一行一个。 + """ + if raw is None: + return [] + s = str(raw).replace("\r\n", "\n").replace("\r", "\n").replace("|", "\n") + return [v.strip() for v in s.split("\n") if v.strip()] + + +def _cmp_hit(got, op, want): + """核心比对:返回 (是否命中, 命中的那个值/惹事的值)。 + + 多个候选值之间是 **OR**("这个号或那个号都算我"): + · 等于 → 文本等于**任一**候选即命中 + · 包含 → 文本含**任一**候选即命中 + · 不等于 → 文本**不在**任何候选里才命中 + · 不包含 → **一个**候选都不出现才命中 + 认不出的运算符、或候选全为空 → 一律不命中("看不懂就算不命中",比猜一个更好排查)。 + """ + got = (got or "").strip() + wants = split_cmp_values(want) + canon = _canon_op(op) + if canon is None or not wants: + return False, "" + if canon == "等于": + hit = got in wants + return hit, (got if hit else "") + if canon == "包含": + for v in wants: + if v in got: + return True, v + return False, "" + if canon == "不等于": + return (got not in wants), "" + for v in wants: # 不包含 + if v in got: + return False, v # 命中了"不该出现"的那个 -> 条件不成立 + return True, "" + + +def _text_matches(got, op, want): + """元素文本比对(布尔版,多处调用只看结果)。""" + return _cmp_hit(got, op, want)[0] class GenericStepsWorker(BaseWorker): @@ -619,6 +655,9 @@ class GenericStepsWorker(BaseWorker): 先按选择器**取到元素的文本**,再拿它和 cmp_value 比(等于/不等于/包含/不包含), 比对结果才是命中与否。例:`resourceId=com.ss.android.ugc.aweme:id/506` 的文本是 `抖音号:35377983067`,cmp_op=包含、cmp_value=35377983067 就是"这台上登录的是它"。 + + `cmp_value` **可以填多个值**(换行或 `|` 分隔,见 `split_cmp_values`), + 多个之间是 **OR**——"这几个号都算我的"。要跑多个号的场景就靠它。 返回 True=命中 / False=未命中 / None=缺选择器或 OCR 不可用。 """ sel_type = params.get("selector_type", "xpath") @@ -698,8 +737,14 @@ class GenericStepsWorker(BaseWorker): else: if got is None: got = self._read_element_text(d, sel_type, sel_val) - hit = _text_matches(got, cmp_op, cmp_val) - detail = f"采集到 '{got}' {cmp_op} '{cmp_val}' → {'符合' if hit else '不符合'}" + hit, hit_val = _cmp_hit(got, cmp_op, cmp_val) + n_val = len(split_cmp_values(cmp_val)) + if hit: + extra = f",命中『{hit_val}』" if hit_val else "" + else: + extra = f",因为出现了『{hit_val}』" if hit_val else "" + detail = (f"采集到 '{got}' {cmp_op} {n_val} 个候选值 → " + f"{'符合' if hit else '不符合'}{extra}") found = hit elif cmp_op and sel_type == "screen": _log.warning(f"[{self.serial}] 屏幕状态没有文本可比,已忽略文本比对设置")