python-qlv-helper 0.2.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.
- python_qlv_helper-0.2.0.dist-info/METADATA +252 -0
- python_qlv_helper-0.2.0.dist-info/RECORD +32 -0
- python_qlv_helper-0.2.0.dist-info/WHEEL +5 -0
- python_qlv_helper-0.2.0.dist-info/licenses/LICENSE +201 -0
- python_qlv_helper-0.2.0.dist-info/top_level.txt +1 -0
- qlv_helper/__init__.py +11 -0
- qlv_helper/controller/__init__.py +11 -0
- qlv_helper/controller/domestic_activity_order.py +24 -0
- qlv_helper/controller/main_page.py +30 -0
- qlv_helper/controller/order_detail.py +35 -0
- qlv_helper/controller/order_table.py +145 -0
- qlv_helper/controller/user_login.py +119 -0
- qlv_helper/http/__init__.py +11 -0
- qlv_helper/http/main_page.py +41 -0
- qlv_helper/http/order_page.py +313 -0
- qlv_helper/http/order_table_page.py +323 -0
- qlv_helper/po/__init__.py +11 -0
- qlv_helper/po/base_po.py +40 -0
- qlv_helper/po/domestic_activity_order_page.py +129 -0
- qlv_helper/po/login_page.py +136 -0
- qlv_helper/po/main_page.py +71 -0
- qlv_helper/po/wechat_auth_page.py +68 -0
- qlv_helper/utils/__init__.py +11 -0
- qlv_helper/utils/browser_utils.py +25 -0
- qlv_helper/utils/datetime_utils.py +16 -0
- qlv_helper/utils/file_handle.py +33 -0
- qlv_helper/utils/html_utils.py +59 -0
- qlv_helper/utils/ocr_helper.py +83 -0
- qlv_helper/utils/po_utils.py +113 -0
- qlv_helper/utils/stealth_browser.py +100 -0
- qlv_helper/utils/type_utils.py +111 -0
- qlv_helper/utils/windows_utils.py +36 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: main_page.py
|
|
6
|
+
# Description: 首页页面对象
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/25
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
from typing import Tuple, Union
|
|
13
|
+
from qlv_helper.po.base_po import BasePo
|
|
14
|
+
from qlv_helper.utils.ocr_helper import get_image_text
|
|
15
|
+
from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MainPage(BasePo):
|
|
19
|
+
url: str = "/"
|
|
20
|
+
__page: Page
|
|
21
|
+
|
|
22
|
+
def __init__(self, page: Page, url: str = "/") -> None:
|
|
23
|
+
super().__init__(page, url)
|
|
24
|
+
self.url = url
|
|
25
|
+
self.__page = page
|
|
26
|
+
|
|
27
|
+
async def get_confirm_btn_with_system_notice_dialog(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
|
|
28
|
+
"""
|
|
29
|
+
获取系统通知弹框中的确认按钮,注意这个地方,存在多个叠加的弹框,因此用last()方法,只需定位到最上面的那个弹框就行
|
|
30
|
+
:return:
|
|
31
|
+
"""
|
|
32
|
+
selector: str = "//div[@class='CommonAlert'][last()]//a[@class='CommonAlertBtnConfirm']"
|
|
33
|
+
try:
|
|
34
|
+
locator = self.__page.locator(selector)
|
|
35
|
+
if locator:
|
|
36
|
+
await locator.wait_for(state='visible', timeout=timeout * 1000)
|
|
37
|
+
return True, locator
|
|
38
|
+
else:
|
|
39
|
+
return False, '没有找到首页中的【系统提醒-确定】按钮'
|
|
40
|
+
except PlaywrightTimeoutError:
|
|
41
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
44
|
+
|
|
45
|
+
async def get_level1_menu_order_checkout(self) -> Tuple[bool, Union[Locator, str]]:
|
|
46
|
+
selector: str = "//span[contains(normalize-space(), '订单出票')]"
|
|
47
|
+
try:
|
|
48
|
+
locator = self.__page.locator(selector)
|
|
49
|
+
if locator:
|
|
50
|
+
await locator.wait_for(state='visible', timeout=timeout * 1000)
|
|
51
|
+
return True, locator
|
|
52
|
+
else:
|
|
53
|
+
return False, '没有找到首页中的【订单出票】左侧一级导航菜单'
|
|
54
|
+
except PlaywrightTimeoutError:
|
|
55
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
58
|
+
|
|
59
|
+
async def get_level2_menu_order_checkout(self) -> Tuple[bool, Union[Locator, str]]:
|
|
60
|
+
selector: str = "//a[@menuname='国内活动订单']"
|
|
61
|
+
try:
|
|
62
|
+
locator = self.__page.locator(selector)
|
|
63
|
+
if locator:
|
|
64
|
+
await locator.wait_for(state='visible', timeout=timeout * 1000)
|
|
65
|
+
return True, locator
|
|
66
|
+
else:
|
|
67
|
+
return False, '没有找到首页中的【国内活动订单】左侧二级导航菜单'
|
|
68
|
+
except PlaywrightTimeoutError:
|
|
69
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: wechat_auth_page.py
|
|
6
|
+
# Description: 微信认证页面对象
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/27
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import asyncio
|
|
14
|
+
from typing import Tuple, Union
|
|
15
|
+
from qlv_helper.po.base_po import BasePo
|
|
16
|
+
from qlv_helper.utils.file_handle import get_caller_dir
|
|
17
|
+
from qlv_helper.utils.windows_utils import gen_allow_btn_image, windows_on_click
|
|
18
|
+
from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WechatAuthPage(BasePo):
|
|
22
|
+
__page: Page
|
|
23
|
+
|
|
24
|
+
def __init__(self, page: Page, url: str = "/connect/qrconnect") -> None:
|
|
25
|
+
super().__init__(page, url)
|
|
26
|
+
self.__page = page
|
|
27
|
+
|
|
28
|
+
async def get_wechat_quick_login_btn(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
|
|
29
|
+
selector: str = '//*[@id="tpl_for_page"]//button[text()="微信快捷登录"]'
|
|
30
|
+
try:
|
|
31
|
+
locator = self.__page.locator(selector)
|
|
32
|
+
if locator:
|
|
33
|
+
await locator.wait_for(state='visible', timeout=timeout * 1000)
|
|
34
|
+
return True, locator
|
|
35
|
+
else:
|
|
36
|
+
return False, '没有找到登录页面中的【微信快捷登录】按钮'
|
|
37
|
+
except PlaywrightTimeoutError:
|
|
38
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
41
|
+
|
|
42
|
+
async def get_dialog_allow_btn(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
|
|
43
|
+
selector: str = '//div[@id="dialog_allow_btn"]'
|
|
44
|
+
try:
|
|
45
|
+
locator = self.__page.locator(selector)
|
|
46
|
+
if locator:
|
|
47
|
+
await locator.wait_for(state='visible', timeout=timeout * 1000)
|
|
48
|
+
return True, locator
|
|
49
|
+
else:
|
|
50
|
+
return False, ',没有找到申请登录弹框中的【允许】按钮'
|
|
51
|
+
except PlaywrightTimeoutError:
|
|
52
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
55
|
+
|
|
56
|
+
async def on_click_allow_btn(self, timeout: int = 5) -> Tuple[bool, str]:
|
|
57
|
+
try:
|
|
58
|
+
image_name = os.path.join(get_caller_dir(), "allow_btn_image.png")
|
|
59
|
+
gen_allow_btn_image(image_name=image_name)
|
|
60
|
+
await asyncio.sleep(delay=timeout)
|
|
61
|
+
windows_on_click(image_name=image_name, windows_title="微信", is_delete=True)
|
|
62
|
+
await asyncio.sleep(delay=timeout)
|
|
63
|
+
if self.is_current_page():
|
|
64
|
+
return False, "【微信快捷登录】登录失败,需要重新登录"
|
|
65
|
+
else:
|
|
66
|
+
return True, "【微信快捷登录】登录成功"
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return False, str(e)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: __init__.py
|
|
6
|
+
# Description: 工具包
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/25
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: browser_utils.py
|
|
6
|
+
# Description: 浏览器工具模块
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/27
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from playwright.async_api import BrowserContext, Page
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def switch_for_table_window(browser: BrowserContext, url_keyword: str, wait_time: int = 10) -> Optional[Page]:
|
|
18
|
+
# 最多等待 wait_time 秒
|
|
19
|
+
for _ in range(wait_time):
|
|
20
|
+
await asyncio.sleep(1)
|
|
21
|
+
for page in browser.pages:
|
|
22
|
+
if url_keyword.lower() in page.url.lower():
|
|
23
|
+
await page.bring_to_front()
|
|
24
|
+
return page
|
|
25
|
+
return None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: datetime_utils.py
|
|
6
|
+
# Description: 时间处理工具模块
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/28
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_current_dtstr() -> str:
|
|
16
|
+
return datetime.now().strftime("%Y%m%d%H%M%S")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: file_handle.py
|
|
6
|
+
# Description: 文件处理工具模块
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/25
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import inspect
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_caller_dir() -> str:
|
|
17
|
+
# 获取调用者的 frame
|
|
18
|
+
frame = inspect.stack()[1]
|
|
19
|
+
caller_file = frame.filename # 调用者文件的完整路径
|
|
20
|
+
return os.path.dirname(os.path.abspath(caller_file))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_image(file_name: str, img_bytes: bytes) -> None:
|
|
24
|
+
"""
|
|
25
|
+
保存验证码图片到本地。
|
|
26
|
+
若文件已存在,会自动覆盖。
|
|
27
|
+
"""
|
|
28
|
+
# 确保目录存在
|
|
29
|
+
os.makedirs(os.path.dirname(file_name), exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# "wb" 会覆盖已有文件
|
|
32
|
+
with open(file_name, "wb") as f:
|
|
33
|
+
f.write(img_bytes)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: html_utils.py
|
|
6
|
+
# Description: html处理工具模块
|
|
7
|
+
# Author: zhouhanlin
|
|
8
|
+
# CreateDate: 2025/12/01
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
from bs4 import BeautifulSoup
|
|
14
|
+
from typing import Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_pagination_info(html: str) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
解析分页信息
|
|
20
|
+
"""
|
|
21
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
22
|
+
|
|
23
|
+
pagination_info = {
|
|
24
|
+
"current_page": 1,
|
|
25
|
+
"pages": 1,
|
|
26
|
+
"is_next_page": False,
|
|
27
|
+
"is_pre_page": False,
|
|
28
|
+
"total": 0,
|
|
29
|
+
"page_size": 20
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# 找到分页的div
|
|
33
|
+
pagination_div = soup.find('div', {'class': 'redirect'})
|
|
34
|
+
if not pagination_div:
|
|
35
|
+
return pagination_info
|
|
36
|
+
|
|
37
|
+
# 解析两个label
|
|
38
|
+
labels = pagination_div.find_all('label')
|
|
39
|
+
|
|
40
|
+
if len(labels) >= 2:
|
|
41
|
+
# 当前页码
|
|
42
|
+
pagination_info["current_page"] = int(labels[0].get_text(strip=True))
|
|
43
|
+
# 总页数
|
|
44
|
+
pagination_info["pages"] = int(labels[1].get_text(strip=True))
|
|
45
|
+
|
|
46
|
+
# 解析显示记录信息
|
|
47
|
+
span_text = pagination_div.find('span').get_text(strip=True) if pagination_div.find('span') else ''
|
|
48
|
+
# 提取数字
|
|
49
|
+
numbers = re.findall(r'\d+', span_text)
|
|
50
|
+
if len(numbers) >= 3:
|
|
51
|
+
pagination_info["page_size"] = int(numbers[1])
|
|
52
|
+
pagination_info["total"] = int(numbers[2])
|
|
53
|
+
|
|
54
|
+
if pagination_info["current_page"] == 1:
|
|
55
|
+
pagination_info["is_pre_page"] = False
|
|
56
|
+
if pagination_info["pages"] > 1 and pagination_info["current_page"] < pagination_info["pages"]:
|
|
57
|
+
pagination_info["is_next_page"] = True
|
|
58
|
+
|
|
59
|
+
return pagination_info
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: ocr_helper.py
|
|
6
|
+
# Description: OCR模块
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/25
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import ddddocr
|
|
13
|
+
import requests
|
|
14
|
+
from typing import Union, Tuple
|
|
15
|
+
from aiohttp import ClientSession
|
|
16
|
+
from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError
|
|
17
|
+
|
|
18
|
+
# 复用 OCR 实例,不用每次都重新加载模型(更快)
|
|
19
|
+
_ocr = ddddocr.DdddOcr(show_ad=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def fetch_and_ocr_captcha(url: str) -> Tuple[str, bytes]:
|
|
23
|
+
# 1) 请求验证码图片
|
|
24
|
+
resp = requests.get(url, timeout=10)
|
|
25
|
+
img_bytes = resp.content
|
|
26
|
+
|
|
27
|
+
# 2) OCR 识别
|
|
28
|
+
result = _ocr.classification(img_bytes)
|
|
29
|
+
|
|
30
|
+
return result, img_bytes
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def async_fetch_and_ocr_captcha(url: str) -> Tuple[str, bytes]:
|
|
34
|
+
async with ClientSession() as session:
|
|
35
|
+
async with session.get(url, timeout=10) as resp:
|
|
36
|
+
img_bytes = await resp.read()
|
|
37
|
+
|
|
38
|
+
result = _ocr.classification(img_bytes)
|
|
39
|
+
return result, img_bytes
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def recognize_captcha(image: Union[str, bytes]) -> str:
|
|
43
|
+
"""
|
|
44
|
+
识别验证码图片,返回识别文本。
|
|
45
|
+
参数:
|
|
46
|
+
image: 图片路径 str,或图片的二进制 bytes
|
|
47
|
+
返回:
|
|
48
|
+
识别出的验证码字符串
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
# 如果是路径,读取文件
|
|
52
|
+
if isinstance(image, str):
|
|
53
|
+
with open(image, "rb") as f:
|
|
54
|
+
img_bytes = f.read()
|
|
55
|
+
else:
|
|
56
|
+
img_bytes = image
|
|
57
|
+
|
|
58
|
+
result = _ocr.classification(img_bytes)
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise RuntimeError(f"OCR 识别失败: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def get_image_text(page: Page, selector: str, timeout: float = 5.0) -> Tuple[bool, str]:
|
|
66
|
+
try:
|
|
67
|
+
# 找到 img
|
|
68
|
+
locator = page.locator(selector)
|
|
69
|
+
if locator:
|
|
70
|
+
img = await locator.element_handle(timeout=timeout * 1000)
|
|
71
|
+
|
|
72
|
+
# 直接截图获取原始图片字节,不刷新图片
|
|
73
|
+
img_bytes = await img.screenshot(timeout=timeout * 1000)
|
|
74
|
+
|
|
75
|
+
# OCR 识别
|
|
76
|
+
text = _ocr.classification(img_bytes)
|
|
77
|
+
return True, text.strip()
|
|
78
|
+
else:
|
|
79
|
+
return False, f'没有找到当前页面中的【{selector}】图片'
|
|
80
|
+
except PlaywrightTimeoutError:
|
|
81
|
+
return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return False, f"检查元素时发生错误: {str(e)}"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: po_utils.py
|
|
6
|
+
# Description: po对象处理工具
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/25
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
from typing import Literal, Union, Tuple
|
|
13
|
+
from playwright.async_api import Page, Locator
|
|
14
|
+
|
|
15
|
+
MouseButton = Literal["left", "middle", "right"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def on_click_locator(locator: Locator, button: MouseButton = "left") -> bool:
|
|
19
|
+
try:
|
|
20
|
+
await locator.click(button=button)
|
|
21
|
+
return True
|
|
22
|
+
except(Exception,):
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def on_click_element(page: Page, selector: str, button: MouseButton = "left") -> bool:
|
|
27
|
+
try:
|
|
28
|
+
await page.locator(selector).click(button=button)
|
|
29
|
+
return True
|
|
30
|
+
except(Exception,):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def locator_input_element(locator: Locator, text: str) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
输入内容,输入前会自动清空原来的内容
|
|
37
|
+
:param locator: Locator对象
|
|
38
|
+
:param text: 输入的内容
|
|
39
|
+
:return:
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
# 填入文字,会清空原内容
|
|
43
|
+
if isinstance(text, str) is False:
|
|
44
|
+
text = str(text)
|
|
45
|
+
await locator.fill(text)
|
|
46
|
+
return True
|
|
47
|
+
except (Exception,):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def simulation_input_element(locator: Locator, text: str, delay: int = 100) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
模拟逐字符输入(可用于触发 JS 事件)
|
|
54
|
+
:param locator: 元素定位器对象
|
|
55
|
+
:param text: 输入的内容
|
|
56
|
+
:param int delay: 每个字符延迟输入 100ms
|
|
57
|
+
:return:
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
if isinstance(text, str) is False:
|
|
61
|
+
text = str(text)
|
|
62
|
+
await locator.type(text, delay=delay)
|
|
63
|
+
return True
|
|
64
|
+
except (Exception,):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def page_screenshot(page: Page, file_name: str = None, is_full_page: bool = False) -> Union[bytes, None]:
|
|
69
|
+
"""
|
|
70
|
+
保存截图,默认截图整个可见页面(不是整个滚动区域)
|
|
71
|
+
:param page:
|
|
72
|
+
:param file_name: 文件名,若没传递,这返回bytes
|
|
73
|
+
:param is_full_page: true---> 截图整个网页(包括滚动区域)
|
|
74
|
+
:return:
|
|
75
|
+
"""
|
|
76
|
+
if is_full_page is True:
|
|
77
|
+
if file_name is None:
|
|
78
|
+
return await page.screenshot()
|
|
79
|
+
await page.screenshot(path=file_name, full_page=True)
|
|
80
|
+
else:
|
|
81
|
+
await page.screenshot(path=file_name)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def locator_element_screenshot(locator: Locator, file_name: str = None) -> bytes:
|
|
85
|
+
"""
|
|
86
|
+
元素截图
|
|
87
|
+
:param locator: Locator对象
|
|
88
|
+
:param file_name: 文件名,若没传递,这返回bytes
|
|
89
|
+
:return:
|
|
90
|
+
"""
|
|
91
|
+
if file_name is None:
|
|
92
|
+
return await locator.screenshot(path=file_name)
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
await locator.screenshot(path=file_name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def is_exists_value(element: Locator) -> Tuple[bool, Union[str, None]]:
|
|
99
|
+
"""
|
|
100
|
+
判断一个元素是否存在值
|
|
101
|
+
:param element:
|
|
102
|
+
:return:
|
|
103
|
+
"""
|
|
104
|
+
value = await element.input_value()
|
|
105
|
+
# value = await element.get_attribute("value")
|
|
106
|
+
if value:
|
|
107
|
+
value = value.strip()
|
|
108
|
+
if len(value) > 0:
|
|
109
|
+
return True, value
|
|
110
|
+
else:
|
|
111
|
+
return False, None
|
|
112
|
+
else:
|
|
113
|
+
return False, None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: qlv-helper
|
|
5
|
+
# FileName: stealth_browser.py
|
|
6
|
+
# Description: 防检测浏览器配置
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/11/27
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
from playwright.async_api import Page
|
|
13
|
+
|
|
14
|
+
CHROME_STEALTH_ARGS = [
|
|
15
|
+
# 核心防检测
|
|
16
|
+
'--disable-blink-features=AutomationControlled',
|
|
17
|
+
'--disable-automation-controlled-blink-features',
|
|
18
|
+
|
|
19
|
+
# 隐藏"Chrome正受到自动测试软件控制"提示
|
|
20
|
+
'--disable-infobars',
|
|
21
|
+
'--disable-popup-blocking',
|
|
22
|
+
|
|
23
|
+
# 性能优化
|
|
24
|
+
'--no-first-run',
|
|
25
|
+
'--no-default-browser-check',
|
|
26
|
+
'--disable-default-apps',
|
|
27
|
+
'--disable-translate',
|
|
28
|
+
|
|
29
|
+
# 禁用自动化标志
|
|
30
|
+
'--disable-background-timer-throttling',
|
|
31
|
+
'--disable-backgrounding-occluded-windows',
|
|
32
|
+
'--disable-renderer-backgrounding',
|
|
33
|
+
|
|
34
|
+
# 网络和安全
|
|
35
|
+
'--disable-web-security',
|
|
36
|
+
'--disable-features=VizDisplayCompositor',
|
|
37
|
+
'--disable-features=RendererCodeIntegrity',
|
|
38
|
+
'--remote-debugging-port=0', # 随机端口
|
|
39
|
+
|
|
40
|
+
# 硬件相关(减少特征)
|
|
41
|
+
'--disable-gpu',
|
|
42
|
+
'--disable-software-rasterizer',
|
|
43
|
+
'--disable-dev-shm-usage',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
IGNORE_ARGS = [
|
|
47
|
+
'--enable-automation',
|
|
48
|
+
'--enable-automation-controlled-blink-features',
|
|
49
|
+
'--password-store=basic', # 避免密码存储提示
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
|
|
53
|
+
|
|
54
|
+
viewport = {'width': 1920, 'height': 1080}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def setup_stealth_page(page: Page):
|
|
58
|
+
"""设置页面为隐身模式"""
|
|
59
|
+
# 修改 navigator.webdriver
|
|
60
|
+
await page.add_init_script("""
|
|
61
|
+
// 进一步修改 navigator 属性
|
|
62
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
63
|
+
get: () => undefined,
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
66
|
+
get: () => [1, 2, 3, 4, 5],
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(navigator, 'languages', {
|
|
69
|
+
get: () => ['zh-CN', 'zh', 'en'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 删除 webdriver 属性
|
|
73
|
+
delete navigator.__proto__.webdriver;
|
|
74
|
+
|
|
75
|
+
// 修改 plugins
|
|
76
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
77
|
+
get: () => [{
|
|
78
|
+
name: 'Chrome PDF Plugin',
|
|
79
|
+
filename: 'internal-pdf-viewer'
|
|
80
|
+
}],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 修改 languages
|
|
84
|
+
Object.defineProperty(navigator, 'languages', {
|
|
85
|
+
get: () => ['zh-CN', 'zh', 'en-US', 'en'],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 修改 platform
|
|
89
|
+
Object.defineProperty(navigator, 'platform', {
|
|
90
|
+
get: () => 'Win32',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 隐藏 chrome 对象
|
|
94
|
+
window.chrome = {
|
|
95
|
+
runtime: {},
|
|
96
|
+
loadTimes: function(){},
|
|
97
|
+
csi: function(){},
|
|
98
|
+
app: {}
|
|
99
|
+
};
|
|
100
|
+
""")
|