qrpa 1.0.9__py3-none-any.whl → 1.0.10__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 +50 -0
- qrpa/__init__.py +3 -1
- qrpa/fun_base.py +71 -4
- qrpa/fun_excel.py +2758 -0
- qrpa/fun_web.py +148 -0
- qrpa/shein_ziniao.py +51 -42
- qrpa/time_utils.py +55 -55
- qrpa/wxwork.py +11 -13
- {qrpa-1.0.9.dist-info → qrpa-1.0.10.dist-info}/METADATA +1 -1
- qrpa-1.0.10.dist-info/RECORD +16 -0
- qrpa-1.0.9.dist-info/RECORD +0 -13
- {qrpa-1.0.9.dist-info → qrpa-1.0.10.dist-info}/WHEEL +0 -0
- {qrpa-1.0.9.dist-info → qrpa-1.0.10.dist-info}/top_level.txt +0 -0
qrpa/fun_web.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
|
|
9
|
+
def fetch(page: Page, url: str, params: Optional[Union[dict, list, str]] = None, headers: Optional[dict] = None, config:
|
|
10
|
+
Optional[dict] = None) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
发送 HTTP POST 请求,支持自定义 headers。
|
|
13
|
+
|
|
14
|
+
:param page: Playwright 的 Page 对象
|
|
15
|
+
:param url: 请求地址
|
|
16
|
+
:param params: 请求参数(dict、list、str 或 None)
|
|
17
|
+
:param headers: 自定义 headers 字典
|
|
18
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
19
|
+
"""
|
|
20
|
+
if params is not None and not isinstance(params, (dict, list, str)):
|
|
21
|
+
raise ValueError("params 参数必须是 dict、list、str 或 None")
|
|
22
|
+
if headers is not None and not isinstance(headers, dict):
|
|
23
|
+
raise ValueError("headers 参数必须是 dict 或 None")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
response = page.evaluate("""
|
|
27
|
+
async ({ url, params, extraHeaders }) => {
|
|
28
|
+
try {
|
|
29
|
+
const defaultHeaders = {
|
|
30
|
+
'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',
|
|
31
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
|
|
35
|
+
const options = {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
credentials: 'include',
|
|
38
|
+
headers: headers
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (params !== null) {
|
|
42
|
+
if (typeof params === 'string') {
|
|
43
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
44
|
+
options.body = params;
|
|
45
|
+
} else {
|
|
46
|
+
options.headers['Content-Type'] = 'application/json';
|
|
47
|
+
options.body = JSON.stringify(params);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await fetch(url, options);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
return await response.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return { "error": "fetch_failed", "message": error.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
""", {"url": url, "params": params, "extraHeaders": headers})
|
|
61
|
+
|
|
62
|
+
return response
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise send_exception()
|
|
65
|
+
# return {"error": "fetch error", "message": str(e)}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def fetch_via_iframe(page: Page, target_domain: str, url: str, params: Optional[Union[dict, list, str]] = None, config:
|
|
69
|
+
Optional[dict] = None) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
方案 2:在 iframe 内部执行 fetch 请求,绕过 CORS 限制
|
|
72
|
+
|
|
73
|
+
:param page: Playwright 的 Page 对象
|
|
74
|
+
:param url: 目标请求的 URL
|
|
75
|
+
:param target_domain: 目标 iframe 所在的域名(用于匹配 iframe)
|
|
76
|
+
:param params: 请求参数(dict、list、str 或 None)
|
|
77
|
+
:return: 服务器返回的 JSON 响应(dict)
|
|
78
|
+
"""
|
|
79
|
+
if params is not None and not isinstance(params, (dict, list, str)):
|
|
80
|
+
raise ValueError("params 参数必须是 dict、list、str 或 None")
|
|
81
|
+
response = None
|
|
82
|
+
try:
|
|
83
|
+
# 获取所有 iframe,查找目标域名的 iframe
|
|
84
|
+
frames = page.frames
|
|
85
|
+
target_frame = None
|
|
86
|
+
for frame in frames:
|
|
87
|
+
if target_domain in frame.url:
|
|
88
|
+
target_frame = frame
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if not target_frame:
|
|
92
|
+
return {"error": "iframe_not_found", "message": f"未找到包含 {target_domain} 的 iframe"}
|
|
93
|
+
|
|
94
|
+
response = target_frame.evaluate("""
|
|
95
|
+
async ({ url, params }) => {
|
|
96
|
+
try {
|
|
97
|
+
const options = {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
credentials: 'include',
|
|
100
|
+
headers: {
|
|
101
|
+
'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',
|
|
102
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (params !== null) {
|
|
107
|
+
if (typeof params === 'string') {
|
|
108
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
109
|
+
options.body = params;
|
|
110
|
+
} else {
|
|
111
|
+
options.headers['Content-Type'] = 'application/json';
|
|
112
|
+
options.body = JSON.stringify(params);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const response = await fetch(url, options);
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
119
|
+
}
|
|
120
|
+
return await response.json();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return { "error": "iframe_fetch_failed", "message": error.message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
""", {"url": url, "params": params})
|
|
126
|
+
|
|
127
|
+
return response
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise send_exception()
|
|
130
|
+
# return {"error": "iframe_exception", "message": str(e)}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# 找到一个页面里面所有的iframe
|
|
134
|
+
def find_all_iframe(page: Page):
|
|
135
|
+
frames = page.frames
|
|
136
|
+
for frame in frames:
|
|
137
|
+
log("找到 iframe:", frame.url)
|
|
138
|
+
return [frame.url for frame in frames]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# 全屏幕截图
|
|
142
|
+
def full_screen_shot(web_page: Page, config):
|
|
143
|
+
# 设置页面的视口大小为一个较大的值,确保截图高清
|
|
144
|
+
web_page.set_viewport_size({"width": 1920, "height": 1080})
|
|
145
|
+
# 截取全页面的高清截图
|
|
146
|
+
full_screenshot_image_path = f'{config.auto_dir}/screenshot/{get_current_datetime()}.png'
|
|
147
|
+
web_page.screenshot(path=full_screenshot_image_path, full_page=True)
|
|
148
|
+
return full_screenshot_image_path
|
qrpa/shein_ziniao.py
CHANGED
|
@@ -24,6 +24,7 @@ from .fun_win import find_software_install_path
|
|
|
24
24
|
from .fun_base import log, hostname, send_exception
|
|
25
25
|
from .fun_file import check_progress_json_ex, get_progress_json_ex, done_progress_json_ex, write_dict_to_file_ex
|
|
26
26
|
|
|
27
|
+
|
|
27
28
|
class ZiniaoClient:
|
|
28
29
|
"""紫鸟客户端管理类"""
|
|
29
30
|
|
|
@@ -52,7 +53,7 @@ class ZiniaoClient:
|
|
|
52
53
|
def _get_user_info(self) -> Dict[str, str]:
|
|
53
54
|
"""获取用户登录信息"""
|
|
54
55
|
return {
|
|
55
|
-
"company"
|
|
56
|
+
"company": self.config.ziniao.company,
|
|
56
57
|
"username": self.config.ziniao.username,
|
|
57
58
|
"password": self.config.ziniao.password
|
|
58
59
|
}
|
|
@@ -89,7 +90,7 @@ class ZiniaoClient:
|
|
|
89
90
|
def update_core(self):
|
|
90
91
|
"""下载所有内核,打开店铺前调用,需客户端版本5.285.7以上"""
|
|
91
92
|
data = {
|
|
92
|
-
"action"
|
|
93
|
+
"action": "updateCore",
|
|
93
94
|
"requestId": str(uuid.uuid4()),
|
|
94
95
|
}
|
|
95
96
|
data.update(self.user_info)
|
|
@@ -145,6 +146,7 @@ class ZiniaoClient:
|
|
|
145
146
|
print('@@ get_exit...' + json.dumps(data))
|
|
146
147
|
self.send_http(data)
|
|
147
148
|
|
|
149
|
+
|
|
148
150
|
class ZiniaoBrowser:
|
|
149
151
|
"""紫鸟浏览器操作类"""
|
|
150
152
|
|
|
@@ -153,22 +155,22 @@ class ZiniaoBrowser:
|
|
|
153
155
|
self.config = config
|
|
154
156
|
|
|
155
157
|
def open_store(self, store_info: str, isWebDriverReadOnlyMode: int = 0,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
isprivacy: int = 0, isHeadless: int = 0,
|
|
159
|
+
cookieTypeSave: int = 0, jsInfo: str = "") -> Dict[str, Any]:
|
|
158
160
|
"""打开店铺"""
|
|
159
161
|
request_id = str(uuid.uuid4())
|
|
160
162
|
data = {
|
|
161
|
-
"action"
|
|
162
|
-
"isWaitPluginUpdate"
|
|
163
|
-
"isHeadless"
|
|
164
|
-
"requestId"
|
|
163
|
+
"action": "startBrowser",
|
|
164
|
+
"isWaitPluginUpdate": 0,
|
|
165
|
+
"isHeadless": isHeadless,
|
|
166
|
+
"requestId": request_id,
|
|
165
167
|
"isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
|
|
166
|
-
"cookieTypeLoad"
|
|
167
|
-
"cookieTypeSave"
|
|
168
|
-
"runMode"
|
|
169
|
-
"isLoadUserPlugin"
|
|
170
|
-
"pluginIdType"
|
|
171
|
-
"privacyMode"
|
|
168
|
+
"cookieTypeLoad": 0,
|
|
169
|
+
"cookieTypeSave": cookieTypeSave,
|
|
170
|
+
"runMode": "1",
|
|
171
|
+
"isLoadUserPlugin": False,
|
|
172
|
+
"pluginIdType": 1,
|
|
173
|
+
"privacyMode": isprivacy
|
|
172
174
|
}
|
|
173
175
|
data.update(self.client.user_info)
|
|
174
176
|
|
|
@@ -194,9 +196,9 @@ class ZiniaoBrowser:
|
|
|
194
196
|
"""关闭店铺"""
|
|
195
197
|
request_id = str(uuid.uuid4())
|
|
196
198
|
data = {
|
|
197
|
-
"action"
|
|
198
|
-
"requestId"
|
|
199
|
-
"duplicate"
|
|
199
|
+
"action": "stopBrowser",
|
|
200
|
+
"requestId": request_id,
|
|
201
|
+
"duplicate": 0,
|
|
200
202
|
"browserOauth": browser_oauth
|
|
201
203
|
}
|
|
202
204
|
data.update(self.client.user_info)
|
|
@@ -215,7 +217,7 @@ class ZiniaoBrowser:
|
|
|
215
217
|
"""获取浏览器列表"""
|
|
216
218
|
request_id = str(uuid.uuid4())
|
|
217
219
|
data = {
|
|
218
|
-
"action"
|
|
220
|
+
"action": "getBrowserList",
|
|
219
221
|
"requestId": request_id
|
|
220
222
|
}
|
|
221
223
|
data.update(self.client.user_info)
|
|
@@ -265,6 +267,7 @@ class ZiniaoBrowser:
|
|
|
265
267
|
# 标记完成
|
|
266
268
|
done_progress_json_ex(self.config, task_key, store_name)
|
|
267
269
|
|
|
270
|
+
|
|
268
271
|
class ZiniaoTaskManager:
|
|
269
272
|
"""紫鸟任务管理类"""
|
|
270
273
|
|
|
@@ -273,10 +276,10 @@ class ZiniaoTaskManager:
|
|
|
273
276
|
self.config = config
|
|
274
277
|
|
|
275
278
|
def run_single_store_task(self, browser_info: Dict[str, Any],
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
run_func: Callable, task_key: str,
|
|
280
|
+
just_store_username: Optional[List[str]] = None,
|
|
281
|
+
is_skip_store: Optional[Callable] = None
|
|
282
|
+
):
|
|
280
283
|
"""运行单个店铺的任务"""
|
|
281
284
|
retry_count = 0
|
|
282
285
|
while True:
|
|
@@ -288,7 +291,8 @@ class ZiniaoTaskManager:
|
|
|
288
291
|
store_username = browser_info.get("store_username")
|
|
289
292
|
|
|
290
293
|
# 记录店铺账号与店铺别名对应关系
|
|
291
|
-
|
|
294
|
+
cache_file = f'{self.config.auto_dir}/shein_store_alias.json'
|
|
295
|
+
write_dict_to_file_ex(cache_file, {store_username: store_name}, [store_username])
|
|
292
296
|
|
|
293
297
|
if is_skip_store and is_skip_store(store_username, store_name):
|
|
294
298
|
return
|
|
@@ -341,25 +345,25 @@ class ZiniaoTaskManager:
|
|
|
341
345
|
self.browser.close_store(store_id)
|
|
342
346
|
break
|
|
343
347
|
except:
|
|
344
|
-
send_exception(
|
|
348
|
+
send_exception(f'第{retry_count}次运行失败,准备重新打开店铺')
|
|
345
349
|
if retry_count > 5:
|
|
346
350
|
break
|
|
347
351
|
|
|
348
352
|
def run_all_stores_task(self, browser_list: List[Dict[str, Any]],
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
+
run_func: Callable, task_key: str,
|
|
354
|
+
just_store_username: Optional[List[str]] = None,
|
|
355
|
+
is_skip_store: Optional[Callable] = None
|
|
356
|
+
):
|
|
353
357
|
"""循环运行所有店铺的任务"""
|
|
354
358
|
for browser_info in browser_list:
|
|
355
359
|
self.run_single_store_task(browser_info, run_func, task_key, just_store_username, is_skip_store)
|
|
356
360
|
|
|
357
361
|
def run_with_thread_pool(self, browser_list: List[Dict[str, Any]],
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
max_threads: int = 3, run_func: Callable = None,
|
|
363
|
+
task_key: str = None,
|
|
364
|
+
just_store_username: Optional[List[str]] = None,
|
|
365
|
+
is_skip_store: Optional[Callable] = None
|
|
366
|
+
):
|
|
363
367
|
"""使用线程池控制最大并发线程数运行任务"""
|
|
364
368
|
with ThreadPoolExecutor(max_workers=max_threads) as executor:
|
|
365
369
|
task = partial(self.run_single_store_task,
|
|
@@ -369,23 +373,27 @@ class ZiniaoTaskManager:
|
|
|
369
373
|
log(f'店铺总数: {len(browser_list)}')
|
|
370
374
|
executor.map(task, browser_list)
|
|
371
375
|
|
|
376
|
+
|
|
372
377
|
class ZiniaoRunner:
|
|
373
378
|
"""紫鸟主运行器类"""
|
|
374
379
|
|
|
375
380
|
def __init__(self, config):
|
|
376
381
|
self.config = config
|
|
382
|
+
os.environ['auto_dir'] = self.config.auto_dir
|
|
383
|
+
os.environ['wxwork_bot_exception'] = self.config.wxwork_bot_exception
|
|
384
|
+
|
|
377
385
|
self.client = ZiniaoClient(config)
|
|
378
386
|
self.browser = ZiniaoBrowser(self.client, config)
|
|
379
387
|
self.task_manager = ZiniaoTaskManager(self.browser, config)
|
|
380
388
|
|
|
381
389
|
def execute(self, run_prepare: Optional[Callable] = None,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
390
|
+
run: Optional[Callable] = None,
|
|
391
|
+
run_summary: Optional[Callable] = None,
|
|
392
|
+
run_notify: Optional[Callable] = None,
|
|
393
|
+
task_key: Optional[str] = None,
|
|
394
|
+
just_store_username: Optional[List[str]] = None,
|
|
395
|
+
is_skip_store: Optional[Callable] = None,
|
|
396
|
+
):
|
|
389
397
|
"""主执行入口"""
|
|
390
398
|
# 前置执行 if run_prepare:
|
|
391
399
|
run_prepare()
|
|
@@ -414,10 +422,10 @@ class ZiniaoRunner:
|
|
|
414
422
|
try_times = 0
|
|
415
423
|
while not check_progress_json_ex(self.config, task_key, just_store_username):
|
|
416
424
|
try_times += 1
|
|
417
|
-
send_exception(
|
|
425
|
+
send_exception(f'检测到任务未全部完成,再次执行: {try_times}')
|
|
418
426
|
self.task_manager.run_with_thread_pool(browser_list, max_threads, run, task_key, just_store_username, is_skip_store)
|
|
419
427
|
if try_times >= 4:
|
|
420
|
-
send_exception(
|
|
428
|
+
send_exception(f'检测到任务未全部完成,再次执行: {try_times}')
|
|
421
429
|
break
|
|
422
430
|
|
|
423
431
|
# 数据汇总
|
|
@@ -437,5 +445,6 @@ class ZiniaoRunner:
|
|
|
437
445
|
# 关闭客户端
|
|
438
446
|
self.client.exit()
|
|
439
447
|
|
|
448
|
+
|
|
440
449
|
if __name__ == "__main__":
|
|
441
450
|
pass
|