uihound 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uihound-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: uihound
3
+ Version: 0.1.0
4
+ Summary: Browser-based Android UI inspector for HIL testing
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi<1.0,>=0.110
8
+ Requires-Dist: pydantic<3.0,>=2.9
9
+ Requires-Dist: pydantic-settings<3.0,>=2.5
10
+ Requires-Dist: uvicorn[standard]<1.0,>=0.27
11
+ Requires-Dist: uiautomator2<4.0,>=3.2
12
+ Requires-Dist: adbutils<3.0,>=2.0
13
+ Requires-Dist: lxml<7,>=4.9
14
+ Requires-Dist: Pillow<12.0,>=10.0
15
+
16
+ # UiHound · UI 猎犬
17
+
18
+ UiHound 是用于 Android 调试与自动化测试的网页工具。在浏览器中查看设备画面和 UI 节点、复制定位表达式、远程操作设备,并完成取色、截图与录屏。
19
+
20
+ ## 功能
21
+
22
+ | 功能 | 说明 |
23
+ |---|---|
24
+ | 元素检查 | UI 节点树、节点虚线边界、选中高亮、属性与坐标展示 |
25
+ | 元素定位 | u2 selector / XPath 搜索与复制,优先生成唯一属性定位 |
26
+ | 远程操作 | scrcpy 实时画面,点击、拖动、长按、双指滚动、主页、返回、后台任务 |
27
+ | 颜色取样 | 像素放大镜、坐标、HEX、RGB、HSB、OpenCV HSV,多点记录与复制 |
28
+ | 截图 | 全屏、元素和区域截图,选区移动、缩放及坐标微调 |
29
+ | 录屏 | 录制设备画面,保存为无音频 WebM,支持视频预览 |
30
+ | 文件管理 | 截图与录屏列表、复制文件、删除单个或全部、打开保存目录 |
31
+ | 工作区 | 可调节分栏、白天 / 黑夜主题、偏好记忆 |
32
+
33
+ ## 安装与启动
34
+
35
+ ### 环境准备
36
+
37
+ - Python 3.10 或以上版本。
38
+ - Android Platform Tools,确保 `adb` 已加入 PATH。
39
+ - macOS、Windows 或 Linux;新版 Chrome 或 Edge,使用本机地址访问。
40
+ - Android 设备开启 USB / 无线调试,并接受调试授权。
41
+
42
+ 在终端执行 `adb devices`,确认设备状态为 `device`。无线连接可先执行 `adb connect 地址:端口`。
43
+
44
+ ### 安装
45
+
46
+ 安装并启动:
47
+
48
+ ```powershell
49
+ pip install uihound
50
+ uihound
51
+ ```
52
+
53
+ 服务启动后自动打开 `http://127.0.0.1:8000`。在网页中选择设备或填写设备地址,点击“连接设备”。
54
+
55
+ 启动参数:
56
+
57
+ ```powershell
58
+ # 指定端口
59
+ uihound --port 8080
60
+
61
+ # 不自动打开浏览器
62
+ uihound --no-browser
63
+ ```
64
+
65
+ 升级到新版本:
66
+
67
+ ```powershell
68
+ pip install --upgrade uihound
69
+ ```
70
+
71
+ 升级后重启 `uihound`,刷新网页即可。
72
+
73
+ ## 使用方法
74
+
75
+ ### 检查元素:hierarchy
76
+
77
+ 点击设备画面上方的 hierarchy 图标,获取当前截图和 UI 节点树。点击画面或树节点查看属性,复制定位表达式;也可在搜索框输入 selector 或 XPath:
78
+
79
+ ```python
80
+ resourceId="com.example:id/title"
81
+ text="设置"
82
+ //*[@resource-id="com.example:id/title"]
83
+ ```
84
+
85
+ XPath 优先使用当前层级中唯一的 `resource-id`、`content-desc`、`text`,没有可用唯一属性时才使用层级路径。
86
+
87
+ **hierarchy 是静态快照,设备页面变化后需点击刷新。** 动画结束、画面稳定后再获取,便于对齐画面和节点。画面上方的边框按钮可显示或隐藏节点虚线。
88
+
89
+ ### 操作设备:remote
90
+
91
+ 点击 remote 图标进入实时操作模式。鼠标点击对应轻触,按住对应长按,拖动或触摸板双指滚动对应滑动。侧栏提供主页、返回、后台任务等操作。
92
+
93
+ 点击冻结按钮暂停网页画面,暂停期间禁用触摸,再次点击恢复。文本输入在“常用操作”页完成,支持 `Ctrl+Enter` 发送。
94
+
95
+ 首次使用 remote 会下载 scrcpy-server v3.3.4。离线环境可预先下载同版本官方文件,并通过 `UIHOUND_SCRCPY_SERVER` 指定路径。
96
+
97
+ ### 取色:color
98
+
99
+ 点击 color 获取当前无损画面,移动鼠标查看像素与颜色值。方向键微调 1px,`Shift+方向键` 微调 10px;点击或按 Enter 保存取样。最新记录置顶,每项可单独复制。
100
+
101
+ 刷新或切换画面会清除取样记录。OpenCV HSV 为范围换算值,边界颜色可能存在 1 个单位的舍入差异。
102
+
103
+ ### 截图与文件管理
104
+
105
+ 通过侧栏保存全屏截图,或选中元素后点击“元素截图”。区域截图先点击框选按钮,再拖出选区;可拖动选区、调整八个边角、输入坐标或用方向键微调。保存成功后自动清除选区。
106
+
107
+ hierarchy / color 截取当前静态画面;remote 全屏截图获取设备当前画面。截图不会包含节点边框。
108
+
109
+ 在“截图”页修改保存目录、查看缩略图、复制或删除文件。**删除会真实移除磁盘文件,操作前会确认;删除全部仅针对当前列表,不递归删除子目录。**
110
+
111
+ “复制文件”后可在目标文件夹使用系统粘贴快捷键(Windows `Ctrl+V`,macOS `Command+V`)粘贴。“打开目录”直接打开保存位置。这两项需要浏览器和服务运行在同一台电脑;文件剪贴板复制支持 Windows 和 macOS,Linux 暂不支持。
112
+
113
+ ### 录屏
114
+
115
+ 点击侧栏圆形录屏按钮开始,再次点击停止并保存;非 remote 模式会自动连接 remote 后开始录制。
116
+
117
+ 录屏默认保存在用户主目录下的 `UiHound\recordings` 文件夹。“录屏”页支持修改保存目录、播放、复制文件、删除和打开目录。录制内容不包含节点边框;冻结网页画面时仍会录制设备新帧。
118
+
119
+ 切换模式或断开设备会停止并保存录屏。录屏接近 240 MiB 时自动停止。**关闭网页前请等待保存完成**;上传失败可在录屏页重试,强制关闭或刷新会丢失未保存的视频。
120
+
121
+ ## 常用设置
122
+
123
+ 截图和录屏页可选择对应根目录内的子目录。需要更换根目录或调整画质时,在启动前设置环境变量:
124
+
125
+ ```powershell
126
+ $env:UIHOUND_SNAPSHOT_DIR = "D:\UiHoundData\snapshots"
127
+ $env:UIHOUND_RECORDING_DIR = "D:\UiHoundData\recordings"
128
+ $env:UIHOUND_SCRCPY_MAX_SIZE = "1920"
129
+ $env:UIHOUND_SCRCPY_MAX_FPS = "60"
130
+ $env:UIHOUND_SCRCPY_BIT_RATE = "8000000"
131
+ uihound
132
+ ```
133
+
134
+ 默认文件位置固定在用户主目录下,不受启动目录或安装目录影响:
135
+
136
+ | 文件 | Windows | macOS / Linux |
137
+ |---|---|---|
138
+ | 截图 | `%USERPROFILE%\UiHound\screenshots` | `~/UiHound/screenshots` |
139
+ | 录屏 | `%USERPROFILE%\UiHound\recordings` | `~/UiHound/recordings` |
140
+
141
+ 环境变量可覆盖默认根目录。升级前保存的文件不会自动移动或删除;浏览器记住的路径若已不在当前允许的根目录内,会提示并切换到默认目录。
142
+
143
+ remote 默认最长边 1920、目标 60 fps、码率 8 Mbps;实际流畅度取决于设备与连接状况。
144
+
145
+ 拖动分隔条调整设备、属性和节点树区域,顶部可重置布局。太阳 / 月亮按钮切换主题,浏览器会记住主题和分栏宽度。
146
+
147
+ ## 自动化调用
148
+
149
+ | 请求 | 参数 |
150
+ |---|---|
151
+ | `GET /device/status` | 设备连接状态 |
152
+ | `GET /snapshot/full` | 可选 `save_dir`、`filename` |
153
+ | `GET /snapshot/element` | 必填 `selector`;可选 `save_dir`、`filename` |
154
+ | `GET /snapshot/region` | 必填 `x1`、`y1`、`x2`、`y2`;可选 `save_dir`、`filename` |
155
+
156
+ 截图接口返回 `success`、`file_path`、`base64`、`timestamp`。`save_dir` 必须位于截图根目录内,`filename` 为 `.png` 文件名;同名文件不会覆盖。
157
+
158
+ 服务用于本机调试,请勿直接暴露到不受信任的网络。
@@ -0,0 +1,143 @@
1
+ # UiHound · UI 猎犬
2
+
3
+ UiHound 是用于 Android 调试与自动化测试的网页工具。在浏览器中查看设备画面和 UI 节点、复制定位表达式、远程操作设备,并完成取色、截图与录屏。
4
+
5
+ ## 功能
6
+
7
+ | 功能 | 说明 |
8
+ |---|---|
9
+ | 元素检查 | UI 节点树、节点虚线边界、选中高亮、属性与坐标展示 |
10
+ | 元素定位 | u2 selector / XPath 搜索与复制,优先生成唯一属性定位 |
11
+ | 远程操作 | scrcpy 实时画面,点击、拖动、长按、双指滚动、主页、返回、后台任务 |
12
+ | 颜色取样 | 像素放大镜、坐标、HEX、RGB、HSB、OpenCV HSV,多点记录与复制 |
13
+ | 截图 | 全屏、元素和区域截图,选区移动、缩放及坐标微调 |
14
+ | 录屏 | 录制设备画面,保存为无音频 WebM,支持视频预览 |
15
+ | 文件管理 | 截图与录屏列表、复制文件、删除单个或全部、打开保存目录 |
16
+ | 工作区 | 可调节分栏、白天 / 黑夜主题、偏好记忆 |
17
+
18
+ ## 安装与启动
19
+
20
+ ### 环境准备
21
+
22
+ - Python 3.10 或以上版本。
23
+ - Android Platform Tools,确保 `adb` 已加入 PATH。
24
+ - macOS、Windows 或 Linux;新版 Chrome 或 Edge,使用本机地址访问。
25
+ - Android 设备开启 USB / 无线调试,并接受调试授权。
26
+
27
+ 在终端执行 `adb devices`,确认设备状态为 `device`。无线连接可先执行 `adb connect 地址:端口`。
28
+
29
+ ### 安装
30
+
31
+ 安装并启动:
32
+
33
+ ```powershell
34
+ pip install uihound
35
+ uihound
36
+ ```
37
+
38
+ 服务启动后自动打开 `http://127.0.0.1:8000`。在网页中选择设备或填写设备地址,点击“连接设备”。
39
+
40
+ 启动参数:
41
+
42
+ ```powershell
43
+ # 指定端口
44
+ uihound --port 8080
45
+
46
+ # 不自动打开浏览器
47
+ uihound --no-browser
48
+ ```
49
+
50
+ 升级到新版本:
51
+
52
+ ```powershell
53
+ pip install --upgrade uihound
54
+ ```
55
+
56
+ 升级后重启 `uihound`,刷新网页即可。
57
+
58
+ ## 使用方法
59
+
60
+ ### 检查元素:hierarchy
61
+
62
+ 点击设备画面上方的 hierarchy 图标,获取当前截图和 UI 节点树。点击画面或树节点查看属性,复制定位表达式;也可在搜索框输入 selector 或 XPath:
63
+
64
+ ```python
65
+ resourceId="com.example:id/title"
66
+ text="设置"
67
+ //*[@resource-id="com.example:id/title"]
68
+ ```
69
+
70
+ XPath 优先使用当前层级中唯一的 `resource-id`、`content-desc`、`text`,没有可用唯一属性时才使用层级路径。
71
+
72
+ **hierarchy 是静态快照,设备页面变化后需点击刷新。** 动画结束、画面稳定后再获取,便于对齐画面和节点。画面上方的边框按钮可显示或隐藏节点虚线。
73
+
74
+ ### 操作设备:remote
75
+
76
+ 点击 remote 图标进入实时操作模式。鼠标点击对应轻触,按住对应长按,拖动或触摸板双指滚动对应滑动。侧栏提供主页、返回、后台任务等操作。
77
+
78
+ 点击冻结按钮暂停网页画面,暂停期间禁用触摸,再次点击恢复。文本输入在“常用操作”页完成,支持 `Ctrl+Enter` 发送。
79
+
80
+ 首次使用 remote 会下载 scrcpy-server v3.3.4。离线环境可预先下载同版本官方文件,并通过 `UIHOUND_SCRCPY_SERVER` 指定路径。
81
+
82
+ ### 取色:color
83
+
84
+ 点击 color 获取当前无损画面,移动鼠标查看像素与颜色值。方向键微调 1px,`Shift+方向键` 微调 10px;点击或按 Enter 保存取样。最新记录置顶,每项可单独复制。
85
+
86
+ 刷新或切换画面会清除取样记录。OpenCV HSV 为范围换算值,边界颜色可能存在 1 个单位的舍入差异。
87
+
88
+ ### 截图与文件管理
89
+
90
+ 通过侧栏保存全屏截图,或选中元素后点击“元素截图”。区域截图先点击框选按钮,再拖出选区;可拖动选区、调整八个边角、输入坐标或用方向键微调。保存成功后自动清除选区。
91
+
92
+ hierarchy / color 截取当前静态画面;remote 全屏截图获取设备当前画面。截图不会包含节点边框。
93
+
94
+ 在“截图”页修改保存目录、查看缩略图、复制或删除文件。**删除会真实移除磁盘文件,操作前会确认;删除全部仅针对当前列表,不递归删除子目录。**
95
+
96
+ “复制文件”后可在目标文件夹使用系统粘贴快捷键(Windows `Ctrl+V`,macOS `Command+V`)粘贴。“打开目录”直接打开保存位置。这两项需要浏览器和服务运行在同一台电脑;文件剪贴板复制支持 Windows 和 macOS,Linux 暂不支持。
97
+
98
+ ### 录屏
99
+
100
+ 点击侧栏圆形录屏按钮开始,再次点击停止并保存;非 remote 模式会自动连接 remote 后开始录制。
101
+
102
+ 录屏默认保存在用户主目录下的 `UiHound\recordings` 文件夹。“录屏”页支持修改保存目录、播放、复制文件、删除和打开目录。录制内容不包含节点边框;冻结网页画面时仍会录制设备新帧。
103
+
104
+ 切换模式或断开设备会停止并保存录屏。录屏接近 240 MiB 时自动停止。**关闭网页前请等待保存完成**;上传失败可在录屏页重试,强制关闭或刷新会丢失未保存的视频。
105
+
106
+ ## 常用设置
107
+
108
+ 截图和录屏页可选择对应根目录内的子目录。需要更换根目录或调整画质时,在启动前设置环境变量:
109
+
110
+ ```powershell
111
+ $env:UIHOUND_SNAPSHOT_DIR = "D:\UiHoundData\snapshots"
112
+ $env:UIHOUND_RECORDING_DIR = "D:\UiHoundData\recordings"
113
+ $env:UIHOUND_SCRCPY_MAX_SIZE = "1920"
114
+ $env:UIHOUND_SCRCPY_MAX_FPS = "60"
115
+ $env:UIHOUND_SCRCPY_BIT_RATE = "8000000"
116
+ uihound
117
+ ```
118
+
119
+ 默认文件位置固定在用户主目录下,不受启动目录或安装目录影响:
120
+
121
+ | 文件 | Windows | macOS / Linux |
122
+ |---|---|---|
123
+ | 截图 | `%USERPROFILE%\UiHound\screenshots` | `~/UiHound/screenshots` |
124
+ | 录屏 | `%USERPROFILE%\UiHound\recordings` | `~/UiHound/recordings` |
125
+
126
+ 环境变量可覆盖默认根目录。升级前保存的文件不会自动移动或删除;浏览器记住的路径若已不在当前允许的根目录内,会提示并切换到默认目录。
127
+
128
+ remote 默认最长边 1920、目标 60 fps、码率 8 Mbps;实际流畅度取决于设备与连接状况。
129
+
130
+ 拖动分隔条调整设备、属性和节点树区域,顶部可重置布局。太阳 / 月亮按钮切换主题,浏览器会记住主题和分栏宽度。
131
+
132
+ ## 自动化调用
133
+
134
+ | 请求 | 参数 |
135
+ |---|---|
136
+ | `GET /device/status` | 设备连接状态 |
137
+ | `GET /snapshot/full` | 可选 `save_dir`、`filename` |
138
+ | `GET /snapshot/element` | 必填 `selector`;可选 `save_dir`、`filename` |
139
+ | `GET /snapshot/region` | 必填 `x1`、`y1`、`x2`、`y2`;可选 `save_dir`、`filename` |
140
+
141
+ 截图接口返回 `success`、`file_path`、`base64`、`timestamp`。`save_dir` 必须位于截图根目录内,`filename` 为 `.png` 文件名;同名文件不会覆盖。
142
+
143
+ 服务用于本机调试,请勿直接暴露到不受信任的网络。
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "uihound"
7
+ version = "0.1.0"
8
+ description = "Browser-based Android UI inspector for HIL testing"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "fastapi>=0.110,<1.0",
13
+ "pydantic>=2.9,<3.0",
14
+ "pydantic-settings>=2.5,<3.0",
15
+ "uvicorn[standard]>=0.27,<1.0",
16
+ "uiautomator2>=3.2,<4.0",
17
+ "adbutils>=2.0,<3.0",
18
+ "lxml>=4.9,<7",
19
+ "Pillow>=10.0,<12.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ uihound = "uihound.main:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ uihound = ["static/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Browser-based Android UI inspector."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ model_config = SettingsConfigDict(env_prefix="UIHOUND_")
9
+
10
+ host: str = "127.0.0.1"
11
+ port: int = Field(default=8000, ge=1, le=65535)
12
+ snapshot_dir: Path = Field(default_factory=lambda: Path.home() / "UiHound" / "screenshots")
13
+ recording_dir: Path = Field(default_factory=lambda: Path.home() / "UiHound" / "recordings")
14
+ log_dir: Path = Field(default_factory=lambda: Path.cwd() / "logs")
15
+ open_browser: bool = True
16
+ screenshot_quality: int = Field(default=90, ge=1, le=100)
17
+ screen_stream_fps: int = Field(default=12, ge=1, le=60)
18
+ screen_stream_quality: int = Field(default=80, ge=1, le=100)
19
+ scrcpy_max_size: int = Field(default=1920, ge=0, le=8192)
20
+ scrcpy_max_fps: int = Field(default=60, ge=1, le=120)
21
+ scrcpy_bit_rate: int = Field(default=8_000_000, ge=100_000, le=100_000_000)
22
+
23
+
24
+ settings = Settings()
25
+ BASE_DIR = Path(__file__).resolve().parent
26
+ STATIC_DIR = BASE_DIR / "static"
27
+ SNAPSHOT_DIR = settings.snapshot_dir.expanduser().resolve()
28
+ RECORDING_DIR = settings.recording_dir.expanduser().resolve()
29
+ LOG_DIR = settings.log_dir.expanduser().resolve()
30
+ HOST = settings.host
31
+ PORT = settings.port
32
+ SCREENSHOT_QUALITY = settings.screenshot_quality
33
+ SCREEN_STREAM_FPS = settings.screen_stream_fps
34
+ SCREEN_STREAM_QUALITY = settings.screen_stream_quality
@@ -0,0 +1,123 @@
1
+ """Bounded, device-scoped screen snapshots used by hierarchy and color modes."""
2
+
3
+ import base64
4
+ import io
5
+ import re
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from collections import OrderedDict
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timezone
12
+ from typing import Any
13
+
14
+ from lxml import etree
15
+ from PIL import Image
16
+ from uiautomator2.xpath import PageSource
17
+
18
+ from .inspector import Inspector
19
+
20
+
21
+ @dataclass
22
+ class Inspection:
23
+ device: Any
24
+ image: Image.Image
25
+ xml: str | None
26
+ hierarchy: dict[str, Any]
27
+ created: float
28
+
29
+
30
+ class InspectionStore:
31
+ def __init__(self):
32
+ self._items: OrderedDict[str, Inspection] = OrderedDict()
33
+ self._lock = threading.Lock()
34
+
35
+ def capture(self, inspector: Inspector, with_hierarchy: bool) -> dict[str, Any]:
36
+ device = inspector.require_device()
37
+ image = inspector.screenshot().convert("RGB")
38
+ xml = device.dump_hierarchy() if with_hierarchy else None
39
+ hierarchy = inspector.hierarchy(xml) if xml is not None else {
40
+ "elements": [], "tree": None, "empty": True, "node_count": 0,
41
+ }
42
+ if inspector.device is not device:
43
+ raise RuntimeError("设备已切换,请重新获取画面")
44
+ snapshot_id = uuid.uuid4().hex
45
+ with self._lock:
46
+ self._items[snapshot_id] = Inspection(device, image, xml, hierarchy, time.monotonic())
47
+ while len(self._items) > 8:
48
+ self._items.popitem(last=False)
49
+ buffer = io.BytesIO()
50
+ image.save(buffer, format="PNG")
51
+ return {
52
+ **hierarchy, "snapshot_id": snapshot_id,
53
+ "image": base64.b64encode(buffer.getvalue()).decode("ascii"),
54
+ "width": image.width, "height": image.height,
55
+ "captured_at": datetime.now(timezone.utc).isoformat(),
56
+ }
57
+
58
+ def get(self, snapshot_id: str, inspector: Inspector) -> Inspection:
59
+ with self._lock:
60
+ item = self._items.get(snapshot_id)
61
+ if item is None or time.monotonic() - item.created > 1800:
62
+ raise ValueError("画面快照已过期,请点击刷新")
63
+ if item.device is not inspector.require_device():
64
+ raise ValueError("设备已切换,请点击刷新")
65
+ return item
66
+
67
+ def clear(self) -> None:
68
+ with self._lock:
69
+ self._items.clear()
70
+
71
+ def search(self, snapshot_id: str, query: str, inspector: Inspector) -> list[dict[str, Any]]:
72
+ item = self.get(snapshot_id, inspector)
73
+ if item.xml is None:
74
+ raise ValueError("当前为颜色快照,请点击 hierarchy 获取层级")
75
+ elements = item.hierarchy["elements"]
76
+ query = query.strip()
77
+ if query.startswith(("/", "(")):
78
+ source = PageSource.parse(item.xml)
79
+ try:
80
+ matches = source.root.xpath(query, namespaces={"re": "http://exslt.org/regular-expressions"})
81
+ if not isinstance(matches, list) or any(not isinstance(node, etree._Element) for node in matches):
82
+ raise ValueError("XPath 必须返回元素节点")
83
+ matched = set(matches)
84
+ return [
85
+ element for element in elements
86
+ if any(node in matched for node in source.root.xpath(element["xpath"]))
87
+ ]
88
+ except etree.XPathError as exc:
89
+ raise ValueError(f"XPath 无效:{exc}") from exc
90
+ selector = Inspector.parse_selector(query)
91
+ instance = selector.pop("instance", None)
92
+ if instance is not None and (type(instance) is not int or instance < 0):
93
+ raise ValueError("instance 必须是非负整数")
94
+ aliases = {"description": "contentDescription"}
95
+ def matches(element: dict[str, Any]) -> bool:
96
+ for key, expected in selector.items():
97
+ operation = "exact"
98
+ for suffix in ("Contains", "Matches"):
99
+ if key.endswith(suffix):
100
+ key, operation = key[:-len(suffix)], suffix
101
+ break
102
+ actual = element.get(aliases.get(key, key), "")
103
+ if operation == "Contains":
104
+ if not isinstance(expected, str):
105
+ raise ValueError("Contains 查询值必须是字符串")
106
+ if expected not in str(actual):
107
+ return False
108
+ elif operation == "Matches":
109
+ if not isinstance(expected, str):
110
+ raise ValueError("Matches 查询值必须是字符串")
111
+ try:
112
+ if not re.fullmatch(expected, str(actual)):
113
+ return False
114
+ except re.error as exc:
115
+ raise ValueError(f"正则表达式无效:{exc}") from exc
116
+ elif actual != expected:
117
+ return False
118
+ return True
119
+ found = [element for element in elements if matches(element)]
120
+ return found if instance is None else found[instance:instance + 1]
121
+
122
+
123
+ inspections = InspectionStore()