dp-cli 0.1.0__py3-none-any.whl
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.
- dp_cli/__init__.py +1 -0
- dp_cli/commands/__init__.py +12 -0
- dp_cli/commands/_utils.py +107 -0
- dp_cli/commands/browser.py +159 -0
- dp_cli/commands/element.py +259 -0
- dp_cli/commands/keyboard.py +126 -0
- dp_cli/commands/misc.py +136 -0
- dp_cli/commands/network.py +169 -0
- dp_cli/commands/page.py +204 -0
- dp_cli/commands/snapshot_cmd.py +391 -0
- dp_cli/commands/storage.py +222 -0
- dp_cli/commands/tab.py +203 -0
- dp_cli/main.py +47 -0
- dp_cli/output.py +97 -0
- dp_cli/session.py +201 -0
- dp_cli/snapshot/__init__.py +23 -0
- dp_cli/snapshot/a11y.py +671 -0
- dp_cli/snapshot/extract.py +158 -0
- dp_cli/snapshot/js_scripts.py +155 -0
- dp_cli/snapshot/utils.py +43 -0
- dp_cli-0.1.0.dist-info/METADATA +103 -0
- dp_cli-0.1.0.dist-info/RECORD +25 -0
- dp_cli-0.1.0.dist-info/WHEEL +5 -0
- dp_cli-0.1.0.dist-info/entry_points.txt +2 -0
- dp_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""快照与数据提取命令: snapshot / extract / query / find / inspect / dom"""
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from dp_cli.output import ok, error
|
|
9
|
+
from dp_cli.session import save_refs
|
|
10
|
+
from dp_cli.snapshot import (extract_structured, query_elements,
|
|
11
|
+
take_a11y_snapshot, render_a11y_text,
|
|
12
|
+
render_a11y_plain_text)
|
|
13
|
+
from dp_cli.snapshot.utils import suggest_locator
|
|
14
|
+
from dp_cli.commands._utils import session_option, _get_page, records_to_csv, resolve_locator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(cli):
|
|
18
|
+
|
|
19
|
+
@cli.command()
|
|
20
|
+
@session_option
|
|
21
|
+
@click.option('--mode',
|
|
22
|
+
type=click.Choice(['full', 'brief', 'text']),
|
|
23
|
+
default='full', show_default=True, help='快照模式')
|
|
24
|
+
@click.option('--selector', default=None, help='限定快照范围的 CSS 选择器')
|
|
25
|
+
@click.option('--format', 'fmt', type=click.Choice(['json', 'text']),
|
|
26
|
+
default='text', show_default=True, help='输出格式')
|
|
27
|
+
@click.option('--filename', default=None, help='保存到文件路径')
|
|
28
|
+
def snapshot(session, mode, selector, fmt, filename):
|
|
29
|
+
"""获取页面快照(基于浏览器原生 a11y tree,通用性极强)。
|
|
30
|
+
|
|
31
|
+
\b
|
|
32
|
+
模式说明(默认 full):
|
|
33
|
+
full 【默认】完整页面快照,包含所有内容和交互元素
|
|
34
|
+
brief 精简模式,保留结构+交互,截断长文本(省 token)
|
|
35
|
+
text 纯文本模式,按阅读顺序输出可见文本
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
示例:
|
|
39
|
+
dp snapshot # 完整快照(推荐首次调用)
|
|
40
|
+
dp snapshot --mode brief # 精简模式(省 token,适合循环调用)
|
|
41
|
+
dp snapshot --mode text # 纯文本(全量文字内容)
|
|
42
|
+
dp snapshot --selector ".main" # 只获取指定区域
|
|
43
|
+
dp snapshot --format json # JSON 格式输出
|
|
44
|
+
"""
|
|
45
|
+
page = _get_page(session)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
data = take_a11y_snapshot(page, selector=selector)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
error('获取页面快照失败', code='SNAPSHOT_FAILED', detail=str(e))
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# 收集 ref 映射(所有模式都收集,便于后续 ref:N 引用)
|
|
54
|
+
refs = {}
|
|
55
|
+
if fmt == 'json':
|
|
56
|
+
render_a11y_text(data, refs=refs) # 触发编号分配
|
|
57
|
+
output = json.dumps({'status': 'ok', 'data': data},
|
|
58
|
+
ensure_ascii=False, indent=2)
|
|
59
|
+
elif mode == 'text':
|
|
60
|
+
output = render_a11y_plain_text(data, refs=refs)
|
|
61
|
+
elif mode == 'brief':
|
|
62
|
+
output = render_a11y_text(data, brief=True, refs=refs)
|
|
63
|
+
else:
|
|
64
|
+
output = render_a11y_text(data, refs=refs)
|
|
65
|
+
|
|
66
|
+
# 保存 refs 映射到 session(供 ref:N 解析使用)
|
|
67
|
+
if refs:
|
|
68
|
+
url = data.get('page', {}).get('url', '')
|
|
69
|
+
save_refs(session, url, refs)
|
|
70
|
+
|
|
71
|
+
if filename:
|
|
72
|
+
Path(filename).write_text(output, encoding='utf-8')
|
|
73
|
+
ok(msg=f'快照已保存到 {filename}')
|
|
74
|
+
else:
|
|
75
|
+
click.echo(output)
|
|
76
|
+
|
|
77
|
+
@cli.command('extract')
|
|
78
|
+
@session_option
|
|
79
|
+
@click.argument('container')
|
|
80
|
+
@click.argument('fields_json')
|
|
81
|
+
@click.option('--limit', default=100, help='最多提取多少条记录', show_default=True)
|
|
82
|
+
@click.option('--output', 'output_fmt', type=click.Choice(['json', 'csv']),
|
|
83
|
+
default='json', show_default=True, help='输出格式')
|
|
84
|
+
@click.option('--filename', default=None, help='保存结果到文件')
|
|
85
|
+
def cmd_extract(session, container, fields_json, limit, output_fmt, filename):
|
|
86
|
+
"""批量提取结构化数据(列表页核心工具)。
|
|
87
|
+
|
|
88
|
+
\b
|
|
89
|
+
CONTAINER 容器元素的定位器(每个容器对应一条记录)
|
|
90
|
+
FIELDS_JSON 字段映射 JSON 字符串
|
|
91
|
+
|
|
92
|
+
\b
|
|
93
|
+
字段映射格式:
|
|
94
|
+
简单形式: {"字段名": "子元素定位器"}
|
|
95
|
+
完整形式: {"字段名": {"selector": "...", "attr": "href", "multi": false}}
|
|
96
|
+
|
|
97
|
+
selector 子元素定位器(相对于容器)
|
|
98
|
+
attr 取属性值而非文本,如 "href"、"src"、"data-id"
|
|
99
|
+
multi true → 返回列表(匹配所有子元素)
|
|
100
|
+
default 元素不存在时的默认值
|
|
101
|
+
|
|
102
|
+
\b
|
|
103
|
+
示例:
|
|
104
|
+
dp extract "css:.card" '{"title":"css:.title","url":{"selector":"css:a","attr":"href"}}'
|
|
105
|
+
dp extract "ref:30" '{"title":"css:h3"}' # 用快照编号定位容器
|
|
106
|
+
|
|
107
|
+
\b
|
|
108
|
+
先用 snapshot 了解页面结构,再用 extract 定位容器和字段。
|
|
109
|
+
"""
|
|
110
|
+
container = resolve_locator(container, session)
|
|
111
|
+
page = _get_page(session)
|
|
112
|
+
try:
|
|
113
|
+
fields = json.loads(fields_json)
|
|
114
|
+
except json.JSONDecodeError as e:
|
|
115
|
+
error(f'fields_json 格式错误: {e}', code='INVALID_JSON')
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
results = extract_structured(page, container, fields, limit=limit)
|
|
120
|
+
data = {'count': len(results), 'records': results}
|
|
121
|
+
if output_fmt == 'csv' or (filename and filename.endswith('.csv')):
|
|
122
|
+
content = records_to_csv(results)
|
|
123
|
+
if filename:
|
|
124
|
+
Path(filename).write_text(content, encoding='utf-8-sig')
|
|
125
|
+
ok(data, msg=f'已提取 {len(results)} 条记录,保存到 {filename}')
|
|
126
|
+
else:
|
|
127
|
+
click.echo(content)
|
|
128
|
+
elif filename:
|
|
129
|
+
Path(filename).write_text(
|
|
130
|
+
json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
131
|
+
ok(data, msg=f'已提取 {len(results)} 条记录,保存到 {filename}')
|
|
132
|
+
else:
|
|
133
|
+
ok(data, msg=f'已提取 {len(results)} 条记录')
|
|
134
|
+
except Exception as e:
|
|
135
|
+
error(f'提取失败', code='EXTRACT_FAILED', detail=str(e))
|
|
136
|
+
|
|
137
|
+
@cli.command('query')
|
|
138
|
+
@session_option
|
|
139
|
+
@click.argument('selector')
|
|
140
|
+
@click.option('--fields', default='text,loc', show_default=True,
|
|
141
|
+
help='提取字段,逗号分隔')
|
|
142
|
+
@click.option('--limit', default=200, help='最多返回多少条', show_default=True)
|
|
143
|
+
@click.option('--filename', default=None, help='保存结果到 JSON 文件')
|
|
144
|
+
def cmd_query(session, selector, fields, limit, filename):
|
|
145
|
+
"""按选择器查询元素,提取内容和定位器。支持动态渲染内容。
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
--fields 支持的字段(默认 text,loc):
|
|
149
|
+
text 元素可见文本(raw_text,过滤隐藏反爬文本)
|
|
150
|
+
tag 标签名
|
|
151
|
+
loc 推荐定位器(简短,可直接用于 click/fill 等)
|
|
152
|
+
css 精确 CSS 路径(JS 生成,可唯一定位)
|
|
153
|
+
xpath 精确 XPath(JS 生成)
|
|
154
|
+
html innerHTML
|
|
155
|
+
outer_html 完整 outerHTML
|
|
156
|
+
href/src/id/class 常用属性
|
|
157
|
+
其他 任意 HTML 属性名
|
|
158
|
+
|
|
159
|
+
\b
|
|
160
|
+
用法示例:
|
|
161
|
+
dp query "css:.job-name" # 默认返回文本+定位器
|
|
162
|
+
dp query "ref:57" # 用快照编号查询
|
|
163
|
+
dp query "ref:57" --fields "text,css,tag,class" # 获取精确 CSS 路径
|
|
164
|
+
dp query "css:a[href]" --fields "text,href"
|
|
165
|
+
dp query "css:.desc" --fields "text,html" # 获取 innerHTML
|
|
166
|
+
"""
|
|
167
|
+
selector = resolve_locator(selector, session)
|
|
168
|
+
page = _get_page(session)
|
|
169
|
+
field_list = [f.strip() for f in fields.split(',') if f.strip()]
|
|
170
|
+
try:
|
|
171
|
+
results = query_elements(page, selector, field_list, limit=limit)
|
|
172
|
+
data = {'count': len(results), 'selector': selector, 'records': results}
|
|
173
|
+
if filename:
|
|
174
|
+
Path(filename).write_text(
|
|
175
|
+
json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
176
|
+
ok(data, msg=f'查询到 {len(results)} 个元素,保存到 {filename}')
|
|
177
|
+
else:
|
|
178
|
+
ok(data, msg=f'查询到 {len(results)} 个元素')
|
|
179
|
+
except Exception as e:
|
|
180
|
+
error(f'查询失败', code='QUERY_FAILED', detail=str(e))
|
|
181
|
+
|
|
182
|
+
@cli.command('find')
|
|
183
|
+
@click.argument('locator')
|
|
184
|
+
@session_option
|
|
185
|
+
@click.option('--all', 'find_all', is_flag=True, help='返回所有匹配元素')
|
|
186
|
+
@click.option('--timeout', default=10, help='等待超时秒数', show_default=True)
|
|
187
|
+
def find(locator, session, find_all, timeout):
|
|
188
|
+
"""查找元素并返回信息。
|
|
189
|
+
|
|
190
|
+
\b
|
|
191
|
+
示例:
|
|
192
|
+
dp find "css:a"
|
|
193
|
+
dp find "css:a" --all
|
|
194
|
+
dp find "ref:5"
|
|
195
|
+
"""
|
|
196
|
+
locator = resolve_locator(locator, session)
|
|
197
|
+
page = _get_page(session)
|
|
198
|
+
try:
|
|
199
|
+
if find_all:
|
|
200
|
+
eles = page.eles(locator, timeout=timeout)
|
|
201
|
+
results = []
|
|
202
|
+
for i, ele in enumerate(eles):
|
|
203
|
+
results.append({
|
|
204
|
+
'index': i,
|
|
205
|
+
'tag': ele.tag,
|
|
206
|
+
'text': (ele.raw_text or '').strip(),
|
|
207
|
+
'attrs': {k: v for k, v in ele.attrs.items()
|
|
208
|
+
if k in ('id', 'class', 'href', 'name', 'type', 'value')},
|
|
209
|
+
})
|
|
210
|
+
ok({'count': len(results), 'elements': results})
|
|
211
|
+
else:
|
|
212
|
+
ele = page.ele(locator, timeout=timeout)
|
|
213
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
214
|
+
ok({'found': False, 'locator': locator})
|
|
215
|
+
return
|
|
216
|
+
ok({'found': True,
|
|
217
|
+
'tag': ele.tag,
|
|
218
|
+
'text': (ele.raw_text or '').strip(),
|
|
219
|
+
'attrs': ele.attrs})
|
|
220
|
+
except Exception as e:
|
|
221
|
+
error(f'查找失败: {locator}', code='FIND_FAILED', detail=str(e))
|
|
222
|
+
|
|
223
|
+
@cli.command('inspect')
|
|
224
|
+
@click.argument('locator')
|
|
225
|
+
@session_option
|
|
226
|
+
@click.option('--index', default=1, help='第几个匹配元素', show_default=True)
|
|
227
|
+
@click.option('--include-rect', is_flag=True, help='包含位置和尺寸信息')
|
|
228
|
+
@click.option('--include-style', is_flag=True, help='包含计算样式')
|
|
229
|
+
@click.option('--timeout', default=10, help='等待超时秒数', show_default=True)
|
|
230
|
+
def inspect(locator, session, index, include_rect, include_style, timeout):
|
|
231
|
+
"""查询元素详细信息(DrissionPage 独有:位置/尺寸/样式/状态/属性)。
|
|
232
|
+
|
|
233
|
+
\b
|
|
234
|
+
示例:
|
|
235
|
+
dp inspect "#submit-btn"
|
|
236
|
+
dp inspect "ref:5" --include-rect
|
|
237
|
+
dp inspect "css:input" --include-style
|
|
238
|
+
"""
|
|
239
|
+
locator = resolve_locator(locator, session)
|
|
240
|
+
page = _get_page(session)
|
|
241
|
+
try:
|
|
242
|
+
ele = page.ele(locator, index=index, timeout=timeout)
|
|
243
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
244
|
+
error(f'未找到元素: {locator}', code='ELEMENT_NOT_FOUND')
|
|
245
|
+
return
|
|
246
|
+
info = {
|
|
247
|
+
'tag': ele.tag,
|
|
248
|
+
'text': (ele.raw_text or '').strip(),
|
|
249
|
+
'attrs': ele.attrs,
|
|
250
|
+
'states': {
|
|
251
|
+
'is_displayed': ele.states.is_displayed,
|
|
252
|
+
'is_enabled': ele.states.is_enabled,
|
|
253
|
+
'is_checked': ele.states.is_checked if ele.tag in ('input', 'option') else None,
|
|
254
|
+
'is_clickable': ele.states.is_clickable,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
if include_rect:
|
|
258
|
+
rect = ele.rect
|
|
259
|
+
info['rect'] = {
|
|
260
|
+
'location': list(rect.location),
|
|
261
|
+
'size': list(rect.size),
|
|
262
|
+
'midpoint': list(rect.midpoint),
|
|
263
|
+
}
|
|
264
|
+
if include_style:
|
|
265
|
+
styles = {}
|
|
266
|
+
for prop in ('color', 'background-color', 'font-size',
|
|
267
|
+
'display', 'visibility', 'position', 'z-index'):
|
|
268
|
+
try:
|
|
269
|
+
styles[prop] = ele.style(prop)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
info['styles'] = styles
|
|
273
|
+
ok(info)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
error(f'查询元素失败: {locator}', code='INSPECT_FAILED', detail=str(e))
|
|
276
|
+
|
|
277
|
+
# ---- DOM 遍历命令 ----
|
|
278
|
+
|
|
279
|
+
def _ele_summary(ele, max_text=60):
|
|
280
|
+
"""生成元素的简洁摘要"""
|
|
281
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
282
|
+
return None
|
|
283
|
+
tag = ele.tag
|
|
284
|
+
attrs = ele.attrs or {}
|
|
285
|
+
cls = attrs.get('class', '')
|
|
286
|
+
eid = attrs.get('id', '')
|
|
287
|
+
text = (ele.raw_text or '').strip()
|
|
288
|
+
label = tag
|
|
289
|
+
if eid:
|
|
290
|
+
label += f'#{eid}'
|
|
291
|
+
elif cls:
|
|
292
|
+
first_cls = cls.strip().split()[0] if cls.strip() else ''
|
|
293
|
+
if first_cls:
|
|
294
|
+
label += f'.{first_cls}'
|
|
295
|
+
loc = suggest_locator(tag, attrs, text[:50])
|
|
296
|
+
summary = {'tag': label, 'loc': loc}
|
|
297
|
+
if text:
|
|
298
|
+
summary['text'] = text[:max_text] + ('…' if len(text) > max_text else '')
|
|
299
|
+
return summary
|
|
300
|
+
|
|
301
|
+
@cli.command('dom')
|
|
302
|
+
@click.argument('locator')
|
|
303
|
+
@session_option
|
|
304
|
+
@click.option('--direction', '-d',
|
|
305
|
+
type=click.Choice(['parent', 'children', 'siblings', 'all']),
|
|
306
|
+
default='all', show_default=True,
|
|
307
|
+
help='查询方向')
|
|
308
|
+
@click.option('--depth', default=1, show_default=True,
|
|
309
|
+
help='向上查几层父节点(仅 parent/all 生效)')
|
|
310
|
+
@click.option('--index', default=1, help='第几个匹配元素', show_default=True)
|
|
311
|
+
@click.option('--timeout', default=10, help='等待超时秒数', show_default=True)
|
|
312
|
+
def cmd_dom(locator, session, direction, depth, index, timeout):
|
|
313
|
+
"""查询元素的 DOM 上下文(父/子/兄弟节点)。
|
|
314
|
+
|
|
315
|
+
\b
|
|
316
|
+
精确定位元素时,先用 snapshot 找到目标,再用 dom 查看周围结构。
|
|
317
|
+
|
|
318
|
+
\b
|
|
319
|
+
示例:
|
|
320
|
+
dp dom "ref:21" # 查看父/子/兄弟全部
|
|
321
|
+
dp dom "ref:21" -d parent # 只看父节点链
|
|
322
|
+
dp dom "ref:21" -d parent --depth 3 # 向上追溯 3 层
|
|
323
|
+
dp dom "ref:21" -d children # 只看子节点
|
|
324
|
+
dp dom "ref:21" -d siblings # 只看兄弟节点
|
|
325
|
+
dp dom "css:.job-name" -d all # 用 CSS 选择器
|
|
326
|
+
"""
|
|
327
|
+
locator = resolve_locator(locator, session)
|
|
328
|
+
page = _get_page(session)
|
|
329
|
+
try:
|
|
330
|
+
ele = page.ele(locator, index=index, timeout=timeout)
|
|
331
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
332
|
+
error(f'未找到元素: {locator}', code='ELEMENT_NOT_FOUND')
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
result = {'self': _ele_summary(ele)}
|
|
336
|
+
|
|
337
|
+
# ---- parent chain ----
|
|
338
|
+
if direction in ('parent', 'all'):
|
|
339
|
+
parents = []
|
|
340
|
+
cur = ele
|
|
341
|
+
for _ in range(depth):
|
|
342
|
+
try:
|
|
343
|
+
par = cur.parent()
|
|
344
|
+
if not par or par.__class__.__name__ == 'NoneElement':
|
|
345
|
+
break
|
|
346
|
+
if par.tag in ('html', 'body'):
|
|
347
|
+
parents.append({'tag': par.tag})
|
|
348
|
+
break
|
|
349
|
+
parents.append(_ele_summary(par))
|
|
350
|
+
cur = par
|
|
351
|
+
except Exception:
|
|
352
|
+
break
|
|
353
|
+
result['parents'] = parents
|
|
354
|
+
|
|
355
|
+
# ---- children ----
|
|
356
|
+
if direction in ('children', 'all'):
|
|
357
|
+
children = []
|
|
358
|
+
try:
|
|
359
|
+
for child in ele.children():
|
|
360
|
+
s = _ele_summary(child)
|
|
361
|
+
if s:
|
|
362
|
+
children.append(s)
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
result['children'] = children
|
|
366
|
+
|
|
367
|
+
# ---- siblings (prev + next) ----
|
|
368
|
+
if direction in ('siblings', 'all'):
|
|
369
|
+
prev_sibs = []
|
|
370
|
+
next_sibs = []
|
|
371
|
+
try:
|
|
372
|
+
for sib in ele.prevs():
|
|
373
|
+
s = _ele_summary(sib)
|
|
374
|
+
if s:
|
|
375
|
+
prev_sibs.append(s)
|
|
376
|
+
prev_sibs.reverse()
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
try:
|
|
380
|
+
for sib in ele.nexts():
|
|
381
|
+
s = _ele_summary(sib)
|
|
382
|
+
if s:
|
|
383
|
+
next_sibs.append(s)
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
386
|
+
result['prev_siblings'] = prev_sibs
|
|
387
|
+
result['next_siblings'] = next_sibs
|
|
388
|
+
|
|
389
|
+
ok(result)
|
|
390
|
+
except Exception as e:
|
|
391
|
+
error(f'DOM 查询失败: {locator}', code='DOM_FAILED', detail=str(e))
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""存储管理命令: cookie-* / localstorage-* / sessionstorage-*"""
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from dp_cli.output import ok, error
|
|
8
|
+
from dp_cli.commands._utils import session_option, _get_page
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(cli):
|
|
12
|
+
|
|
13
|
+
# ── Cookie ──────────────────────────────────
|
|
14
|
+
|
|
15
|
+
@cli.command('cookie-list')
|
|
16
|
+
@session_option
|
|
17
|
+
@click.option('--domain', default=None, help='按域名过滤')
|
|
18
|
+
@click.option('--url', default=None, help='按 URL 过滤')
|
|
19
|
+
def cookie_list(session, domain, url):
|
|
20
|
+
"""列出所有 Cookie。
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
示例:
|
|
24
|
+
dp cookie-list
|
|
25
|
+
dp cookie-list --domain example.com
|
|
26
|
+
"""
|
|
27
|
+
page = _get_page(session)
|
|
28
|
+
try:
|
|
29
|
+
cookies = page.cookies(all_domains=True).as_dict()
|
|
30
|
+
cookie_list_data = []
|
|
31
|
+
for name, value in cookies.items():
|
|
32
|
+
cookie_list_data.append({'name': name, 'value': value})
|
|
33
|
+
ok({'cookies': cookie_list_data, 'count': len(cookie_list_data)})
|
|
34
|
+
except Exception as e:
|
|
35
|
+
error(f'获取 Cookie 失败', code='COOKIE_FAILED', detail=str(e))
|
|
36
|
+
|
|
37
|
+
@cli.command('cookie-get')
|
|
38
|
+
@click.argument('name')
|
|
39
|
+
@session_option
|
|
40
|
+
def cookie_get(name, session):
|
|
41
|
+
"""获取指定名称的 Cookie 值。"""
|
|
42
|
+
page = _get_page(session)
|
|
43
|
+
try:
|
|
44
|
+
cookies = page.cookies().as_dict()
|
|
45
|
+
value = cookies.get(name)
|
|
46
|
+
if value is None:
|
|
47
|
+
error(f'Cookie 不存在: {name}', code='COOKIE_NOT_FOUND')
|
|
48
|
+
return
|
|
49
|
+
ok({'name': name, 'value': value})
|
|
50
|
+
except Exception as e:
|
|
51
|
+
error(f'获取 Cookie 失败', code='COOKIE_FAILED', detail=str(e))
|
|
52
|
+
|
|
53
|
+
@cli.command('cookie-set')
|
|
54
|
+
@click.argument('name')
|
|
55
|
+
@click.argument('value')
|
|
56
|
+
@session_option
|
|
57
|
+
@click.option('--domain', default=None, help='Cookie 域名')
|
|
58
|
+
@click.option('--path', default='/', help='Cookie 路径', show_default=True)
|
|
59
|
+
@click.option('--http-only', is_flag=True, help='设置 HttpOnly')
|
|
60
|
+
@click.option('--secure', is_flag=True, help='设置 Secure')
|
|
61
|
+
def cookie_set(name, value, session, domain, path, http_only, secure):
|
|
62
|
+
"""设置 Cookie。
|
|
63
|
+
|
|
64
|
+
\b
|
|
65
|
+
示例:
|
|
66
|
+
dp cookie-set session_id abc123
|
|
67
|
+
dp cookie-set token xxx --domain example.com --http-only --secure
|
|
68
|
+
"""
|
|
69
|
+
page = _get_page(session)
|
|
70
|
+
try:
|
|
71
|
+
kwargs = {'name': name, 'value': value, 'path': path}
|
|
72
|
+
if domain:
|
|
73
|
+
kwargs['domain'] = domain
|
|
74
|
+
if http_only:
|
|
75
|
+
kwargs['httpOnly'] = True
|
|
76
|
+
if secure:
|
|
77
|
+
kwargs['secure'] = True
|
|
78
|
+
page.set.cookies(kwargs)
|
|
79
|
+
ok({'name': name, 'value': value}, msg='Cookie 已设置')
|
|
80
|
+
except Exception as e:
|
|
81
|
+
error(f'设置 Cookie 失败', code='COOKIE_FAILED', detail=str(e))
|
|
82
|
+
|
|
83
|
+
@cli.command('cookie-delete')
|
|
84
|
+
@click.argument('name')
|
|
85
|
+
@session_option
|
|
86
|
+
def cookie_delete(name, session):
|
|
87
|
+
"""删除指定 Cookie。"""
|
|
88
|
+
page = _get_page(session)
|
|
89
|
+
try:
|
|
90
|
+
page.run_cdp('Network.deleteCookies', name=name)
|
|
91
|
+
ok({'name': name}, msg='Cookie 已删除')
|
|
92
|
+
except Exception as e:
|
|
93
|
+
error(f'删除 Cookie 失败', code='COOKIE_FAILED', detail=str(e))
|
|
94
|
+
|
|
95
|
+
@cli.command('cookie-clear')
|
|
96
|
+
@session_option
|
|
97
|
+
def cookie_clear(session):
|
|
98
|
+
"""清除所有 Cookie。"""
|
|
99
|
+
page = _get_page(session)
|
|
100
|
+
try:
|
|
101
|
+
page.run_cdp('Network.clearBrowserCookies')
|
|
102
|
+
ok(msg='所有 Cookie 已清除')
|
|
103
|
+
except Exception as e:
|
|
104
|
+
error(f'清除 Cookie 失败', code='COOKIE_FAILED', detail=str(e))
|
|
105
|
+
|
|
106
|
+
# ── LocalStorage ────────────────────────────
|
|
107
|
+
|
|
108
|
+
@cli.command('localstorage-list')
|
|
109
|
+
@session_option
|
|
110
|
+
def localstorage_list(session):
|
|
111
|
+
"""列出所有 localStorage 条目。"""
|
|
112
|
+
page = _get_page(session)
|
|
113
|
+
try:
|
|
114
|
+
result = page.run_js(
|
|
115
|
+
'return JSON.stringify(Object.fromEntries(Object.entries(localStorage)))',
|
|
116
|
+
as_expr=True)
|
|
117
|
+
data = json.loads(result) if isinstance(result, str) else result or {}
|
|
118
|
+
ok({'storage': data, 'count': len(data)})
|
|
119
|
+
except Exception as e:
|
|
120
|
+
error(f'获取 localStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
121
|
+
|
|
122
|
+
@cli.command('localstorage-get')
|
|
123
|
+
@click.argument('key')
|
|
124
|
+
@session_option
|
|
125
|
+
def localstorage_get(key, session):
|
|
126
|
+
"""获取 localStorage 指定键的值。"""
|
|
127
|
+
page = _get_page(session)
|
|
128
|
+
try:
|
|
129
|
+
value = page.local_storage(key)
|
|
130
|
+
ok({'key': key, 'value': value})
|
|
131
|
+
except Exception as e:
|
|
132
|
+
error(f'获取 localStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
133
|
+
|
|
134
|
+
@cli.command('localstorage-set')
|
|
135
|
+
@click.argument('key')
|
|
136
|
+
@click.argument('value')
|
|
137
|
+
@session_option
|
|
138
|
+
def localstorage_set(key, value, session):
|
|
139
|
+
"""设置 localStorage 键值。"""
|
|
140
|
+
page = _get_page(session)
|
|
141
|
+
try:
|
|
142
|
+
page.run_js(f'localStorage.setItem({json.dumps(key)}, {json.dumps(value)})',
|
|
143
|
+
as_expr=True)
|
|
144
|
+
ok({'key': key, 'value': value}, msg='localStorage 已设置')
|
|
145
|
+
except Exception as e:
|
|
146
|
+
error(f'设置 localStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
147
|
+
|
|
148
|
+
@cli.command('localstorage-delete')
|
|
149
|
+
@click.argument('key')
|
|
150
|
+
@session_option
|
|
151
|
+
def localstorage_delete(key, session):
|
|
152
|
+
"""删除 localStorage 指定键。"""
|
|
153
|
+
page = _get_page(session)
|
|
154
|
+
try:
|
|
155
|
+
page.run_js(f'localStorage.removeItem({json.dumps(key)})', as_expr=True)
|
|
156
|
+
ok({'key': key}, msg='localStorage 键已删除')
|
|
157
|
+
except Exception as e:
|
|
158
|
+
error(f'删除 localStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
159
|
+
|
|
160
|
+
@cli.command('localstorage-clear')
|
|
161
|
+
@session_option
|
|
162
|
+
def localstorage_clear(session):
|
|
163
|
+
"""清除所有 localStorage。"""
|
|
164
|
+
page = _get_page(session)
|
|
165
|
+
try:
|
|
166
|
+
page.run_js('localStorage.clear()', as_expr=True)
|
|
167
|
+
ok(msg='localStorage 已清除')
|
|
168
|
+
except Exception as e:
|
|
169
|
+
error(f'清除 localStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
170
|
+
|
|
171
|
+
# ── SessionStorage ───────────────────────────
|
|
172
|
+
|
|
173
|
+
@cli.command('sessionstorage-list')
|
|
174
|
+
@session_option
|
|
175
|
+
def sessionstorage_list(session):
|
|
176
|
+
"""列出所有 sessionStorage 条目。"""
|
|
177
|
+
page = _get_page(session)
|
|
178
|
+
try:
|
|
179
|
+
result = page.run_js(
|
|
180
|
+
'JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))',
|
|
181
|
+
as_expr=True)
|
|
182
|
+
data = json.loads(result) if isinstance(result, str) else result or {}
|
|
183
|
+
ok({'storage': data, 'count': len(data)})
|
|
184
|
+
except Exception as e:
|
|
185
|
+
error(f'获取 sessionStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
186
|
+
|
|
187
|
+
@cli.command('sessionstorage-get')
|
|
188
|
+
@click.argument('key')
|
|
189
|
+
@session_option
|
|
190
|
+
def sessionstorage_get(key, session):
|
|
191
|
+
"""获取 sessionStorage 指定键的值。"""
|
|
192
|
+
page = _get_page(session)
|
|
193
|
+
try:
|
|
194
|
+
value = page.session_storage(key)
|
|
195
|
+
ok({'key': key, 'value': value})
|
|
196
|
+
except Exception as e:
|
|
197
|
+
error(f'获取 sessionStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
198
|
+
|
|
199
|
+
@cli.command('sessionstorage-set')
|
|
200
|
+
@click.argument('key')
|
|
201
|
+
@click.argument('value')
|
|
202
|
+
@session_option
|
|
203
|
+
def sessionstorage_set(key, value, session):
|
|
204
|
+
"""设置 sessionStorage 键值。"""
|
|
205
|
+
page = _get_page(session)
|
|
206
|
+
try:
|
|
207
|
+
page.run_js(f'sessionStorage.setItem({json.dumps(key)}, {json.dumps(value)})',
|
|
208
|
+
as_expr=True)
|
|
209
|
+
ok({'key': key, 'value': value}, msg='sessionStorage 已设置')
|
|
210
|
+
except Exception as e:
|
|
211
|
+
error(f'设置 sessionStorage 失败', code='STORAGE_FAILED', detail=str(e))
|
|
212
|
+
|
|
213
|
+
@cli.command('sessionstorage-clear')
|
|
214
|
+
@session_option
|
|
215
|
+
def sessionstorage_clear(session):
|
|
216
|
+
"""清除所有 sessionStorage。"""
|
|
217
|
+
page = _get_page(session)
|
|
218
|
+
try:
|
|
219
|
+
page.run_js('sessionStorage.clear()', as_expr=True)
|
|
220
|
+
ok(msg='sessionStorage 已清除')
|
|
221
|
+
except Exception as e:
|
|
222
|
+
error(f'清除 sessionStorage 失败', code='STORAGE_FAILED', detail=str(e))
|