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.
@@ -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
+ """)