做了什么:
- 包改为分层: tcaptcha/{protocol,telemetry,solvers,ocr,adapters,models,tests}
* protocol/ client.py transport.py proxy.py
* telemetry/ crypto.py values.py key.py opcodes.py
disasm.py program.py layout.py modules.py
* solvers/ text_click.py icon_click.py icon_click_rotate.py slider.py
* ocr/ engines.py slicer.py
* adapters/ one_ms.py
- tools/ 里原本是**包运行时依赖**的 tdc_{disasm,program,layout,modules}.py 并入 telemetry/
(放在 tools/ 属于分层倒置),tools/ 只留 CLI: tdc_key_pure.py / tdc_profile.py / cdp_*.js
- crypto.py 529 -> 340 行: 切除「成功样本模板回填」与「Node 插桩兜底」两条老路;
generate_telemetry 只剩 python_values 一种模式,传别的会直接报错并说明原因
- 求解器改名分清: solver.py -> solvers/icon_click.py(client 实际在用的 YOLO+ResNet18 ONNX),
icon_click_solver.py -> solvers/icon_click_rotate.py(仅测试在用,留作对照)
- pyproject: 去掉 ultralytics/onnx(训练栈已删),补上真正在用的 onnxruntime
删除了什么:
- 雨云整条线 + dist/rainyun_qinglong(104M 旧快照)
- 模板回填: models/success_collect_template.b64、tdc_worker.js、analyze_collect_layout.py
- 「补环境」时代浏览器 JS: chrome_fetch/manual_captcha/probe_captcha/solve_in_browser/tdc_browser
- 旧 VM 探索工具 10 个(tdc_collectgen/tdc_slotmap/tdc_lift/tdc_vm_run/tdc_query/tdc_probe 等)
+ 8 个基于它们的旧 verify 脚本
- 训练/标注/评估: train_classifier/train_detector/prepare_yolo/web_labeler/collector/
siamese/evaluate/benchmark_verify
- tcaptcha/core_js/(60 个分析产物)、TOOLCHAIN_REPORT.md(整篇在描述已删工具)
- 3 个已无任何引用的 PyTorch 权重:icon_classifier.pt 43M / yolo_detector.pt 5.9M /
siamese_matcher.pt 1.6M —— 线上只跑 onnx,tcaptcha/ 117M -> 66M
为什么:
- 项目后面要专门承载「滑块 / 文字点选 / 图标点选」三种求解器;扁平包 + 运行时依赖混在
tools/ + 两条已废弃的 collect 生成路线,会让后续扩展持续踩坑
怎么验证:
- .venv/bin/python -m tcaptcha.selftest 全绿
- scripts/verify/verify_offline.py 逐槽全等 + 分段几何一致
- scripts/verify/verify_values.py 4 份真机明文 152/152 逐槽全等
- scripts/verify/send_test.py --mode python_values 1ms 签到页文字点选 errorCode=0
- 回滚点: tag backup/pre-refactor-20261012
453 lines
20 KiB
Python
453 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
tdc_modules.py —— **按案例 §5.3 的字符串指纹识别 37 个采集器模块**。
|
||
|
||
> "从 v1 的全部错位到指纹方案的全部正确,关键不是更复杂的分析,
|
||
> 而是换了一个更好的问题 —— 不再问『这个模块返回什么值』,
|
||
> 而是问『这个模块的代码长什么样』。" —— docs/tdc-案例.md §5.3
|
||
|
||
为什么必须做这件事(2026-10-12 实测,推翻项目早期假设):
|
||
* **模块 id 每份构建重新分配**。两份构建清单交集的 28 个模块里,
|
||
`id -> 语义` 完全相同的只有 **1/11**(模块 8 在 A 是 `navigator.platform`,
|
||
在 B 是 canvas 指纹)。按 id 对齐必然错位。
|
||
* 但**模块代码里的字符串集合跨构建完全不变**:采集 userAgent 的模块
|
||
一定含 `'userAgent'`;采集时区的一定含 `'getTimezoneOffset'`。这就是不变量。
|
||
|
||
本模块做三件事:
|
||
1. `module_strings()` —— 取一个模块的**可达**字符串集合(含内部闭包,递归委派)
|
||
2. `resolve_delegate()` —— 解析**委派链**(见下)
|
||
3. `identify()` —— 用指纹规则表把模块判成语义名
|
||
|
||
委派链(本项目新发现,案例未提但实测必需):
|
||
清单里有 6 个模块**自身不采集任何东西**,身体只是 `exports.get = require(N)`:
|
||
项8 模块21 -> require(22)
|
||
项9 模块24 -> require(25)
|
||
项27 模块49 -> require(31)
|
||
项31 模块53 -> require(0)
|
||
它们的字符串集合只有 {'Array','Object','exports','get'} —— **零区分性**,
|
||
必须顺着委派链跟到真正的采集器才能识别。这 6 个就是案例 §5.2 说的
|
||
"6 个无 .get 模块"的来源之一。
|
||
|
||
用法:
|
||
from tdc_modules import analyze
|
||
info = analyze("har/1ms_dianxuan/tdc.js")
|
||
for row in info:
|
||
print(row["item"], row["module"], row["name"], row["strings"][:4])
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||
|
||
from tcaptcha.telemetry import layout
|
||
from tcaptcha.telemetry import program as tp
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 通用字符串(每个模块都有,无区分性,进指纹前必须剔除) #
|
||
# --------------------------------------------------------------------------- #
|
||
NOISE: Set[str] = {
|
||
"Array", "Object", "exports", "get", "Math", "RegExp", "String", "Number",
|
||
"JSON", "set", "reset", "on", "length", "slice", "prototype", "constructor",
|
||
"push", "apply", "call", "bind", "toString", "valueOf", "hasOwnProperty",
|
||
}
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 指纹规则表(案例 §5.3)—— 每个语义名对应一组「必须出现的字符串」 #
|
||
# --------------------------------------------------------------------------- #
|
||
# 形式: 语义名 -> (必需串集合, 权重提示)
|
||
# 判定: 模块的区分串集合 **包含** 某个规则的必需串 -> 命中该规则
|
||
# 形式: (语义名, 必需串, 禁用串)
|
||
# 判定: required ⊆ strings 且 forbidden ∩ strings == ∅
|
||
#
|
||
# ⚠️ 规则必须**按 specificity 从高到低排**(必需串多的在前)。
|
||
# 早期版本只按列表顺序取第一个命中,结果 webdriver_detect 被误判成 user_agent
|
||
# —— 两者都只要求 1 个串,而 user_agent 恰好排在前面。
|
||
# 给特征明显的规则补上更多必需串(如 webdriver 要求 2 个自动化标记),
|
||
# 它们就会天然压过 user_agent 这类单串规则。
|
||
#
|
||
# fp_salt 之所以需要 forbidden: 它和 iframe_detect 的串集合几乎相同
|
||
# (都是 appendChild/body/createElement/document),唯一区别是 iframe_detect
|
||
# 多一个 contentWindow。用禁用串把两者分开。
|
||
FINGERPRINTS: List[Tuple[str, Set[str], Set[str]]] = [
|
||
# ---- 复合指纹(多必需串,天然压过单串规则)----
|
||
("screen_fingerprint", {"screen", "width", "height", "availHeight", "colorDepth"}, set()),
|
||
("webrtc_ip", {"RTCPeerConnection", "createDataChannel", "createOffer"}, set()),
|
||
("webgl_renderer", {"UNMASKED_RENDERER_WEBGL", "WEBGL_debug_renderer_info"}, set()),
|
||
("webgl_vendor", {"UNMASKED_VENDOR_WEBGL", "WEBGL_debug_renderer_info"}, set()),
|
||
("webdriver_detect", {"$cdc_asdjflasutopfhvcZLmcfl_", "__webdriver_script_fn"}, set()),
|
||
("canvas_fingerprint", {"ClientJS,org <canvas> 1.0", "#069"}, set()),
|
||
("dom_compliance", {"::marker", "CSS"}, set()),
|
||
("template_url", {"?rand=*", "URL"}, set()),
|
||
("charset", {"characterSet", "charset"}, set()),
|
||
("viewport_size", {"clientWidth", "clientHeight"}, set()),
|
||
("time_since_load", {"getTime", "load"}, set()),
|
||
# 轨迹采集器:真机实测其字符串集是鼠标/触摸事件名(2026-10-12 修正)。
|
||
# 旧规则写成 {"stringify"} —— 那抓到的是另一个只做 JSON.stringify 的模块,
|
||
# 结果「空数组」被记成 trajectory,真正的轨迹却在 window_time 名下。
|
||
("trajectory", {"mouseMoveEvent", "touchStartEvent"}, set()),
|
||
("struct_76_33", {"stringify"}, set()),
|
||
# ---- 单串规则(放最后,避免压过上面的复合规则)----
|
||
("user_agent", {"userAgent"}, set()),
|
||
("platform", {"platform"}, set()),
|
||
("languages", {"languages"}, set()),
|
||
("hardware_concurrency", {"hardwareConcurrency"}, set()),
|
||
("cookie_enabled", {"cookieEnabled"}, set()),
|
||
("timezone_offset", {"getTimezoneOffset"}, set()),
|
||
("itoken", {"TDC_itoken"}, set()),
|
||
("guid_token", {r"^\d+:\d+$"}, set()),
|
||
("referrer", {"TCaptchaReferrer"}, set()),
|
||
("location_href", {"href"}, {"?rand=*", "TCaptchaReferrer"}),
|
||
("iframe_detect", {"contentWindow"}, set()),
|
||
("console_detect", {"console"}, set()),
|
||
("touch_detect", {"ontouchstart"}, set()),
|
||
("window_time", {"getTime"}, set()),
|
||
("constant_98k", {"98k"}, set()),
|
||
("screen_width", {"screen", "width"}, set()),
|
||
("screen_height", {"screen", "height"}, set()),
|
||
("screen_color_depth", {"colorDepth"}, set()),
|
||
]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 结构兜底规则(案例 §5.3: "对于没有区分性字符串的通用模块,
|
||
# 用 .get 函数的指令数 + entry 函数的指令数 + 特定指令来区分")
|
||
# --------------------------------------------------------------------------- #
|
||
# 形式: (语义名, 判定函数) —— 只在**字符串规则全落空**时启用。
|
||
#
|
||
# ⚠️ 命名纪律:下面这些名字**只描述「观测到的结构分组」,不猜测业务语义**。
|
||
# 早期版本把 entry48/get9/call1 那组臆断成 "itoken_relay" —— 那是没有证据的猜测。
|
||
# 现在统一用 "struct_<entry>_<get>[_call]" 这种**纯结构标签**:
|
||
# 它诚实地表达「我们只知道它长这样,还不知道它采集什么」,
|
||
# 同时又因为跨构建稳定,仍然可以当**对齐锚点**用。
|
||
#
|
||
# 实测(4 份全新在线构建 har/online/20261012-01*,2026-10-12):
|
||
# entry135/get121/… -> 每份构建恰好 1 个 => fp_salt(随机串每次都变,只能靠结构)
|
||
# entry46/get9/… -> 每份构建恰好 1 个 => 稳定分组
|
||
# entry48/get9/call1 -> 每份构建恰好 1 个 => 稳定分组
|
||
# entry40/get9/… -> 每份构建 **2 个** => 组内无法再分,需运行时取样才能定
|
||
STRUCT_RULES = [
|
||
("fp_salt", lambda s: s["entry_instr"] >= 120 and s["get_instr"] >= 100),
|
||
("struct_46_9", lambda s: s["entry_instr"] == 46 and s["get_instr"] == 9),
|
||
("struct_48_9_call", lambda s: s["entry_instr"] == 48 and s["get_instr"] == 9
|
||
and s["call_func"]),
|
||
("struct_40_9", lambda s: s["entry_instr"] == 40 and s["get_instr"] == 9),
|
||
]
|
||
|
||
|
||
def struct_signature(p: tp.Program, reg: Dict[int, int], mid: int) -> Dict:
|
||
"""取一个模块的**结构签名**(案例 §5.3 兜底用)。
|
||
|
||
entry_instr = 模块工厂函数体的指令数
|
||
get_instr = `.get` 闭包(JUMP 目标)函数体的指令数
|
||
"""
|
||
entry = reg.get(mid)
|
||
if entry is None:
|
||
return {"entry_instr": -1, "get_instr": -1, "call_func": False,
|
||
"closure_kind": ""}
|
||
code = p.function_code(entry)
|
||
ops = Counter(inst["name"] for inst in code)
|
||
get_n = -1
|
||
for inst in code:
|
||
if inst["name"] == "JUMP" and inst["operands"]:
|
||
try:
|
||
get_n = len(p.function_code(inst["operands"][0]))
|
||
break
|
||
except Exception:
|
||
continue
|
||
return {
|
||
"entry_instr": len(code),
|
||
"get_instr": get_n,
|
||
"call_func": ops.get("CALL_FUNC", 0) > 0,
|
||
"closure_kind": _closure_kind(p, entry),
|
||
}
|
||
|
||
|
||
def _closure_kind(p: tp.Program, entry: int) -> str:
|
||
"""模块唯一闭包**最后写入什么** —— 用于拆开结构完全相同的模块。
|
||
|
||
背景(2026-10-12,4 份全新在线构建):
|
||
有 2 个模块 entry 都是 40 条、.get 都是 9 条、指令逐条相同,
|
||
只差一处 JUMP 绝对地址(每构建不同,不能当特征)。
|
||
真正的区别在**闭包体**(CREATE_CLOSURE 的 target,不是 JUMP 目标):
|
||
|
||
A: new Array()[0] = "" (PUSH_EMPTY_STR; SET_PROP)
|
||
B: new Array()[0] = 1558325408 (PUSH_IMM <随机int32>; SET_PROP)
|
||
|
||
实测每份构建恰好 empty×1 + rand×1 —— 稳定、可静态消歧。
|
||
|
||
返回: "empty" | "rand" | "const" | ""(无闭包)
|
||
"""
|
||
cls = [c for c in p.closures if p.owner(c.pc) == entry]
|
||
if not cls:
|
||
return ""
|
||
try:
|
||
body = p.function_code(cls[0].target)
|
||
except Exception:
|
||
return ""
|
||
imms = [inst["operands"][0] for inst in body
|
||
if inst["name"] == "PUSH_IMM" and inst["operands"]]
|
||
if any(v > 1_000_000 for v in imms):
|
||
return "rand"
|
||
if any(inst["name"] == "PUSH_EMPTY_STR" for inst in body):
|
||
return "empty"
|
||
return "const" if imms else ""
|
||
|
||
|
||
def _identify_struct(p: tp.Program, reg: Dict[int, int], mid: int) -> Optional[str]:
|
||
"""字符串规则落空时的兜底识别。"""
|
||
sig = struct_signature(p, reg, mid)
|
||
for name, pred in STRUCT_RULES:
|
||
try:
|
||
if pred(sig):
|
||
return name
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
|
||
def _clean(s: str) -> str:
|
||
return s.strip()
|
||
|
||
|
||
def module_strings(p: tp.Program, reg: Dict[int, int], mid: int,
|
||
depth: int = 3) -> Set[str]:
|
||
"""取一个模块的**可达**字符串集合(模块自身 + 内部闭包,递归 depth 层)。"""
|
||
entry = reg.get(mid)
|
||
if entry is None:
|
||
return set()
|
||
by_owner: Dict[int, Set[str]] = defaultdict(set)
|
||
for pc, s in p.strings:
|
||
by_owner[p.owner(pc)].add(s)
|
||
|
||
seen: Set[int] = set()
|
||
frontier = {entry}
|
||
out: Set[str] = set()
|
||
for _ in range(depth):
|
||
nxt: Set[int] = set()
|
||
for e in frontier:
|
||
if e in seen:
|
||
continue
|
||
seen.add(e)
|
||
out |= by_owner.get(e, set())
|
||
for c in p.closures:
|
||
if p.owner(c.pc) == e and c.target not in seen:
|
||
nxt.add(c.target)
|
||
frontier = nxt
|
||
if not frontier:
|
||
break
|
||
return out
|
||
|
||
|
||
def resolve_delegate(p: tp.Program, reg: Dict[int, int], mid: int,
|
||
max_hops: int = 6) -> List[int]:
|
||
"""解析模块 m 的委派链: `exports.X = require(N)` 里的 N。
|
||
|
||
返回 [m, N1, N2, ...](含自身)。取不到委派时返回 [m]。
|
||
|
||
形状(实测): 模块体里出现 `LOAD_REG_IMM; PUSH_IMM N; CALL_FUNC` 且 N 在
|
||
registry 里 -> 该模块委派给 N。有些模块 require 的是自己,要防环。
|
||
"""
|
||
chain: List[int] = [mid]
|
||
seen = {mid}
|
||
cur = mid
|
||
for _ in range(max_hops):
|
||
entry = reg.get(cur)
|
||
if entry is None:
|
||
break
|
||
code = p.function_code(entry)
|
||
nxt: Optional[int] = None
|
||
for k, inst in enumerate(code):
|
||
if inst["name"] != "PUSH_IMM" or not inst["operands"]:
|
||
continue
|
||
if k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM" \
|
||
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC":
|
||
cand = int(inst["operands"][0])
|
||
if cand in reg and cand not in seen:
|
||
nxt = cand
|
||
break
|
||
if nxt is None:
|
||
break
|
||
chain.append(nxt)
|
||
seen.add(nxt)
|
||
cur = nxt
|
||
return chain
|
||
|
||
|
||
def _has(strings: Set[str], token: str) -> bool:
|
||
"""判断 token 是否出现在字符串集合里。
|
||
|
||
两种模式:
|
||
* 普通 token -> **精确**相等
|
||
* `前缀*` 结尾 -> **子串/前缀**匹配
|
||
|
||
为什么需要后者(踩过的坑): `?rand=` 在真机里是 `?rand=1519713624347`
|
||
(带会话随机数),精确匹配永远命中不了,导致 template_url 被误判成
|
||
location_href。凡是"带运行时随机后缀"的串都必须用前缀匹配。
|
||
"""
|
||
if token in strings:
|
||
return True
|
||
if token.endswith("*"):
|
||
stem = token[:-1]
|
||
return any(stem in s for s in strings)
|
||
return False
|
||
|
||
|
||
def _matches(strings: Set[str]) -> List[Tuple[str, int, Set[str]]]:
|
||
"""列出所有命中的规则,按 specificity(必需串个数)降序。"""
|
||
hits: List[Tuple[str, int, Set[str]]] = []
|
||
for name, required, forbidden in FINGERPRINTS:
|
||
if not all(_has(strings, t) for t in required):
|
||
continue
|
||
if any(_has(strings, t) for t in forbidden):
|
||
continue
|
||
hits.append((name, len(required), required))
|
||
hits.sort(key=lambda x: -x[1])
|
||
return hits
|
||
|
||
|
||
def _identify(strings: Set[str]) -> Tuple[str, Set[str]]:
|
||
"""用指纹规则判定语义名。返回 (名字, 命中的必需串)。
|
||
|
||
取**必需串最多**的那条规则(specificity 优先),而不是列表里第一条 ——
|
||
这样 `webdriver_detect`(2 个必需串)不会被 `user_agent`(1 个)抢走。
|
||
"""
|
||
hits = _matches(strings)
|
||
if not hits:
|
||
return "unknown", set()
|
||
return hits[0][0], hits[0][2]
|
||
|
||
|
||
def disambiguate(rows: List[Dict]) -> None:
|
||
"""给**重名**的项加稳定后缀,使 37 个锚点尽可能唯一(原地修改 name)。
|
||
|
||
为什么需要这一步(2026-10-12 实测,只看 har/online/ 全新样本):
|
||
字符串规则会把不同采集器判成同一个名字。跨 4 份新构建统计,重名是**稳定的**:
|
||
|
||
user_agent x2 -> entry48(委派链) / entry68(直判) 每份各 1 个 ✓ 可消歧
|
||
window_time x3 -> entry279 / entry40 / entry48(委派链) 每份各 1 个 ✓ 可消歧
|
||
struct_40_9 x2 -> 结构完全相同 每份各 2 个 ✗ 静态不可分
|
||
|
||
消歧后缀用**结构签名**(entry/get 指令数 + 是否含 CALL_FUNC),因为它是
|
||
跨构建稳定的实测分组依据,而不是猜的语义。
|
||
|
||
⚠️ struct_40_9 那组每份构建固定 2 个、结构完全一致 —— 这是**静态识别的能力上限**。
|
||
要靠运行时取样(TDC.getData 对照)才能分开。本函数会如实保留重名,
|
||
并在 row["ambiguous_static"] 上标记,调用方据此决定是否需要运行时标定。
|
||
"""
|
||
groups: Dict[str, List[Dict]] = defaultdict(list)
|
||
for r in rows:
|
||
groups[r["name"]].append(r)
|
||
|
||
for name, members in groups.items():
|
||
if len(members) <= 1:
|
||
continue
|
||
for r in members:
|
||
s = r.get("struct") or {}
|
||
tag = "e%s_g%s%s%s" % (s.get("entry_instr", "-"),
|
||
s.get("get_instr", "-"),
|
||
"_call" if s.get("call_func") else "",
|
||
# 结构完全相同时,用"闭包最后写入什么"再拆一层
|
||
("_" + s["closure_kind"]) if s.get("closure_kind") else "")
|
||
# 同组内的 tag 是否唯一?唯一才能消歧
|
||
r["_tag"] = tag
|
||
tags = [r["_tag"] for r in members]
|
||
can_split = len(set(tags)) == len(tags)
|
||
for r in members:
|
||
r["ambiguous_static"] = not can_split
|
||
r["name_unique"] = "%s#%s" % (name, r["_tag"]) if can_split else name
|
||
if can_split:
|
||
for r in members:
|
||
r["name"] = r["name_unique"]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 主入口 #
|
||
# --------------------------------------------------------------------------- #
|
||
def analyze_prog(p: tp.Program, depth: int = 3) -> List[Dict]:
|
||
"""分析一个**已加载**的 Program(供调用方复用,避免重复解析 tdc.js)。"""
|
||
reg = dict(p.module_registry(0))
|
||
_mid, _entry, seq = layout.find_manifest_module(p)
|
||
|
||
rows: List[Dict] = []
|
||
for i, m in enumerate(seq):
|
||
own = module_strings(p, reg, m, depth=depth)
|
||
chain = resolve_delegate(p, reg, m)
|
||
# 顺着委派链往下找,直到能识别出语义(或链走完)
|
||
name, hit = _identify(own - NOISE)
|
||
used_chain = False
|
||
if name == "unknown" and len(chain) > 1:
|
||
for deeper in chain[1:]:
|
||
s = module_strings(p, reg, deeper, depth=depth)
|
||
n2, h2 = _identify(s - NOISE)
|
||
if n2 != "unknown":
|
||
name, hit = n2, h2
|
||
used_chain = True
|
||
break
|
||
# 字符串规则全落空 -> 启用案例 §5.3 的结构兜底(按指令数识别)
|
||
via_struct = False
|
||
if name == "unknown":
|
||
n3 = _identify_struct(p, reg, m)
|
||
if n3:
|
||
name, hit = n3, set()
|
||
via_struct = True
|
||
sig = sorted((own - NOISE))
|
||
rows.append({
|
||
"item": i,
|
||
"module": m,
|
||
"name": name,
|
||
"hit": sorted(hit),
|
||
"delegate": chain if len(chain) > 1 else [],
|
||
"via_delegate": used_chain,
|
||
"via_struct": via_struct,
|
||
"struct": struct_signature(p, reg, m),
|
||
"ambiguous_static": False,
|
||
"strings": sig,
|
||
})
|
||
disambiguate(rows)
|
||
return rows
|
||
|
||
|
||
def analyze(path: str, depth: int = 3) -> List[Dict]:
|
||
"""分析一份 tdc.js,返回清单每一项的识别结果(按清单顺序)。"""
|
||
return analyze_prog(tp.load(path), depth=depth)
|
||
|
||
|
||
def main() -> int:
|
||
import argparse
|
||
import json
|
||
|
||
ap = argparse.ArgumentParser(description="按字符串指纹识别 37 个采集器模块")
|
||
ap.add_argument("path")
|
||
ap.add_argument("--json", default=None, help="把结果写成 JSON")
|
||
ap.add_argument("--only-unknown", action="store_true", help="只列出未识别的")
|
||
args = ap.parse_args()
|
||
|
||
rows = analyze(args.path)
|
||
unknown = [r for r in rows if r["name"] == "unknown"]
|
||
print("文件 : %s" % args.path)
|
||
print("清单项 : %d 已识别 %d 未识别 %d"
|
||
% (len(rows), len(rows) - len(unknown), len(unknown)))
|
||
print()
|
||
print("项 模块 语义名 委派链 区分串")
|
||
print("-" * 108)
|
||
for r in rows:
|
||
if args.only_unknown and r["name"] != "unknown":
|
||
continue
|
||
dele = "->".join(str(x) for x in r["delegate"]) if r["delegate"] else ""
|
||
mark = " *" if r["via_delegate"] else " "
|
||
print("%2d %-5d %s%-21s %-17s %s"
|
||
% (r["item"], r["module"], mark, r["name"], dele,
|
||
r["strings"][:5]))
|
||
if args.json:
|
||
Path(args.json).write_text(
|
||
json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
print("\n结果 -> %s" % args.json)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|