python-playwright-helper 0.0.2__tar.gz → 0.1.7__tar.gz
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_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/PKG-INFO +3 -3
- python_playwright_helper-0.1.7/playwright_helper/libs/base_po.py +113 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/executor.py +71 -21
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/po_utils.py +0 -8
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/type_utils.py +20 -2
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/pyproject.toml +3 -3
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/PKG-INFO +3 -3
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/SOURCES.txt +1 -0
- python_playwright_helper-0.1.7/python_playwright_helper.egg-info/requires.txt +4 -0
- python_playwright_helper-0.0.2/python_playwright_helper.egg-info/requires.txt +0 -4
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/LICENSE +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/README.md +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/__init__.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/__init__.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/browser_pool.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/middlewares/__init__.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/middlewares/stealth.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/__init__.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/file_handle.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/log_utils.py +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/dependency_links.txt +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/top_level.txt +0 -0
- {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_playwright_helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: playwright helper python package
|
|
5
5
|
Author-email: ckf10000 <ckf10000@sina.com>
|
|
6
6
|
License: Apache License
|
|
@@ -210,8 +210,8 @@ Project-URL: Issues, https://github.com/ckf10000/playwright-helper/issues
|
|
|
210
210
|
Requires-Python: >=3.12
|
|
211
211
|
Description-Content-Type: text/markdown
|
|
212
212
|
License-File: LICENSE
|
|
213
|
-
Requires-Dist: playwright
|
|
214
|
-
Requires-Dist: playwright-stealth
|
|
213
|
+
Requires-Dist: playwright==1.56.0; python_version >= "3.12"
|
|
214
|
+
Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
|
|
215
215
|
Dynamic: license-file
|
|
216
216
|
|
|
217
217
|
# playwright-helper
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
# ProjectName: playwright-helper
|
|
5
|
+
# FileName: base_po.py
|
|
6
|
+
# Description: po对象基础类
|
|
7
|
+
# Author: ASUS
|
|
8
|
+
# CreateDate: 2025/12/13
|
|
9
|
+
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
from logging import Logger
|
|
14
|
+
from typing import List, Any, cast
|
|
15
|
+
from playwright.async_api import Page, Locator, TimeoutError as PlaywrightTimeoutError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BasePo(object):
|
|
19
|
+
__page: Page
|
|
20
|
+
|
|
21
|
+
def __init__(self, page: Page, url: str):
|
|
22
|
+
self.url = url
|
|
23
|
+
self.__page = page
|
|
24
|
+
|
|
25
|
+
def get_page(self) -> Page:
|
|
26
|
+
return self.__page
|
|
27
|
+
|
|
28
|
+
def is_current_page(self) -> bool:
|
|
29
|
+
return self.iss_current_page(self.__page, self.url)
|
|
30
|
+
|
|
31
|
+
def get_url_domain(self) -> str:
|
|
32
|
+
if isinstance(self.__page, Page):
|
|
33
|
+
page_slice: List[str] = self.__page.url.split("/")
|
|
34
|
+
return f"{page_slice[0]}://{page_slice[2]}"
|
|
35
|
+
else:
|
|
36
|
+
raise AttributeError("PO对象中的page属性未被初始化")
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def iss_current_page(page: Page, url: str) -> bool:
|
|
40
|
+
if isinstance(page, Page):
|
|
41
|
+
page_url_prefix = page.url.split("?")[0]
|
|
42
|
+
url_prefix = url.split("?")[0]
|
|
43
|
+
if page_url_prefix.endswith(url_prefix):
|
|
44
|
+
return True
|
|
45
|
+
else:
|
|
46
|
+
return False
|
|
47
|
+
else:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
async def exists(locator):
|
|
52
|
+
return await locator.count() > 0
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
async def exists_one(locator):
|
|
56
|
+
return await locator.count() == 1
|
|
57
|
+
|
|
58
|
+
async def get_locator(self, selector: str, timeout: float = 3.0) -> Locator:
|
|
59
|
+
"""
|
|
60
|
+
获取页面元素locator
|
|
61
|
+
:param selector: 选择器表达式
|
|
62
|
+
:param timeout: 超时时间(秒)
|
|
63
|
+
:return: 元素对象
|
|
64
|
+
:return:
|
|
65
|
+
"""
|
|
66
|
+
locator = self.__page.locator(selector)
|
|
67
|
+
try:
|
|
68
|
+
await locator.first.wait_for(state='visible', timeout=timeout * 1000)
|
|
69
|
+
return locator
|
|
70
|
+
except (PlaywrightTimeoutError,):
|
|
71
|
+
raise PlaywrightTimeoutError(f"元素 '{selector}' 未在 {timeout} 秒内找到")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise RuntimeError(f"检查元素时发生错误: {str(e)}")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
async def get_sub_locator(locator: Locator, selector: str, timeout: float = 3.0) -> Locator:
|
|
77
|
+
"""
|
|
78
|
+
获取页面locator的子locator
|
|
79
|
+
:param locator: 页面Locator对象
|
|
80
|
+
:param selector: 选择器表达式
|
|
81
|
+
:param timeout: 超时时间(秒)
|
|
82
|
+
:return: 元素对象
|
|
83
|
+
:return:
|
|
84
|
+
"""
|
|
85
|
+
locator_inner = locator.locator(selector)
|
|
86
|
+
try:
|
|
87
|
+
await locator_inner.first.wait_for(state='visible', timeout=timeout * 1000)
|
|
88
|
+
return locator_inner
|
|
89
|
+
except (PlaywrightTimeoutError,):
|
|
90
|
+
raise PlaywrightTimeoutError(f"元素 '{selector}' 未在 {timeout} 秒内找到")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise RuntimeError(f"检查元素时发生错误: {str(e)}")
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
async def handle_po_cookie_tip(cls, page: Any, logger: Logger, timeout: float = 3.0, selectors: List[str] = None) -> None:
|
|
96
|
+
selectors_inner: List[str] = [
|
|
97
|
+
'//div[@id="isReadedCookie"]/button',
|
|
98
|
+
'//button[@id="continue-btn"]/span[normalize-space(text())="同意"]'
|
|
99
|
+
]
|
|
100
|
+
if selectors:
|
|
101
|
+
selectors_inner.extend(selectors)
|
|
102
|
+
for selector in selectors_inner:
|
|
103
|
+
try:
|
|
104
|
+
page_inner = cast(cls, page)
|
|
105
|
+
cookie: Locator = await cls.get_locator(self=page_inner, selector=selector, timeout=timeout)
|
|
106
|
+
logger.info(
|
|
107
|
+
f'找到页面中存在cookie提示:[本网站使用cookie,用于在您的电脑中储存信息。这些cookie可以使网站正常运行,以及帮助我们改进用户体验。使用本网站,即表示您接受放置这些cookie。]')
|
|
108
|
+
await cookie.click(button="left")
|
|
109
|
+
logger.info("【同意】按钮点击完成")
|
|
110
|
+
await asyncio.sleep(1)
|
|
111
|
+
return
|
|
112
|
+
except (Exception,):
|
|
113
|
+
pass
|
{python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/executor.py
RENAMED
|
@@ -15,11 +15,13 @@ import uuid
|
|
|
15
15
|
import asyncio
|
|
16
16
|
from logging import Logger
|
|
17
17
|
from playwright.async_api import async_playwright
|
|
18
|
+
from playwright_helper.utils.type_utils import RunResult
|
|
18
19
|
from playwright_helper.utils.log_utils import logger as log
|
|
19
20
|
from playwright_helper.libs.browser_pool import BrowserPool
|
|
20
21
|
from playwright_helper.utils.file_handle import get_caller_dir
|
|
21
|
-
from playwright.async_api import Page, Browser, BrowserContext
|
|
22
22
|
from typing import Any, List, Optional, cast, Callable, Literal
|
|
23
|
+
from playwright.async_api import Page, Browser, BrowserContext, TimeoutError as PlaywrightTimeoutError, \
|
|
24
|
+
Error as PlaywrightError
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class PlaywrightBrowserExecutor:
|
|
@@ -61,7 +63,7 @@ class PlaywrightBrowserExecutor:
|
|
|
61
63
|
async def _safe_screenshot(self, page: Page, name: str = None):
|
|
62
64
|
try:
|
|
63
65
|
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
64
|
-
if name is None:
|
|
66
|
+
if name is None or "unknown" in name:
|
|
65
67
|
name = f"error_{int(time.time())}"
|
|
66
68
|
path = os.path.join(self.screenshot_dir, f"{name}.png")
|
|
67
69
|
await page.screenshot(path=path)
|
|
@@ -121,12 +123,23 @@ class PlaywrightBrowserExecutor:
|
|
|
121
123
|
return context
|
|
122
124
|
|
|
123
125
|
async def _cleanup_context(self, context: BrowserContext):
|
|
124
|
-
|
|
126
|
+
if getattr(context, "_closed", False):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
context._closed = True # type: ignore
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
await context.close()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.debug(f"[Cleanup] ignore close error: {e}")
|
|
125
135
|
|
|
126
136
|
browser = getattr(context, "_browser", None)
|
|
127
137
|
if browser:
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
try:
|
|
139
|
+
self.logger.debug("[Executor] Release browser to pool")
|
|
140
|
+
await self.browser_pool.release(browser)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self.logger.error(f"[Cleanup] ignore release error: {e}")
|
|
130
143
|
|
|
131
144
|
async def _run_callback_chain(self, *, callback: Callable, page: Page, context: BrowserContext, **kwargs) -> Any:
|
|
132
145
|
# Run middlewares before callback
|
|
@@ -136,38 +149,75 @@ class PlaywrightBrowserExecutor:
|
|
|
136
149
|
# Main callback
|
|
137
150
|
return await callback(page=page, logger=self.logger, context=context, **kwargs)
|
|
138
151
|
|
|
139
|
-
async def run(self, *, callback: Callable, **kwargs: Any) ->
|
|
152
|
+
async def run(self, *, callback: Callable, **kwargs: Any) -> RunResult:
|
|
140
153
|
attempt = 0
|
|
154
|
+
last_error: Optional[Exception] = None
|
|
155
|
+
task_id: Optional[str] = "unknown"
|
|
156
|
+
result: Any = None
|
|
141
157
|
|
|
142
158
|
while attempt <= self.retries:
|
|
143
159
|
page = None
|
|
144
160
|
context: BrowserContext = cast(BrowserContext, None)
|
|
145
161
|
try:
|
|
146
162
|
context = await self._create_context()
|
|
163
|
+
task_id = getattr(context, "_task_id", "unknown")
|
|
147
164
|
page = await context.new_page()
|
|
148
165
|
|
|
149
166
|
result = await self._run_callback_chain(
|
|
150
167
|
callback=callback, page=page, context=context, **kwargs
|
|
151
168
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
self.logger.info(f"[Task<{task_id}> Success]")
|
|
170
|
+
return RunResult(
|
|
171
|
+
success=True,
|
|
172
|
+
attempts=attempt + 1,
|
|
173
|
+
task_id=task_id,
|
|
174
|
+
error=last_error,
|
|
175
|
+
result=result
|
|
176
|
+
)
|
|
177
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
178
|
+
# ⚠️ 不要当成错误
|
|
179
|
+
self.logger.warning(f"[Task<{task_id}> Cancelled]")
|
|
180
|
+
|
|
181
|
+
# 清理资源
|
|
182
|
+
if context and self.record_trace:
|
|
183
|
+
os.makedirs(self.trace_dir, exist_ok=True)
|
|
184
|
+
trace_path = os.path.join(self.trace_dir, f"{task_id}.zip")
|
|
185
|
+
await context.tracing.stop(path=trace_path)
|
|
186
|
+
self.logger.error(f"[Trace Saved] {trace_path}")
|
|
187
|
+
|
|
188
|
+
# ❗关键:不要 raise
|
|
189
|
+
return RunResult(
|
|
190
|
+
success=False,
|
|
191
|
+
attempts=attempt + 1,
|
|
192
|
+
error=asyncio.CancelledError(),
|
|
193
|
+
task_id=task_id,
|
|
194
|
+
result=result
|
|
195
|
+
)
|
|
196
|
+
except (PlaywrightTimeoutError, PlaywrightError, Exception) as e:
|
|
197
|
+
last_error = e
|
|
198
|
+
self.logger.error(f"[Task<{task_id}> Attempt {attempt + 1} Failed] {e}")
|
|
157
199
|
if page:
|
|
158
|
-
await self._safe_screenshot(page, task_id)
|
|
200
|
+
await self._safe_screenshot(page=page, name=task_id)
|
|
159
201
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
if context and self.record_trace:
|
|
203
|
+
os.makedirs(self.trace_dir, exist_ok=True)
|
|
204
|
+
trace_path = os.path.join(self.trace_dir, f"{task_id}.zip")
|
|
205
|
+
await context.tracing.stop(path=trace_path)
|
|
206
|
+
self.logger.error(f"[Trace Saved] {trace_path}")
|
|
165
207
|
|
|
166
208
|
attempt += 1
|
|
167
|
-
if attempt
|
|
168
|
-
|
|
169
|
-
raise e
|
|
170
|
-
await asyncio.sleep(1)
|
|
209
|
+
if attempt <= self.retries:
|
|
210
|
+
await asyncio.sleep(1)
|
|
171
211
|
finally:
|
|
172
212
|
if context:
|
|
173
213
|
await self._cleanup_context(context)
|
|
214
|
+
# 所有重试结束,仍然失败
|
|
215
|
+
self.logger.error(f"[Task<{task_id}> Final Failure]")
|
|
216
|
+
|
|
217
|
+
return RunResult(
|
|
218
|
+
success=False,
|
|
219
|
+
attempts=attempt,
|
|
220
|
+
error=last_error,
|
|
221
|
+
task_id=task_id,
|
|
222
|
+
result=result
|
|
223
|
+
)
|
|
@@ -15,14 +15,6 @@ from playwright.async_api import Page, Locator
|
|
|
15
15
|
MouseButton = Literal["left", "middle", "right"]
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
async def exists(locator):
|
|
19
|
-
return await locator.count() > 0
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
async def exists_one(locator):
|
|
23
|
-
return await locator.count() == 1
|
|
24
|
-
|
|
25
|
-
|
|
26
18
|
async def on_click_locator(locator: Locator, button: MouseButton = "left") -> bool:
|
|
27
19
|
try:
|
|
28
20
|
await locator.click(button=button)
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
# ---------------------------------------------------------------------------------------------------------
|
|
11
11
|
"""
|
|
12
12
|
import re
|
|
13
|
-
from
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, Tuple, Union, Optional
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def safe_convert_advanced(value, return_type='auto') -> Any:
|
|
@@ -85,6 +86,7 @@ def safe_convert_advanced(value, return_type='auto') -> Any:
|
|
|
85
86
|
# 所有转换都失败,返回原值
|
|
86
87
|
return value
|
|
87
88
|
|
|
89
|
+
|
|
88
90
|
def convert_order_amount_text(amount_text: str) -> Tuple[bool, Union[Dict[str, Any], str]]:
|
|
89
91
|
"""
|
|
90
92
|
将页面中的金额文案,解析成字典数据
|
|
@@ -108,4 +110,20 @@ def convert_order_amount_text(amount_text: str) -> Tuple[bool, Union[Dict[str, A
|
|
|
108
110
|
amount = safe_convert_advanced(value=amount_str.replace(",", ""))
|
|
109
111
|
else:
|
|
110
112
|
return False, f"订单金额文案[{amount_text}]解析金额信息有异常"
|
|
111
|
-
return True, dict(currency=currency, amount=amount)
|
|
113
|
+
return True, dict(currency=currency, amount=amount)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class RunResult:
|
|
118
|
+
"""
|
|
119
|
+
success:是否成功
|
|
120
|
+
attempts:实际尝试次数
|
|
121
|
+
error:最终失败的异常(有就带,没有就 None)
|
|
122
|
+
task_id:方便框架层日志关联
|
|
123
|
+
result: 执行结果
|
|
124
|
+
"""
|
|
125
|
+
success: bool
|
|
126
|
+
attempts: int
|
|
127
|
+
error: Optional[Exception] = None
|
|
128
|
+
task_id: Optional[str] = None
|
|
129
|
+
result: Optional[Any] = None
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python_playwright_helper" # pip包名(不要大写)
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.1.7" # 版本号
|
|
8
8
|
description = "playwright helper python package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12" # Python 版本要求
|
|
@@ -16,8 +16,8 @@ authors = [
|
|
|
16
16
|
|
|
17
17
|
# 运行时依赖
|
|
18
18
|
dependencies = [
|
|
19
|
-
"playwright
|
|
20
|
-
"playwright-stealth
|
|
19
|
+
"playwright==1.56.0; python_version >= '3.12'",
|
|
20
|
+
"playwright-stealth==2.0.0; python_version >= '3.12'",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_playwright_helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: playwright helper python package
|
|
5
5
|
Author-email: ckf10000 <ckf10000@sina.com>
|
|
6
6
|
License: Apache License
|
|
@@ -210,8 +210,8 @@ Project-URL: Issues, https://github.com/ckf10000/playwright-helper/issues
|
|
|
210
210
|
Requires-Python: >=3.12
|
|
211
211
|
Description-Content-Type: text/markdown
|
|
212
212
|
License-File: LICENSE
|
|
213
|
-
Requires-Dist: playwright
|
|
214
|
-
Requires-Dist: playwright-stealth
|
|
213
|
+
Requires-Dist: playwright==1.56.0; python_version >= "3.12"
|
|
214
|
+
Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
|
|
215
215
|
Dynamic: license-file
|
|
216
216
|
|
|
217
217
|
# playwright-helper
|
|
File without changes
|
|
File without changes
|
{python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/__init__.py
RENAMED
|
File without changes
|
{python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|