qrpa 1.1.79__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.
Potentially problematic release.
This version of qrpa might be problematic. Click here for more details.
- qrpa/RateLimitedSender.py +45 -0
- qrpa/__init__.py +31 -0
- qrpa/db_migrator.py +601 -0
- qrpa/feishu_bot_app.py +607 -0
- qrpa/feishu_client.py +410 -0
- qrpa/feishu_logic.py +1443 -0
- qrpa/fun_base.py +339 -0
- qrpa/fun_excel.py +3470 -0
- qrpa/fun_file.py +319 -0
- qrpa/fun_web.py +473 -0
- qrpa/fun_win.py +198 -0
- qrpa/mysql_module/__init__.py +0 -0
- qrpa/mysql_module/new_product_analysis_model.py +556 -0
- qrpa/mysql_module/shein_ledger_model.py +468 -0
- qrpa/mysql_module/shein_ledger_month_report_model.py +599 -0
- qrpa/mysql_module/shein_product_model.py +495 -0
- qrpa/mysql_module/shein_return_order_model.py +776 -0
- qrpa/mysql_module/shein_store_model.py +595 -0
- qrpa/mysql_module/shein_supplier_info_model.py +554 -0
- qrpa/mysql_module/shein_wallet_model.py +638 -0
- qrpa/shein_daily_report_model.py +375 -0
- qrpa/shein_excel.py +3809 -0
- qrpa/shein_lib.py +5780 -0
- qrpa/shein_mysql.py +106 -0
- qrpa/shein_sqlite.py +154 -0
- qrpa/shein_ziniao.py +531 -0
- qrpa/temu_chrome.py +56 -0
- qrpa/temu_excel.py +139 -0
- qrpa/temu_lib.py +156 -0
- qrpa/time_utils.py +882 -0
- qrpa/time_utils_example.py +243 -0
- qrpa/wxwork.py +318 -0
- qrpa-1.1.79.dist-info/METADATA +9 -0
- qrpa-1.1.79.dist-info/RECORD +36 -0
- qrpa-1.1.79.dist-info/WHEEL +5 -0
- qrpa-1.1.79.dist-info/top_level.txt +1 -0
qrpa/fun_web.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
from playwright.sync_api import Page
|
|
4
|
+
|
|
5
|
+
from .fun_base import log, send_exception
|
|
6
|
+
from .time_utils import get_current_datetime
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
|
|
10
|
+
def debug_shein_signature(page: Page) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
调试函数:检测 _SHEIN_CALC_SIGNATURE_ 函数的加载状态。
|
|
13
|
+
用于排查签名函数未加载的问题。
|
|
14
|
+
|
|
15
|
+
:param page: Playwright 的 Page 对象
|
|
16
|
+
:return: 包含调试信息的字典
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
result = page.evaluate("""
|
|
20
|
+
() => {
|
|
21
|
+
const info = {
|
|
22
|
+
has_signature_func: typeof window._SHEIN_CALC_SIGNATURE_ === 'function',
|
|
23
|
+
page_url: window.location.href,
|
|
24
|
+
ready_state: document.readyState,
|
|
25
|
+
window_keys_with_shein: Object.keys(window).filter(k => k.toLowerCase().includes('shein')),
|
|
26
|
+
timestamp: new Date().toISOString()
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 检查常见的签名/安全相关全局变量
|
|
30
|
+
const securityVars = ['_SHEIN_CALC_SIGNATURE_', '__INITIAL_STATE__', '__NUXT__', 'Vue', 'React'];
|
|
31
|
+
info.global_vars = {};
|
|
32
|
+
securityVars.forEach(v => {
|
|
33
|
+
info.global_vars[v] = typeof window[v];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return info;
|
|
37
|
+
}
|
|
38
|
+
""")
|
|
39
|
+
log("希音签名调试信息:", result)
|
|
40
|
+
return result
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return {"error": str(e)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# 希音签名脚本的 URL(可能需要根据版本更新)
|
|
46
|
+
SHEIN_SIGNATURE_SCRIPT_URL = 'https://assets.dotfashion.cn/webassets/sig_bundle/D58CJi06nV8d45rPGs7SrW2nvz6Dn4/1.0.7/1/main.js'
|
|
47
|
+
|
|
48
|
+
def ensure_shein_signature(page: Page, timeout: int = 15000) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
确保希音签名函数可用。
|
|
51
|
+
|
|
52
|
+
该函数会检测页面是否存在 window._SHEIN_CALC_SIGNATURE_ 函数,
|
|
53
|
+
如果不存在,则尝试直接注入签名脚本。
|
|
54
|
+
|
|
55
|
+
:param page: Playwright 的 Page 对象
|
|
56
|
+
:param timeout: 等待超时时间(毫秒),默认 15 秒
|
|
57
|
+
:return: True 如果签名函数可用,False 如果失败
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# 首先检查函数是否已存在
|
|
61
|
+
has_func = page.evaluate("typeof window._SHEIN_CALC_SIGNATURE_ === 'function'")
|
|
62
|
+
if has_func:
|
|
63
|
+
log("希音签名函数已存在")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
log("希音签名函数不存在,尝试注入签名脚本...")
|
|
67
|
+
|
|
68
|
+
# 方法1:尝试直接注入签名脚本
|
|
69
|
+
try:
|
|
70
|
+
page.add_script_tag(url=SHEIN_SIGNATURE_SCRIPT_URL)
|
|
71
|
+
# 等待函数可用
|
|
72
|
+
page.wait_for_function(
|
|
73
|
+
"typeof window._SHEIN_CALC_SIGNATURE_ === 'function'",
|
|
74
|
+
timeout=timeout
|
|
75
|
+
)
|
|
76
|
+
log("签名脚本注入成功,签名函数已可用")
|
|
77
|
+
return True
|
|
78
|
+
except Exception as e:
|
|
79
|
+
log(f"直接注入签名脚本失败: {e}")
|
|
80
|
+
|
|
81
|
+
# 方法2:尝试触发页面加载签名脚本(通过等待)
|
|
82
|
+
import time
|
|
83
|
+
start_time = time.time()
|
|
84
|
+
max_wait_sec = timeout / 1000
|
|
85
|
+
|
|
86
|
+
while (time.time() - start_time) < max_wait_sec:
|
|
87
|
+
has_func = page.evaluate("typeof window._SHEIN_CALC_SIGNATURE_ === 'function'")
|
|
88
|
+
if has_func:
|
|
89
|
+
log(f"希音签名函数已加载,耗时 {time.time() - start_time:.2f} 秒")
|
|
90
|
+
return True
|
|
91
|
+
time.sleep(0.5)
|
|
92
|
+
|
|
93
|
+
log(f"等待希音签名函数超时({timeout/1000}秒)")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
log(f"确保签名函数可用时出错: {e}")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def fetch(page: Page, url: str, params: Optional[Union[dict, list, str]] = None, headers: Optional[dict] = None, config:
|
|
101
|
+
Optional[dict] = None) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
发送 HTTP POST 请求,支持自定义 headers 和重定向处理。
|
|
104
|
+
|
|
105
|
+
:param page: Playwright 的 Page 对象
|
|
106
|
+
:param url: 请求地址
|
|
107
|
+
:param params: 请求参数(dict、list、str 或 None)
|
|
108
|
+
:param headers: 自定义 headers 字典
|
|
109
|
+
:param config: 请求配置字典
|
|
110
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
111
|
+
"""
|
|
112
|
+
if params is not None and not isinstance(params, (dict, list, str)):
|
|
113
|
+
raise ValueError("params 参数必须是 dict、list、str 或 None")
|
|
114
|
+
if headers is not None and not isinstance(headers, dict):
|
|
115
|
+
raise ValueError("headers 参数必须是 dict 或 None")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
page.wait_for_load_state('load')
|
|
119
|
+
response = page.evaluate("""
|
|
120
|
+
async ({ url, params, extraHeaders, config }) => {
|
|
121
|
+
try {
|
|
122
|
+
const defaultHeaders = {
|
|
123
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
124
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
|
|
128
|
+
const options = {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
credentials: 'include',
|
|
131
|
+
redirect: 'follow', // 明确设置跟随重定向
|
|
132
|
+
headers: headers
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 应用额外配置
|
|
136
|
+
if (config) {
|
|
137
|
+
Object.assign(options, config);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (params !== null) {
|
|
141
|
+
if (typeof params === 'string') {
|
|
142
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
143
|
+
options.body = params;
|
|
144
|
+
} else {
|
|
145
|
+
options.headers['Content-Type'] = 'application/json';
|
|
146
|
+
options.body = JSON.stringify(params);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const response = await fetch(url, options);
|
|
151
|
+
|
|
152
|
+
// 处理重定向
|
|
153
|
+
if (response.redirected) {
|
|
154
|
+
console.log(`请求被重定向到: ${response.url}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
// 如果是重定向相关的状态码,尝试获取响应内容
|
|
159
|
+
if (response.status >= 300 && response.status < 400) {
|
|
160
|
+
const text = await response.text();
|
|
161
|
+
return {
|
|
162
|
+
"error": "redirect_error",
|
|
163
|
+
"message": `HTTP ${response.status} - ${response.statusText}`,
|
|
164
|
+
"redirect_url": response.url,
|
|
165
|
+
"response_text": text,
|
|
166
|
+
"status": response.status
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 尝试解析 JSON,如果失败则返回文本内容
|
|
173
|
+
const contentType = response.headers.get('content-type');
|
|
174
|
+
if (contentType && contentType.includes('application/json')) {
|
|
175
|
+
return await response.json();
|
|
176
|
+
} else {
|
|
177
|
+
const text = await response.text();
|
|
178
|
+
return { "content": text, "content_type": contentType, "final_url": response.url };
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { "error": "fetch_failed", "message": error.message };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
""", {"url": url, "params": params, "extraHeaders": headers, "config": config})
|
|
185
|
+
|
|
186
|
+
return response
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise send_exception()
|
|
189
|
+
# return {"error": "fetch error", "message": str(e)}
|
|
190
|
+
|
|
191
|
+
def fetch_shein(page: Page, url: str, params: Optional[Union[dict, list, str]] = None, headers: Optional[dict] = None, config:
|
|
192
|
+
Optional[dict] = None) -> dict:
|
|
193
|
+
"""
|
|
194
|
+
发送 HTTP POST 请求,专门用于希音接口,自动处理 x-gw-auth 签名。
|
|
195
|
+
|
|
196
|
+
该函数会先确保签名函数可用(通过注入脚本),然后自动生成签名并添加到请求头中。
|
|
197
|
+
|
|
198
|
+
:param page: Playwright 的 Page 对象
|
|
199
|
+
:param url: 请求地址(完整 URL 或相对路径,如 /sbn/common/get_update_time)
|
|
200
|
+
:param params: 请求参数(dict、list、str 或 None)
|
|
201
|
+
:param headers: 自定义 headers 字典
|
|
202
|
+
:param config: 请求配置字典
|
|
203
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
204
|
+
"""
|
|
205
|
+
if params is not None and not isinstance(params, (dict, list, str)):
|
|
206
|
+
raise ValueError("params 参数必须是 dict、list、str 或 None")
|
|
207
|
+
if headers is not None and not isinstance(headers, dict):
|
|
208
|
+
raise ValueError("headers 参数必须是 dict 或 None")
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
page.wait_for_load_state('load')
|
|
212
|
+
|
|
213
|
+
# 在 Python 端确保签名函数可用(注入脚本)
|
|
214
|
+
ensure_shein_signature(page)
|
|
215
|
+
|
|
216
|
+
response = page.evaluate("""
|
|
217
|
+
async ({ url, params, extraHeaders, config }) => {
|
|
218
|
+
try {
|
|
219
|
+
const defaultHeaders = {
|
|
220
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
221
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
222
|
+
'Origin-Url': window.location.href,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
|
|
226
|
+
|
|
227
|
+
// 提取相对路径用于签名
|
|
228
|
+
let signPath = url;
|
|
229
|
+
if (url.startsWith('http')) {
|
|
230
|
+
try {
|
|
231
|
+
const urlObj = new URL(url);
|
|
232
|
+
signPath = urlObj.pathname;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
// 解析失败使用原 url
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 生成希音签名(Python 端已确保函数可用)
|
|
239
|
+
if (typeof window._SHEIN_CALC_SIGNATURE_ === 'function') {
|
|
240
|
+
try {
|
|
241
|
+
const signature = await window._SHEIN_CALC_SIGNATURE_(signPath);
|
|
242
|
+
if (signature) {
|
|
243
|
+
headers['x-gw-auth'] = signature;
|
|
244
|
+
console.log('希音签名生成成功');
|
|
245
|
+
}
|
|
246
|
+
} catch (signError) {
|
|
247
|
+
console.warn('生成希音签名失败:', signError.message);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
console.warn('签名函数不存在,将不带签名发送请求');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const options = {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
credentials: 'include',
|
|
256
|
+
redirect: 'follow',
|
|
257
|
+
headers: headers
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// 应用额外配置
|
|
261
|
+
if (config) {
|
|
262
|
+
Object.assign(options, config);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (params !== null) {
|
|
266
|
+
if (typeof params === 'string') {
|
|
267
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
268
|
+
options.body = params;
|
|
269
|
+
} else {
|
|
270
|
+
options.headers['Content-Type'] = 'application/json';
|
|
271
|
+
options.body = JSON.stringify(params);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const response = await fetch(url, options);
|
|
276
|
+
|
|
277
|
+
// 处理重定向
|
|
278
|
+
if (response.redirected) {
|
|
279
|
+
console.log(`请求被重定向到: ${response.url}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
// 如果是重定向相关的状态码,尝试获取响应内容
|
|
284
|
+
if (response.status >= 300 && response.status < 400) {
|
|
285
|
+
const text = await response.text();
|
|
286
|
+
return {
|
|
287
|
+
"error": "redirect_error",
|
|
288
|
+
"message": `HTTP ${response.status} - ${response.statusText}`,
|
|
289
|
+
"redirect_url": response.url,
|
|
290
|
+
"response_text": text,
|
|
291
|
+
"status": response.status
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 尝试解析 JSON,如果失败则返回文本内容
|
|
298
|
+
const contentType = response.headers.get('content-type');
|
|
299
|
+
if (contentType && contentType.includes('application/json')) {
|
|
300
|
+
return await response.json();
|
|
301
|
+
} else {
|
|
302
|
+
const text = await response.text();
|
|
303
|
+
return { "content": text, "content_type": contentType, "final_url": response.url };
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return { "error": "fetch_failed", "message": error.message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
""", {"url": url, "params": params, "extraHeaders": headers, "config": config})
|
|
310
|
+
|
|
311
|
+
return response
|
|
312
|
+
except Exception as e:
|
|
313
|
+
raise send_exception()
|
|
314
|
+
# return {"error": "fetch error", "message": str(e)}
|
|
315
|
+
|
|
316
|
+
def fetch_via_iframe(page: Page, target_domain: str, url: str, params: Optional[Union[dict, list, str]] = None, config:
|
|
317
|
+
Optional[dict] = None) -> dict:
|
|
318
|
+
"""
|
|
319
|
+
方案 2:在 iframe 内部执行 fetch 请求,绕过 CORS 限制
|
|
320
|
+
|
|
321
|
+
:param page: Playwright 的 Page 对象
|
|
322
|
+
:param url: 目标请求的 URL
|
|
323
|
+
:param target_domain: 目标 iframe 所在的域名(用于匹配 iframe)
|
|
324
|
+
:param params: 请求参数(dict、list、str 或 None)
|
|
325
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
326
|
+
"""
|
|
327
|
+
if params is not None and not isinstance(params, (dict, list, str)):
|
|
328
|
+
raise ValueError("params 参数必须是 dict、list、str 或 None")
|
|
329
|
+
response = None
|
|
330
|
+
try:
|
|
331
|
+
# 获取所有 iframe,查找目标域名的 iframe
|
|
332
|
+
frames = page.frames
|
|
333
|
+
target_frame = None
|
|
334
|
+
for frame in frames:
|
|
335
|
+
if target_domain in frame.url:
|
|
336
|
+
target_frame = frame
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
if not target_frame:
|
|
340
|
+
return {"error": "iframe_not_found", "message": f"未找到包含 {target_domain} 的 iframe"}
|
|
341
|
+
|
|
342
|
+
response = target_frame.evaluate("""
|
|
343
|
+
async ({ url, params }) => {
|
|
344
|
+
try {
|
|
345
|
+
const options = {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
credentials: 'include',
|
|
348
|
+
headers: {
|
|
349
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
350
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (params !== null) {
|
|
355
|
+
if (typeof params === 'string') {
|
|
356
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
357
|
+
options.body = params;
|
|
358
|
+
} else {
|
|
359
|
+
options.headers['Content-Type'] = 'application/json';
|
|
360
|
+
options.body = JSON.stringify(params);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const response = await fetch(url, options);
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
367
|
+
}
|
|
368
|
+
return await response.json();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return { "error": "iframe_fetch_failed", "message": error.message };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
""", {"url": url, "params": params})
|
|
374
|
+
|
|
375
|
+
return response
|
|
376
|
+
except Exception as e:
|
|
377
|
+
raise send_exception()
|
|
378
|
+
# return {"error": "iframe_exception", "message": str(e)}
|
|
379
|
+
|
|
380
|
+
# 找到一个页面里面所有的iframe
|
|
381
|
+
def find_all_iframe(page: Page):
|
|
382
|
+
frames = page.frames
|
|
383
|
+
for frame in frames:
|
|
384
|
+
log("找到 iframe:", frame.url)
|
|
385
|
+
return [frame.url for frame in frames]
|
|
386
|
+
|
|
387
|
+
# 全屏幕截图
|
|
388
|
+
def full_screen_shot(web_page: Page, config):
|
|
389
|
+
# 设置页面的视口大小为一个较大的值,确保截图高清
|
|
390
|
+
web_page.set_viewport_size({"width": 1920, "height": 1080})
|
|
391
|
+
# 截取全页面的高清截图
|
|
392
|
+
full_screenshot_image_path = f'{config.auto_dir}/screenshot/{get_current_datetime()}.png'
|
|
393
|
+
web_page.screenshot(path=full_screenshot_image_path, full_page=True)
|
|
394
|
+
return full_screenshot_image_path
|
|
395
|
+
|
|
396
|
+
def fetch_get(page: Page, url: str, headers: Optional[dict] = None, config: Optional[dict] = None) -> dict:
|
|
397
|
+
"""
|
|
398
|
+
发送 HTTP GET 请求,支持自定义 headers 和配置,支持重定向处理。
|
|
399
|
+
|
|
400
|
+
:param page: Playwright 的 Page 对象
|
|
401
|
+
:param url: 请求地址
|
|
402
|
+
:param headers: 自定义 headers 字典
|
|
403
|
+
:param config: 请求配置字典,可包含 credentials, mode, referrer, referrerPolicy 等
|
|
404
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
405
|
+
"""
|
|
406
|
+
if headers is not None and not isinstance(headers, dict):
|
|
407
|
+
raise ValueError("headers 参数必须是 dict 或 None")
|
|
408
|
+
if config is not None and not isinstance(config, dict):
|
|
409
|
+
raise ValueError("config 参数必须是 dict 或 None")
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
page.wait_for_load_state('load')
|
|
413
|
+
response = page.evaluate("""
|
|
414
|
+
async ({ url, extraHeaders, config }) => {
|
|
415
|
+
try {
|
|
416
|
+
const defaultHeaders = {
|
|
417
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const defaultConfig = {
|
|
421
|
+
method: 'GET',
|
|
422
|
+
credentials: 'include',
|
|
423
|
+
mode: 'cors',
|
|
424
|
+
redirect: 'follow' // 明确设置跟随重定向
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
|
|
428
|
+
const options = Object.assign({}, defaultConfig, config || {}, { headers: headers });
|
|
429
|
+
|
|
430
|
+
const response = await fetch(url, options);
|
|
431
|
+
|
|
432
|
+
// 处理重定向
|
|
433
|
+
if (response.redirected) {
|
|
434
|
+
console.log(`请求被重定向到: ${response.url}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
// 如果是重定向相关的状态码,尝试获取响应内容
|
|
439
|
+
if (response.status >= 300 && response.status < 400) {
|
|
440
|
+
const text = await response.text();
|
|
441
|
+
return {
|
|
442
|
+
"error": "redirect_error",
|
|
443
|
+
"message": `HTTP ${response.status} - ${response.statusText}`,
|
|
444
|
+
"redirect_url": response.url,
|
|
445
|
+
"response_text": text,
|
|
446
|
+
"status": response.status
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 尝试解析 JSON,如果失败则返回文本内容
|
|
453
|
+
const contentType = response.headers.get('content-type');
|
|
454
|
+
if (contentType && contentType.includes('application/json')) {
|
|
455
|
+
return await response.json();
|
|
456
|
+
} else {
|
|
457
|
+
const text = await response.text();
|
|
458
|
+
return { "content": text, "content_type": contentType, "final_url": response.url };
|
|
459
|
+
}
|
|
460
|
+
} catch (error) {
|
|
461
|
+
return { "error": "fetch_failed", "message": error.message };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
""", {"url": url, "extraHeaders": headers, "config": config})
|
|
465
|
+
|
|
466
|
+
return response
|
|
467
|
+
except Exception as e:
|
|
468
|
+
raise send_exception()
|
|
469
|
+
|
|
470
|
+
def safe_goto(page, url, **kwargs):
|
|
471
|
+
caller = inspect.stack()[1]
|
|
472
|
+
log(f"[DEBUG] goto called from {caller.filename}:{caller.lineno} url={url}")
|
|
473
|
+
return page.goto(url, **kwargs)
|
qrpa/fun_win.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import win32com.client
|
|
3
|
+
import winreg
|
|
4
|
+
|
|
5
|
+
import requests, subprocess, time
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
from .fun_base import log, create_file_path
|
|
9
|
+
|
|
10
|
+
default_chrome_user_data = 'D:\chrome_user_data'
|
|
11
|
+
|
|
12
|
+
def set_chrome_system_path():
|
|
13
|
+
path = os.path.dirname(find_software_install_path('chrome'))
|
|
14
|
+
add_to_system_path(path)
|
|
15
|
+
|
|
16
|
+
def add_to_system_path(path: str, scope: str = "user"):
|
|
17
|
+
"""
|
|
18
|
+
将指定路径添加到系统环境变量 Path 中
|
|
19
|
+
:param path: 要添加的路径(应为绝对路径)
|
|
20
|
+
:param scope: 'user' 表示用户变量,'system' 表示系统变量(需要管理员权限)
|
|
21
|
+
"""
|
|
22
|
+
if not os.path.isabs(path):
|
|
23
|
+
raise ValueError("必须提供绝对路径")
|
|
24
|
+
|
|
25
|
+
path = os.path.normpath(path)
|
|
26
|
+
|
|
27
|
+
if scope == "user":
|
|
28
|
+
root = winreg.HKEY_CURRENT_USER
|
|
29
|
+
subkey = r"Environment"
|
|
30
|
+
elif scope == "system":
|
|
31
|
+
root = winreg.HKEY_LOCAL_MACHINE
|
|
32
|
+
subkey = r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("scope 参数必须是 'user' 或 'system'")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with winreg.OpenKey(root, subkey, 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
|
|
38
|
+
current_path, _ = winreg.QueryValueEx(key, "Path")
|
|
39
|
+
paths = current_path.split(";")
|
|
40
|
+
|
|
41
|
+
if path in paths:
|
|
42
|
+
print("路径已存在于 Path 中,无需添加: ", path)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
new_path = current_path + ";" + path
|
|
46
|
+
winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
|
|
47
|
+
print("✅ 路径已成功添加到Path中: ", new_path)
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
except PermissionError:
|
|
51
|
+
print("❌ 权限不足,系统变量修改需要管理员权限")
|
|
52
|
+
return False
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"❌ 添加失败: {e}")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def find_software_install_path(app_keyword: str):
|
|
58
|
+
"""从开始菜单或桌面查找指定软件的安装路径"""
|
|
59
|
+
possible_dirs = [
|
|
60
|
+
os.environ.get('PROGRAMDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
|
|
61
|
+
os.environ.get('APPDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
|
|
62
|
+
os.environ.get('USERPROFILE', '') + r'\Desktop',
|
|
63
|
+
os.environ.get('PUBLIC', '') + r'\Desktop'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
shell = win32com.client.Dispatch("WScript.Shell")
|
|
67
|
+
|
|
68
|
+
for base_dir in possible_dirs:
|
|
69
|
+
for root, _, files in os.walk(base_dir):
|
|
70
|
+
for file in files:
|
|
71
|
+
if file.lower().endswith('.lnk') and app_keyword.lower() in file.lower():
|
|
72
|
+
lnk_path = os.path.join(root, file)
|
|
73
|
+
try:
|
|
74
|
+
shortcut = shell.CreateShortcut(lnk_path)
|
|
75
|
+
target_path = shortcut.Targetpath
|
|
76
|
+
if os.path.exists(target_path):
|
|
77
|
+
return target_path
|
|
78
|
+
except Exception as e:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
log(f'未能查找到{str}安装位置')
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def init_chrome_env(account_list):
|
|
85
|
+
target = find_software_install_path('chrome')
|
|
86
|
+
for account in account_list:
|
|
87
|
+
store_key, port, *rest = account
|
|
88
|
+
user_data = rest[0] if rest and rest[0] else fr'{default_chrome_user_data}\{port}'
|
|
89
|
+
create_file_path(user_data)
|
|
90
|
+
args = fr'--remote-debugging-port={port} --user-data-dir="{user_data}"'
|
|
91
|
+
shortcut_name = f'{port}_{store_key}.lnk'
|
|
92
|
+
create_shortcut_on_desktop(target_path=target, arguments=args, shortcut_name=shortcut_name)
|
|
93
|
+
|
|
94
|
+
def create_shortcut_on_desktop(target_path, arguments='', shortcut_name='MyShortcut.lnk', icon_path=None):
|
|
95
|
+
"""
|
|
96
|
+
在桌面上创建快捷方式,若已存在指向相同 target + arguments 的快捷方式,则跳过创建。
|
|
97
|
+
"""
|
|
98
|
+
# 获取当前用户桌面路径
|
|
99
|
+
desktop_path = os.path.join(os.environ['USERPROFILE'], 'Desktop')
|
|
100
|
+
|
|
101
|
+
shell = win32com.client.Dispatch('WScript.Shell')
|
|
102
|
+
|
|
103
|
+
# 检查是否已有相同目标的快捷方式
|
|
104
|
+
for file in os.listdir(desktop_path):
|
|
105
|
+
if file.lower().endswith('.lnk'):
|
|
106
|
+
shortcut_file = os.path.join(desktop_path, file)
|
|
107
|
+
shortcut = shell.CreateShortCut(shortcut_file)
|
|
108
|
+
if (os.path.normpath(shortcut.Targetpath) == os.path.normpath(target_path)
|
|
109
|
+
and shortcut.Arguments.strip() == arguments.strip()):
|
|
110
|
+
log("已存在指向该 target + args 的快捷方式,跳过创建")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# 创建新的快捷方式
|
|
114
|
+
shortcut_path = os.path.join(desktop_path, shortcut_name)
|
|
115
|
+
shortcut = shell.CreateShortCut(shortcut_path)
|
|
116
|
+
shortcut.Targetpath = target_path
|
|
117
|
+
shortcut.Arguments = arguments
|
|
118
|
+
shortcut.WorkingDirectory = os.path.dirname(target_path)
|
|
119
|
+
if icon_path:
|
|
120
|
+
shortcut.IconLocation = icon_path
|
|
121
|
+
shortcut.save()
|
|
122
|
+
log(f"已创建快捷方式:{shortcut_path}")
|
|
123
|
+
|
|
124
|
+
def check_chrome_dev(port=3000):
|
|
125
|
+
try:
|
|
126
|
+
url = f"http://127.0.0.1:{port}/json"
|
|
127
|
+
response = requests.get(url, timeout=5) # 设置超时,避免长时间等待
|
|
128
|
+
if response.status_code == 200:
|
|
129
|
+
try:
|
|
130
|
+
data = response.json()
|
|
131
|
+
if data:
|
|
132
|
+
# print("接口返回了数据:", data)
|
|
133
|
+
print("接口返回了数据:")
|
|
134
|
+
return True
|
|
135
|
+
else:
|
|
136
|
+
print("接口返回了空数据")
|
|
137
|
+
return False
|
|
138
|
+
except ValueError:
|
|
139
|
+
print("返回的不是有效的 JSON")
|
|
140
|
+
return False
|
|
141
|
+
else:
|
|
142
|
+
print(f"接口返回了错误状态码: {response.status_code}")
|
|
143
|
+
return False
|
|
144
|
+
except requests.RequestException as e:
|
|
145
|
+
print(f"请求接口时发生错误: {e}")
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def is_chrome_running():
|
|
149
|
+
try:
|
|
150
|
+
output = subprocess.check_output('tasklist', shell=True, text=True)
|
|
151
|
+
return 'chrome.exe' in output.lower()
|
|
152
|
+
except subprocess.CalledProcessError:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
@contextmanager
|
|
156
|
+
def get_chrome_page_v3(p, port=3000, user_data=None):
|
|
157
|
+
browser = context = page = None
|
|
158
|
+
is_custom_chrome_opened = False # 标记是否是程序自己开的浏览器
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
if not check_chrome_dev(port):
|
|
162
|
+
set_chrome_system_path()
|
|
163
|
+
chrome_path = r'"chrome.exe"'
|
|
164
|
+
debugging_port = fr"--remote-debugging-port={port}"
|
|
165
|
+
if user_data is not None:
|
|
166
|
+
chrome_user_data = fr'--user-data-dir="{user_data}"'
|
|
167
|
+
else:
|
|
168
|
+
chrome_user_data = fr'--user-data-dir="{create_file_path(default_chrome_user_data)}\{port}"'
|
|
169
|
+
|
|
170
|
+
disable_webrtc = "--disable-features=WebRTC"
|
|
171
|
+
disable_webrtc_hw_encoder = "--disable-features=WebRTC-HW-ENCODER"
|
|
172
|
+
disable_webrtc_alt = "--disable-webrtc"
|
|
173
|
+
start_maximized = "--start-maximized"
|
|
174
|
+
|
|
175
|
+
command = f"{chrome_path} {debugging_port} {chrome_user_data} {disable_webrtc} {disable_webrtc_hw_encoder} {disable_webrtc_alt}"
|
|
176
|
+
subprocess.Popen(command, shell=True)
|
|
177
|
+
is_custom_chrome_opened = True
|
|
178
|
+
time.sleep(1)
|
|
179
|
+
|
|
180
|
+
browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
|
|
181
|
+
context = browser.contexts[0] if browser.contexts else browser.new_context()
|
|
182
|
+
page = context.pages[0] if context.pages else context.new_page()
|
|
183
|
+
|
|
184
|
+
yield browser, context, page
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
# 向上抛出错误,否则主函数感知不到错误
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
finally:
|
|
191
|
+
for obj in [("page", page), ("context", context), ("browser", browser)]:
|
|
192
|
+
name, target = obj
|
|
193
|
+
try:
|
|
194
|
+
if target and is_custom_chrome_opened:
|
|
195
|
+
log(f'关闭: {name}')
|
|
196
|
+
target.close()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass # 你可以在这里加日志记录关闭失败
|
|
File without changes
|