python-qlv-helper 0.6.0__py3-none-any.whl → 0.9.3__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.
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.9.3.dist-info}/METADATA +9 -7
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.9.3.dist-info}/RECORD +17 -16
- qlv_helper/config/url_const.py +2 -0
- qlv_helper/controller/main_page.py +18 -2
- qlv_helper/controller/order_detail.py +202 -212
- qlv_helper/controller/order_table.py +178 -16
- qlv_helper/controller/user_login.py +106 -102
- qlv_helper/controller/wechat_login.py +50 -0
- qlv_helper/http/order_page.py +187 -24
- qlv_helper/po/domestic_activity_order_page.py +91 -0
- qlv_helper/po/login_page.py +36 -74
- qlv_helper/po/main_page.py +7 -38
- qlv_helper/po/order_detail_page.py +46 -1
- qlv_helper/utils/ocr_helper.py +38 -43
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.9.3.dist-info}/WHEEL +0 -0
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.9.3.dist-info}/licenses/LICENSE +0 -0
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.9.3.dist-info}/top_level.txt +0 -0
|
@@ -10,10 +10,17 @@
|
|
|
10
10
|
# ---------------------------------------------------------------------------------------------------------
|
|
11
11
|
"""
|
|
12
12
|
import asyncio
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
from logging import Logger
|
|
13
15
|
from aiohttp import CookieJar
|
|
14
|
-
from
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from playwright.async_api import Page, Locator
|
|
18
|
+
import qlv_helper.config.url_const as url_const
|
|
15
19
|
from http_helper.client.async_proxy import HttpClientFactory
|
|
16
20
|
from qlv_helper.utils.html_utils import parse_pagination_info
|
|
21
|
+
from qlv_helper.utils.type_utils import safe_convert_advanced
|
|
22
|
+
from typing import Optional, Dict, Any, Callable, List, cast, Tuple
|
|
23
|
+
from qlv_helper.po.domestic_activity_order_page import DomesticActivityOrderPage
|
|
17
24
|
from qlv_helper.http.order_table_page import get_domestic_activity_order_page_html, get_domestic_ticket_outed_page_html, \
|
|
18
25
|
parse_order_table, get_domestic_unticketed_order_page_html
|
|
19
26
|
|
|
@@ -28,7 +35,7 @@ async def _get_paginated_order_table(
|
|
|
28
35
|
cookie_jar: Optional[CookieJar],
|
|
29
36
|
playwright_state: Dict[str, Any],
|
|
30
37
|
table_state: str,
|
|
31
|
-
fetch_page_fn: Callable[..., Any],
|
|
38
|
+
fetch_page_fn: Callable[..., Any], # 拿到第一页/分页 HTML 的函数
|
|
32
39
|
) -> Dict[str, Any]:
|
|
33
40
|
"""通用分页表格抓取(支持并发)"""
|
|
34
41
|
|
|
@@ -46,9 +53,10 @@ async def _get_paginated_order_table(
|
|
|
46
53
|
response = await fetch_page_fn(
|
|
47
54
|
domain=domain, protocol=protocol, retry=retry, timeout=timeout,
|
|
48
55
|
enable_log=enable_log, cookie_jar=cookie_jar, playwright_state=playwright_state,
|
|
49
|
-
order_http_client=order_http_client, is_end=
|
|
56
|
+
order_http_client=order_http_client, is_end=False
|
|
50
57
|
)
|
|
51
58
|
if response.get("code") != 200:
|
|
59
|
+
await order_http_client.close()
|
|
52
60
|
return response
|
|
53
61
|
|
|
54
62
|
html = response["data"]
|
|
@@ -66,6 +74,7 @@ async def _get_paginated_order_table(
|
|
|
66
74
|
"pages": 1
|
|
67
75
|
})
|
|
68
76
|
response["data"] = pagination_info
|
|
77
|
+
await order_http_client.close()
|
|
69
78
|
return response
|
|
70
79
|
|
|
71
80
|
# --- 3. 多页:并发抓取第 2~pages 页 ---
|
|
@@ -75,24 +84,14 @@ async def _get_paginated_order_table(
|
|
|
75
84
|
resp = await fetch_page_fn(
|
|
76
85
|
domain=domain, protocol=protocol, retry=retry, timeout=timeout,
|
|
77
86
|
enable_log=enable_log, cookie_jar=cookie_jar, playwright_state=playwright_state,
|
|
78
|
-
order_http_client=client, current_page=page, pages=pages, is_end=
|
|
87
|
+
order_http_client=client, current_page=page, pages=pages, is_end=False
|
|
79
88
|
)
|
|
80
89
|
if resp.get("code") == 200:
|
|
81
90
|
return parse_order_table(html=resp["data"], table_state=table_state)
|
|
82
|
-
except (Exception,
|
|
91
|
+
except (Exception,):
|
|
83
92
|
return list() # 抓取失败则返回空,不影响整体
|
|
84
93
|
return list()
|
|
85
94
|
|
|
86
|
-
# 🔥 并发:一口气抓全部分页
|
|
87
|
-
order_http_client = HttpClientFactory(
|
|
88
|
-
protocol=protocol if protocol == "http" else "https",
|
|
89
|
-
domain=domain,
|
|
90
|
-
timeout=timeout,
|
|
91
|
-
retry=retry,
|
|
92
|
-
enable_log=enable_log,
|
|
93
|
-
cookie_jar=cookie_jar,
|
|
94
|
-
playwright_state=playwright_state
|
|
95
|
-
)
|
|
96
95
|
tasks = [fetch_page(client=order_http_client, page=page) for page in range(2, pages + 1)]
|
|
97
96
|
results = await asyncio.gather(*tasks)
|
|
98
97
|
|
|
@@ -109,8 +108,10 @@ async def _get_paginated_order_table(
|
|
|
109
108
|
"pages": 1
|
|
110
109
|
})
|
|
111
110
|
response["data"] = pagination_info
|
|
111
|
+
await order_http_client.close()
|
|
112
112
|
return response
|
|
113
113
|
|
|
114
|
+
|
|
114
115
|
async def get_domestic_activity_order_table(
|
|
115
116
|
domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
|
|
116
117
|
cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
|
|
@@ -144,6 +145,7 @@ async def get_domestic_ticket_outed_table(
|
|
|
144
145
|
fetch_page_fn=get_domestic_ticket_outed_page_html
|
|
145
146
|
)
|
|
146
147
|
|
|
148
|
+
|
|
147
149
|
async def get_domestic_unticketed_order_table(
|
|
148
150
|
domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
|
|
149
151
|
cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
|
|
@@ -158,4 +160,164 @@ async def get_domestic_unticketed_order_table(
|
|
|
158
160
|
playwright_state=playwright_state,
|
|
159
161
|
table_state="proccessing",
|
|
160
162
|
fetch_page_fn=get_domestic_unticketed_order_page_html
|
|
161
|
-
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def open_domestic_activity_order_page(
|
|
167
|
+
*, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 20.0
|
|
168
|
+
) -> DomesticActivityOrderPage:
|
|
169
|
+
url_prefix = f"{qlv_protocol}://{qlv_domain}"
|
|
170
|
+
domestic_activity_order_url = url_prefix + url_const.domestic_activity_order_url
|
|
171
|
+
await page.goto(domestic_activity_order_url)
|
|
172
|
+
|
|
173
|
+
domestic_activity_order_po = DomesticActivityOrderPage(page=page, url=domestic_activity_order_url)
|
|
174
|
+
await domestic_activity_order_po.url_wait_for(url=domestic_activity_order_url, timeout=timeout)
|
|
175
|
+
logger.info(f"即将进入国内活动订单页面,页面URL<{domestic_activity_order_url}>")
|
|
176
|
+
return domestic_activity_order_po
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def pop_will_expire_domestic_activity_order(
|
|
180
|
+
*, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, last_minute_threshold: int,
|
|
181
|
+
timeout: float = 20.0, **kwargs: Any
|
|
182
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
183
|
+
# 1. 打开国内活动订单页面
|
|
184
|
+
domestic_activity_order_po = await open_domestic_activity_order_page(
|
|
185
|
+
page=page, logger=logger, qlv_protocol=qlv_protocol, qlv_domain=qlv_domain, timeout=timeout
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# TODO 暂时不考虑分页的情况
|
|
189
|
+
# 2. 获取table所有tr的Locator对象
|
|
190
|
+
trs_locator = await domestic_activity_order_po.get_flight_table_trs_locator(timeout=timeout)
|
|
191
|
+
trs_locator = await trs_locator.all()
|
|
192
|
+
table_data = list()
|
|
193
|
+
feilds = {
|
|
194
|
+
"to_from": "", "urgant_state": "", "order_id": 0, "pre_order_id": "", "aduit_pnr": "", "child_pnr": "",
|
|
195
|
+
"payment_time": "", "last_time_ticket": "", "dat_dep": "", "code_dep": "", "code_arr": "", "flight_no": "",
|
|
196
|
+
"cabin": "", "policy": "", "total_people": 0, "total_adult": 0, "total_child": 0, "receipted": 0.00,
|
|
197
|
+
"stat_opration": "", "more_seats": "", "operation_info": "", "substitute_btn_locator": ""
|
|
198
|
+
}
|
|
199
|
+
pre_pop_orders = list()
|
|
200
|
+
is_pop = False
|
|
201
|
+
for tr_locator in trs_locator[1:]:
|
|
202
|
+
row_locator = await domestic_activity_order_po.get_flight_table_trs_td(locator=tr_locator, timeout=timeout)
|
|
203
|
+
tds_locators = await row_locator.all()
|
|
204
|
+
sub_feilds = list()
|
|
205
|
+
copy_feilds = deepcopy(feilds)
|
|
206
|
+
for index, td_locator in enumerate(tds_locators):
|
|
207
|
+
try:
|
|
208
|
+
text = (await td_locator.inner_text()).strip()
|
|
209
|
+
if index == 0:
|
|
210
|
+
copy_feilds["to_from"] = text
|
|
211
|
+
sub_feilds.append("to_from")
|
|
212
|
+
elif index == 1:
|
|
213
|
+
order_id = await domestic_activity_order_po.get_flight_table_td_order_id(
|
|
214
|
+
locator=td_locator, timeout=timeout
|
|
215
|
+
)
|
|
216
|
+
urgant_state = await domestic_activity_order_po.get_flight_table_td_urgant(
|
|
217
|
+
locator=td_locator, timeout=timeout
|
|
218
|
+
)
|
|
219
|
+
copy_feilds["order_id"] = safe_convert_advanced(value=order_id)
|
|
220
|
+
copy_feilds["urgant_state"] = urgant_state
|
|
221
|
+
sub_feilds.extend(["order_id", "urgant_state"])
|
|
222
|
+
elif index == 2:
|
|
223
|
+
copy_feilds["pre_order_id"] = text
|
|
224
|
+
sub_feilds.append("pre_order_id")
|
|
225
|
+
elif index == 3:
|
|
226
|
+
text = text.replace("\xa0", "")
|
|
227
|
+
text_slice = text.split("|")
|
|
228
|
+
copy_feilds["aduit_pnr"] = text_slice[0].strip()
|
|
229
|
+
copy_feilds["child_pnr"] = text_slice[1].strip()
|
|
230
|
+
sub_feilds.extend(["aduit_pnr", "child_pnr"])
|
|
231
|
+
elif index == 4:
|
|
232
|
+
copy_feilds["payment_time"] = text
|
|
233
|
+
sub_feilds.append("payment_time")
|
|
234
|
+
elif index == 5:
|
|
235
|
+
continue
|
|
236
|
+
elif index == 6:
|
|
237
|
+
copy_feilds["last_time_ticket"] = text
|
|
238
|
+
sub_feilds.append("last_time_ticket")
|
|
239
|
+
elif index == 7:
|
|
240
|
+
continue
|
|
241
|
+
elif index == 8:
|
|
242
|
+
text = text.replace("\xa0", "|")
|
|
243
|
+
text_slice = [i for i in text.split("|") if i.strip()]
|
|
244
|
+
ctrip = text_slice[1].split("-")
|
|
245
|
+
copy_feilds["dat_dep"] = text_slice[0].strip()
|
|
246
|
+
copy_feilds["code_dep"] = ctrip[0].strip()
|
|
247
|
+
copy_feilds["code_arr"] = ctrip[1].strip()
|
|
248
|
+
copy_feilds["flight_no"] = text_slice[2].strip()
|
|
249
|
+
copy_feilds["cabin"] = text_slice[3].strip()
|
|
250
|
+
sub_feilds.extend(["dat_dep", "code_dep", "code_arr", "flight_no", "cabin"])
|
|
251
|
+
elif index == 9:
|
|
252
|
+
text = text.replace("\xa0", "")
|
|
253
|
+
text = text.replace(">", "")
|
|
254
|
+
text = text.replace("<", "")
|
|
255
|
+
text = text.replace("&", "")
|
|
256
|
+
text = text.replace("<br>", "\n")
|
|
257
|
+
copy_feilds["policy"] = text
|
|
258
|
+
sub_feilds.append("policy")
|
|
259
|
+
elif index == 10:
|
|
260
|
+
text = text.replace("【 ", "|")
|
|
261
|
+
text = text.replace("/", "|")
|
|
262
|
+
text = text.replace("】", "")
|
|
263
|
+
text_slice = text.split("|")
|
|
264
|
+
copy_feilds["total_people"] = safe_convert_advanced(value=text_slice[0].strip())
|
|
265
|
+
copy_feilds["total_adult"] = safe_convert_advanced(value=text_slice[1].strip())
|
|
266
|
+
copy_feilds["total_child"] = safe_convert_advanced(value=text_slice[2].strip())
|
|
267
|
+
sub_feilds.extend(["total_people", "total_adult", "total_child"])
|
|
268
|
+
elif index == 11:
|
|
269
|
+
copy_feilds["receipted"] = safe_convert_advanced(value=text)
|
|
270
|
+
sub_feilds.append("receipted")
|
|
271
|
+
elif index == 12:
|
|
272
|
+
copy_feilds["stat_opration"] = text
|
|
273
|
+
sub_feilds.append("stat_opration")
|
|
274
|
+
elif index == 13:
|
|
275
|
+
copy_feilds["more_seats"] = safe_convert_advanced(value=text)
|
|
276
|
+
sub_feilds.append("more_seats")
|
|
277
|
+
elif index == 14:
|
|
278
|
+
text_slice = text.split(" ")
|
|
279
|
+
operation_info = dict(
|
|
280
|
+
lock_btn_locator=cast(Locator, None), pop_btn_locator=cast(Locator, None), locked=""
|
|
281
|
+
)
|
|
282
|
+
if "锁定" in text_slice[0]:
|
|
283
|
+
lock_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_lock_btn(
|
|
284
|
+
locator=td_locator, timeout=timeout
|
|
285
|
+
)
|
|
286
|
+
operation_info["lock_btn_locator"] = lock_btn_locator
|
|
287
|
+
else:
|
|
288
|
+
operation_info["locked"] = text_slice[0].strip()
|
|
289
|
+
if "踢出" in text:
|
|
290
|
+
pop_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_pop_btn(
|
|
291
|
+
locator=td_locator, timeout=timeout
|
|
292
|
+
)
|
|
293
|
+
operation_info["pop_btn_locator"] = pop_btn_locator
|
|
294
|
+
copy_feilds["operation_info"] = operation_info
|
|
295
|
+
sub_feilds.append("operation_info")
|
|
296
|
+
elif index == 15:
|
|
297
|
+
copy_feilds[
|
|
298
|
+
"substitute_btn_locator"
|
|
299
|
+
] = await domestic_activity_order_po.get_flight_table_td_operation_substitute_btn(
|
|
300
|
+
locator=td_locator, timeout=timeout)
|
|
301
|
+
sub_feilds.append("substitute_btn_locator")
|
|
302
|
+
except (Exception,) as e:
|
|
303
|
+
logger.error(f"第<{index + 1}>列数据处理异常,原因:{e}")
|
|
304
|
+
if len(sub_feilds) == 22:
|
|
305
|
+
table_data.append(copy_feilds)
|
|
306
|
+
if datetime.strptime(
|
|
307
|
+
copy_feilds.get("last_time_ticket"), "%Y-%m-%d %H:%M:%S"
|
|
308
|
+
) < datetime.now() + timedelta(minutes=last_minute_threshold):
|
|
309
|
+
pre_pop_orders.append(copy_feilds)
|
|
310
|
+
for pre_pop_order in pre_pop_orders:
|
|
311
|
+
order_id = pre_pop_order.get("order_id")
|
|
312
|
+
operation_info = pre_pop_order.get("operation_info")
|
|
313
|
+
pop_btn_locator = operation_info.get("pop_btn_locator")
|
|
314
|
+
last_time_ticket = pre_pop_order.get("last_time_ticket")
|
|
315
|
+
if pop_btn_locator and isinstance(pop_btn_locator, Locator):
|
|
316
|
+
minute = (datetime.strptime(last_time_ticket, "%Y-%m-%d %H:%M:%S") - datetime.now()).total_seconds() / 60
|
|
317
|
+
await pop_btn_locator.click()
|
|
318
|
+
if is_pop is False:
|
|
319
|
+
is_pop = True
|
|
320
|
+
logger.info(
|
|
321
|
+
f"订单<{order_id}>,距离最晚出票时限: {last_time_ticket},仅剩<{minute}>分钟,已将工单剔出活动订单"
|
|
322
|
+
)
|
|
323
|
+
return table_data, is_pop
|
|
@@ -9,111 +9,115 @@
|
|
|
9
9
|
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
10
|
# ---------------------------------------------------------------------------------------------------------
|
|
11
11
|
"""
|
|
12
|
+
import os
|
|
12
13
|
import asyncio
|
|
13
|
-
|
|
14
|
+
import traceback
|
|
15
|
+
from logging import Logger
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Dict, Any, Optional
|
|
14
18
|
from qlv_helper.po.login_page import LoginPage
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from qlv_helper.utils.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return is_success, wechat_quick_login_btn
|
|
66
|
-
await on_click_locator(locator=wechat_quick_login_btn)
|
|
67
|
-
|
|
68
|
-
# 3. 点击微信弹框的中【允许】按钮
|
|
69
|
-
return await wachat_po.on_click_allow_btn(timeout=int(timeout) * 3)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
async def username_login(
|
|
73
|
-
login_po: LoginPage, username: str, password: str, timeout: float = 5.0, retry: int = 3
|
|
74
|
-
) -> Tuple[bool, str]:
|
|
75
|
-
# 1. 第一次全流程的登录
|
|
76
|
-
await _username_login(login_po=login_po, username=username, password=password, timeout=timeout)
|
|
77
|
-
for _ in range(retry):
|
|
78
|
-
# 2. 判断是否为当前页
|
|
79
|
-
if login_po.is_current_page() is False:
|
|
80
|
-
return True, f"账号:{username} 登录成功"
|
|
81
|
-
|
|
82
|
-
# 3. 判断是否存在登录警告,存在的话,继续输入验证码,再次登录
|
|
83
|
-
is_warn: bool = await login_po.is_exist_login_warn(timeout=timeout)
|
|
84
|
-
if is_warn is True:
|
|
85
|
-
# 4. 获取一层验证码
|
|
86
|
-
is_success, code_str = await login_po.get_number_code(timeout=timeout)
|
|
87
|
-
if is_success is False:
|
|
88
|
-
return is_success, code_str
|
|
89
|
-
|
|
90
|
-
# 5. 输入一层验证码
|
|
91
|
-
is_success, code_input = await login_po.get_login_number_code_input(timeout=timeout)
|
|
92
|
-
if is_success is False:
|
|
93
|
-
return is_success, code_input
|
|
94
|
-
await locator_input_element(locator=code_input, text=code_str.lower())
|
|
19
|
+
import qlv_helper.config.url_const as url_const
|
|
20
|
+
from playwright.async_api import Page, ElementHandle
|
|
21
|
+
from qlv_helper.utils.ocr_helper import get_image_text
|
|
22
|
+
|
|
23
|
+
async def _username_login(
|
|
24
|
+
*, login_po: LoginPage, logger: Logger, username: str, password: str, screenshot_dir: str, api_key: str,
|
|
25
|
+
secret_key: str, timeout: float = 5.0, attempt: int = 10
|
|
26
|
+
) -> Optional[Dict[str, Any]]:
|
|
27
|
+
for index in range(1, attempt + 1):
|
|
28
|
+
try:
|
|
29
|
+
# 1. 输入用户名
|
|
30
|
+
username_input = await login_po.get_login_username_input(timeout=timeout)
|
|
31
|
+
await username_input.fill(value=username)
|
|
32
|
+
logger.info(f"登录页面,用户名<{username}>输入完成")
|
|
33
|
+
except (Exception,):
|
|
34
|
+
pass
|
|
35
|
+
try:
|
|
36
|
+
# 2. 输入密码
|
|
37
|
+
password_input = await login_po.get_login_password_input(timeout=timeout)
|
|
38
|
+
await password_input.fill(value=password)
|
|
39
|
+
logger.info(f"登录页面,用户密码<{password}>输入完成")
|
|
40
|
+
except (Exception,):
|
|
41
|
+
pass
|
|
42
|
+
try:
|
|
43
|
+
# 3. 首次获取验证码,并点击
|
|
44
|
+
# captcha_1 = await login_po.get_captcha(timeout=timeout)
|
|
45
|
+
# await captcha_1.click(button="left")
|
|
46
|
+
# await asyncio.sleep(delay=3)
|
|
47
|
+
|
|
48
|
+
# 4. 再次获取验证码
|
|
49
|
+
captcha_2 = await login_po.get_captcha(timeout=timeout)
|
|
50
|
+
# 4.1 获取验证码类型
|
|
51
|
+
captcha_type: int = await login_po.get_captcha_type(locator=captcha_2, timeout=timeout)
|
|
52
|
+
logger.info(f"登录页面,验证码类型<{captcha_type}>获取成功")
|
|
53
|
+
# 4.2 获取验证码图片,直接截图获取原始图片字节,不刷新图片
|
|
54
|
+
image: ElementHandle = await login_po.get_captcha_image(timeout=timeout)
|
|
55
|
+
dt_str: str = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
56
|
+
fn: str = os.path.join(screenshot_dir, f"captcha_{username}_{captcha_type}_{dt_str}.png")
|
|
57
|
+
await image.screenshot(path=fn, timeout=timeout * 1000)
|
|
58
|
+
logger.info(f"登录页面,验证码图片已经生成,图片路径:{fn}")
|
|
59
|
+
# 4.3 获取验证码内容
|
|
60
|
+
capthcha_text = await get_image_text(
|
|
61
|
+
image_path=fn, captcha_type=captcha_type, api_key=api_key, secret_key=secret_key
|
|
62
|
+
)
|
|
63
|
+
logger.info(f"登录页面,验证码内容:<{capthcha_text}>识别成功")
|
|
64
|
+
|
|
65
|
+
# 5. 获取验证码输入框
|
|
66
|
+
captcha_input = await login_po.get_login_captcha_input(timeout=timeout)
|
|
67
|
+
await captcha_input.fill(value=capthcha_text)
|
|
68
|
+
logger.info(f"登录页面,验证码<{capthcha_text}>输入完成")
|
|
95
69
|
|
|
96
70
|
# 6. 点击登录
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
# 7.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
71
|
+
login_btn = await login_po.get_login_btn(timeout=timeout)
|
|
72
|
+
await login_btn.click(button="left")
|
|
73
|
+
logger.info(f"登录页面,【登录】按钮点击完成")
|
|
74
|
+
await asyncio.sleep(delay=3)
|
|
75
|
+
|
|
76
|
+
# 7. 验证登录是否成功
|
|
77
|
+
result = login_po.is_current_page()
|
|
78
|
+
if result is False:
|
|
79
|
+
logger.info(f"用户<{username}>登录成功,登录流程结束")
|
|
80
|
+
|
|
81
|
+
# 9. 获取当前cookie,不指定 path,Playwright 会返回 JSON 字符串
|
|
82
|
+
return await login_po.get_page().context.storage_state()
|
|
83
|
+
else:
|
|
84
|
+
raise RuntimeError("登录失败")
|
|
85
|
+
except (RuntimeError,):
|
|
86
|
+
if index == attempt:
|
|
87
|
+
logger.error(f"尝试登录<{attempt}>次,均失败,登录结束")
|
|
88
|
+
else:
|
|
89
|
+
logger.error(f"第<{index}>次登录失败,等待下一次登录")
|
|
90
|
+
except (Exception,):
|
|
91
|
+
logger.error(traceback.format_exc())
|
|
92
|
+
if index == attempt:
|
|
93
|
+
logger.error(f"尝试登录<{attempt}>次,均失败,登录结束")
|
|
94
|
+
else:
|
|
95
|
+
logger.error(f"第<{index}>次登录失败,等待下一次登录")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def open_login_page(
|
|
99
|
+
*, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 60.0
|
|
100
|
+
) -> LoginPage:
|
|
101
|
+
url_prefix = f"{qlv_protocol}://{qlv_domain}"
|
|
102
|
+
login_url = url_prefix + url_const.login_url
|
|
103
|
+
await page.goto(login_url)
|
|
104
|
+
|
|
105
|
+
login_po = LoginPage(page=page, url=login_url)
|
|
106
|
+
await login_po.url_wait_for(url=login_url, timeout=timeout)
|
|
107
|
+
logger.info(f"即将进入登录页,页面URL<{login_url}>")
|
|
108
|
+
return login_po
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
async def
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
async def username_login(
|
|
112
|
+
*, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, username: str, screenshot_dir: str,
|
|
113
|
+
password: str, api_key: str, secret_key: str, timeout: float = 60.0, attempt: int = 10, **kwargs: Any
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
# 1. 打开登录页面
|
|
116
|
+
login_po = await open_login_page(
|
|
117
|
+
page=page, logger=logger, qlv_domain=qlv_domain, qlv_protocol=qlv_protocol, timeout=timeout
|
|
118
|
+
)
|
|
119
|
+
# 2. 一次全流程的登录
|
|
120
|
+
return await _username_login(
|
|
121
|
+
login_po=login_po, logger=logger, username=username, password=password, screenshot_dir=screenshot_dir,
|
|
122
|
+
timeout=timeout, api_key=api_key, secret_key=secret_key, attempt=attempt
|
|
123
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: wechat_login.py
|
|
6
|
+
# Description: 微信登录模块
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/12/31
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
from typing import Tuple
|
|
13
|
+
from qlv_helper.po.login_page import LoginPage
|
|
14
|
+
from playwright.async_api import BrowserContext
|
|
15
|
+
|
|
16
|
+
from qlv_helper.utils.po_utils import on_click_locator
|
|
17
|
+
from qlv_helper.po.wechat_auth_page import WechatAuthPage
|
|
18
|
+
from qlv_helper.utils.browser_utils import switch_for_table_window
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _wechat_login(browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0) -> Tuple[bool, str]:
|
|
22
|
+
# 1. 点击微信登录快捷入口
|
|
23
|
+
is_success, wechat_entrance = await login_po.get_wechat_entrance(timeout=timeout)
|
|
24
|
+
if is_success is False:
|
|
25
|
+
return is_success, wechat_entrance
|
|
26
|
+
await on_click_locator(locator=wechat_entrance)
|
|
27
|
+
|
|
28
|
+
page_new = await switch_for_table_window(browser=browser, url_keyword="open.weixin.qq.com", wait_time=int(timeout))
|
|
29
|
+
wachat_po = WechatAuthPage(page=page_new)
|
|
30
|
+
|
|
31
|
+
# 2. 点击【微信快捷登录】按钮
|
|
32
|
+
is_success, wechat_quick_login_btn = await wachat_po.get_wechat_quick_login_btn(timeout=timeout)
|
|
33
|
+
if is_success is False:
|
|
34
|
+
return is_success, wechat_quick_login_btn
|
|
35
|
+
await on_click_locator(locator=wechat_quick_login_btn)
|
|
36
|
+
|
|
37
|
+
# 3. 点击微信弹框的中【允许】按钮
|
|
38
|
+
return await wachat_po.on_click_allow_btn(timeout=int(timeout) * 3)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def wechat_login(
|
|
42
|
+
*, browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0, retry: int = 3
|
|
43
|
+
) -> Tuple[bool, str]:
|
|
44
|
+
for index in range(1, retry + 1):
|
|
45
|
+
# 全流程的登录
|
|
46
|
+
is_success, message = await _wechat_login(browser=browser, login_po=login_po, timeout=timeout)
|
|
47
|
+
|
|
48
|
+
# 判断是否为当前页
|
|
49
|
+
if is_success is True or index == retry:
|
|
50
|
+
return is_success, message
|