yuehua-ziniao-webdriver 0.1.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.
- yuehua_ziniao_webdriver/__init__.py +149 -0
- yuehua_ziniao_webdriver/browser.py +258 -0
- yuehua_ziniao_webdriver/client.py +435 -0
- yuehua_ziniao_webdriver/config.py +265 -0
- yuehua_ziniao_webdriver/exceptions.py +237 -0
- yuehua_ziniao_webdriver/http_client.py +214 -0
- yuehua_ziniao_webdriver/process.py +270 -0
- yuehua_ziniao_webdriver/store.py +429 -0
- yuehua_ziniao_webdriver/types.py +172 -0
- yuehua_ziniao_webdriver/utils.py +310 -0
- yuehua_ziniao_webdriver-0.1.0.dist-info/METADATA +438 -0
- yuehua_ziniao_webdriver-0.1.0.dist-info/RECORD +15 -0
- yuehua_ziniao_webdriver-0.1.0.dist-info/WHEEL +5 -0
- yuehua_ziniao_webdriver-0.1.0.dist-info/licenses/LICENSE +21 -0
- yuehua_ziniao_webdriver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""类型定义模块
|
|
2
|
+
|
|
3
|
+
定义所有用于类型提示的 TypedDict、协议和类型别名。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Dict, List, Optional, Protocol, Union
|
|
8
|
+
|
|
9
|
+
# Python 3.8 兼容性处理
|
|
10
|
+
if sys.version_info >= (3, 8):
|
|
11
|
+
from typing import Literal, TypedDict
|
|
12
|
+
else:
|
|
13
|
+
from typing_extensions import Literal, TypedDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# 店铺相关类型
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
class StoreInfo(TypedDict, total=False):
|
|
21
|
+
"""店铺信息"""
|
|
22
|
+
browserOauth: str # 店铺 OAuth 标识(必需)
|
|
23
|
+
browserName: str # 店铺名称(必需)
|
|
24
|
+
browserId: Optional[str] # 店铺 ID
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Store(TypedDict, total=False):
|
|
28
|
+
"""店铺对象(扩展版)"""
|
|
29
|
+
browserOauth: str
|
|
30
|
+
browserName: str
|
|
31
|
+
browserId: Optional[str]
|
|
32
|
+
# 可能的其他字段
|
|
33
|
+
browserType: Optional[str]
|
|
34
|
+
createTime: Optional[str]
|
|
35
|
+
updateTime: Optional[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# 浏览器相关类型
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
class BrowserStartResult(TypedDict, total=False):
|
|
43
|
+
"""打开店铺返回结果"""
|
|
44
|
+
statusCode: int # 状态码,0 表示成功
|
|
45
|
+
debuggingPort: int # 调试端口
|
|
46
|
+
browserOauth: str # 店铺 OAuth 标识
|
|
47
|
+
browserId: Optional[str] # 店铺 ID
|
|
48
|
+
ipDetectionPage: str # IP 检测页面 URL
|
|
49
|
+
launcherPage: str # 启动页面 URL
|
|
50
|
+
message: Optional[str] # 错误消息
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BrowserStopResult(TypedDict):
|
|
54
|
+
"""关闭店铺返回结果"""
|
|
55
|
+
statusCode: int
|
|
56
|
+
message: Optional[str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BrowserListResult(TypedDict):
|
|
60
|
+
"""获取店铺列表返回结果"""
|
|
61
|
+
statusCode: int
|
|
62
|
+
browserList: List[Store]
|
|
63
|
+
message: Optional[str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# HTTP 通信相关类型
|
|
68
|
+
# ============================================================================
|
|
69
|
+
|
|
70
|
+
class HttpRequestData(TypedDict, total=False):
|
|
71
|
+
"""HTTP 请求数据"""
|
|
72
|
+
action: str # 操作类型
|
|
73
|
+
requestId: str # 请求 ID
|
|
74
|
+
company: str # 企业名称
|
|
75
|
+
username: str # 用户名
|
|
76
|
+
password: str # 密码
|
|
77
|
+
# 其他可选字段
|
|
78
|
+
browserId: Optional[str]
|
|
79
|
+
browserOauth: Optional[str]
|
|
80
|
+
isHeadless: Optional[int]
|
|
81
|
+
isWebDriverReadOnlyMode: Optional[int]
|
|
82
|
+
cookieTypeSave: Optional[int]
|
|
83
|
+
injectJsInfo: Optional[str]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class HttpResponse(TypedDict, total=False):
|
|
87
|
+
"""HTTP 响应数据"""
|
|
88
|
+
statusCode: int
|
|
89
|
+
message: Optional[str]
|
|
90
|
+
data: Optional[Dict[str, Any]]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ============================================================================
|
|
94
|
+
# 配置相关类型
|
|
95
|
+
# ============================================================================
|
|
96
|
+
|
|
97
|
+
VersionType = Literal["v5", "v6"]
|
|
98
|
+
"""紫鸟客户端版本类型"""
|
|
99
|
+
|
|
100
|
+
PlatformType = Literal["Windows", "Darwin", "Linux"]
|
|
101
|
+
"""操作系统平台类型"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConfigDict(TypedDict, total=False):
|
|
105
|
+
"""配置字典"""
|
|
106
|
+
client_path: str # 客户端路径(必需)
|
|
107
|
+
socket_port: int # 通信端口
|
|
108
|
+
company: str # 企业名称
|
|
109
|
+
username: str # 用户名
|
|
110
|
+
password: str # 密码
|
|
111
|
+
version: VersionType # 客户端版本
|
|
112
|
+
request_timeout: int # 请求超时时间(秒)
|
|
113
|
+
max_retries: int # 最大重试次数
|
|
114
|
+
retry_delay: float # 重试延迟(秒)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ============================================================================
|
|
118
|
+
# 店铺操作选项
|
|
119
|
+
# ============================================================================
|
|
120
|
+
|
|
121
|
+
class StoreOpenOptions(TypedDict, total=False):
|
|
122
|
+
"""打开店铺的选项"""
|
|
123
|
+
isWebDriverReadOnlyMode: int # 只读模式,默认 0
|
|
124
|
+
isprivacy: int # 隐私模式,默认 0
|
|
125
|
+
isHeadless: int # 无头模式,默认 0
|
|
126
|
+
cookieTypeSave: int # Cookie 保存类型,默认 0
|
|
127
|
+
jsInfo: str # 注入的 JS 信息
|
|
128
|
+
isWaitPluginUpdate: int # 是否等待插件更新,默认 0
|
|
129
|
+
cookieTypeLoad: int # Cookie 加载类型,默认 0
|
|
130
|
+
runMode: str # 运行模式,默认 "1"
|
|
131
|
+
isLoadUserPlugin: bool # 是否加载用户插件,默认 False
|
|
132
|
+
pluginIdType: int # 插件 ID 类型,默认 1
|
|
133
|
+
privacyMode: int # 隐私模式,默认 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# 协议定义
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
class BrowserSessionProtocol(Protocol):
|
|
141
|
+
"""浏览器会话协议"""
|
|
142
|
+
|
|
143
|
+
def check_ip(self, ip_check_url: Optional[str] = None, timeout: int = 60) -> bool:
|
|
144
|
+
"""检查 IP 是否可用"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
def get_tab(self):
|
|
148
|
+
"""获取当前标签页"""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
def close(self) -> None:
|
|
152
|
+
"""关闭浏览器会话"""
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
def __enter__(self):
|
|
156
|
+
"""上下文管理器入口"""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
160
|
+
"""上下文管理器退出"""
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ============================================================================
|
|
165
|
+
# 类型别名
|
|
166
|
+
# ============================================================================
|
|
167
|
+
|
|
168
|
+
ConfigSource = Union["ZiniaoConfig", ConfigDict, str]
|
|
169
|
+
"""配置源类型:可以是配置对象、字典或文件路径"""
|
|
170
|
+
|
|
171
|
+
StoreIdentifier = Union[str, int]
|
|
172
|
+
"""店铺标识:可以是 browserOauth 字符串或 browserId 数字"""
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""工具函数模块
|
|
2
|
+
|
|
3
|
+
提供平台检测、缓存管理等通用工具函数。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .types import PlatformType
|
|
14
|
+
from .exceptions import ZiniaoError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# 平台检测
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
def get_platform() -> PlatformType:
|
|
24
|
+
"""获取当前操作系统平台
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
PlatformType: "Windows", "Darwin" (macOS), 或 "Linux"
|
|
28
|
+
"""
|
|
29
|
+
system = platform.system()
|
|
30
|
+
if system in ("Windows", "Darwin", "Linux"):
|
|
31
|
+
return system # type: ignore
|
|
32
|
+
return "Linux" # 默认返回 Linux
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_windows() -> bool:
|
|
36
|
+
"""判断是否为 Windows 平台
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
bool: Windows 返回 True
|
|
40
|
+
"""
|
|
41
|
+
return platform.system() == "Windows"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_mac() -> bool:
|
|
45
|
+
"""判断是否为 macOS 平台
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
bool: macOS 返回 True
|
|
49
|
+
"""
|
|
50
|
+
return platform.system() == "Darwin"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_linux() -> bool:
|
|
54
|
+
"""判断是否为 Linux 平台
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: Linux 返回 True
|
|
58
|
+
"""
|
|
59
|
+
return platform.system() == "Linux"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# 缓存管理
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
def get_default_cache_path() -> Optional[str]:
|
|
67
|
+
"""获取默认的缓存路径
|
|
68
|
+
|
|
69
|
+
仅适用于 Windows 平台。
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Optional[str]: 缓存路径,如果不是 Windows 返回 None
|
|
73
|
+
"""
|
|
74
|
+
if not is_windows():
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
local_appdata = os.getenv('LOCALAPPDATA')
|
|
78
|
+
if local_appdata:
|
|
79
|
+
return os.path.join(local_appdata, 'SuperBrowser')
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def delete_cache(cache_path: Optional[str] = None) -> bool:
|
|
85
|
+
"""删除缓存目录
|
|
86
|
+
|
|
87
|
+
仅适用于 Windows 平台。非必要操作,仅在店铺特别多、硬盘空间不够时使用。
|
|
88
|
+
|
|
89
|
+
警告:
|
|
90
|
+
- 当有店铺正在运行时,删除可能会失败
|
|
91
|
+
- 此操作会删除所有店铺的缓存数据
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
cache_path: 自定义缓存路径,如果为 None 则使用默认路径
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
bool: 成功删除返回 True,否则返回 False
|
|
98
|
+
"""
|
|
99
|
+
if not is_windows():
|
|
100
|
+
logger.warning("删除缓存功能仅支持 Windows 平台")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
# 确定缓存路径
|
|
104
|
+
if cache_path is None:
|
|
105
|
+
cache_path = get_default_cache_path()
|
|
106
|
+
else:
|
|
107
|
+
cache_path = os.path.join(cache_path, 'SuperBrowser')
|
|
108
|
+
|
|
109
|
+
if cache_path is None:
|
|
110
|
+
logger.error("无法确定缓存路径")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
# 检查路径是否存在
|
|
114
|
+
if not os.path.exists(cache_path):
|
|
115
|
+
logger.info(f"缓存路径不存在,无需删除:{cache_path}")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
# 删除缓存
|
|
119
|
+
try:
|
|
120
|
+
shutil.rmtree(cache_path)
|
|
121
|
+
logger.info(f"成功删除缓存:{cache_path}")
|
|
122
|
+
return True
|
|
123
|
+
except PermissionError as e:
|
|
124
|
+
logger.error(
|
|
125
|
+
f"删除缓存失败(权限不足),可能有店铺正在运行:{cache_path}, "
|
|
126
|
+
f"错误:{e}"
|
|
127
|
+
)
|
|
128
|
+
return False
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"删除缓存失败:{cache_path}, 错误:{e}")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_cache_size(cache_path: Optional[str] = None) -> int:
|
|
135
|
+
"""获取缓存目录大小(字节)
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
cache_path: 自定义缓存路径,如果为 None 则使用默认路径
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
int: 缓存大小(字节),如果路径不存在返回 0
|
|
142
|
+
"""
|
|
143
|
+
if not is_windows():
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
# 确定缓存路径
|
|
147
|
+
if cache_path is None:
|
|
148
|
+
cache_path = get_default_cache_path()
|
|
149
|
+
else:
|
|
150
|
+
cache_path = os.path.join(cache_path, 'SuperBrowser')
|
|
151
|
+
|
|
152
|
+
if cache_path is None or not os.path.exists(cache_path):
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
total_size = 0
|
|
156
|
+
try:
|
|
157
|
+
for dirpath, dirnames, filenames in os.walk(cache_path):
|
|
158
|
+
for filename in filenames:
|
|
159
|
+
filepath = os.path.join(dirpath, filename)
|
|
160
|
+
if os.path.exists(filepath):
|
|
161
|
+
total_size += os.path.getsize(filepath)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"计算缓存大小失败:{e}")
|
|
164
|
+
|
|
165
|
+
return total_size
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def format_bytes(size_bytes: int) -> str:
|
|
169
|
+
"""格式化字节大小为人类可读格式
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
size_bytes: 字节数
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: 格式化后的字符串(如 "1.5 GB")
|
|
176
|
+
"""
|
|
177
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
178
|
+
if size_bytes < 1024.0:
|
|
179
|
+
return f"{size_bytes:.2f} {unit}"
|
|
180
|
+
size_bytes /= 1024.0
|
|
181
|
+
return f"{size_bytes:.2f} PB"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ============================================================================
|
|
185
|
+
# 路径处理
|
|
186
|
+
# ============================================================================
|
|
187
|
+
|
|
188
|
+
def normalize_path(path: str) -> str:
|
|
189
|
+
"""规范化路径
|
|
190
|
+
|
|
191
|
+
将路径转换为绝对路径,并处理不同平台的路径分隔符。
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: 原始路径
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
str: 规范化后的绝对路径
|
|
198
|
+
"""
|
|
199
|
+
return os.path.abspath(os.path.expanduser(path))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ensure_dir(directory: str) -> None:
|
|
203
|
+
"""确保目录存在,不存在则创建
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
directory: 目录路径
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ZiniaoError: 创建目录失败时
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
raise ZiniaoError(f"创建目录失败:{directory}", {"error": str(e)})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ============================================================================
|
|
218
|
+
# 字符串处理
|
|
219
|
+
# ============================================================================
|
|
220
|
+
|
|
221
|
+
def fuzzy_match(text: str, pattern: str, case_sensitive: bool = False) -> bool:
|
|
222
|
+
"""模糊匹配字符串
|
|
223
|
+
|
|
224
|
+
检查 text 是否包含 pattern。
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
text: 被搜索的文本
|
|
228
|
+
pattern: 搜索模式
|
|
229
|
+
case_sensitive: 是否区分大小写,默认 False
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bool: 匹配返回 True
|
|
233
|
+
"""
|
|
234
|
+
if not case_sensitive:
|
|
235
|
+
text = text.lower()
|
|
236
|
+
pattern = pattern.lower()
|
|
237
|
+
|
|
238
|
+
return pattern in text
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def exact_match(text: str, pattern: str, case_sensitive: bool = False) -> bool:
|
|
242
|
+
"""精确匹配字符串
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
text: 被比较的文本
|
|
246
|
+
pattern: 比较模式
|
|
247
|
+
case_sensitive: 是否区分大小写,默认 False
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
bool: 完全匹配返回 True
|
|
251
|
+
"""
|
|
252
|
+
if not case_sensitive:
|
|
253
|
+
text = text.lower()
|
|
254
|
+
pattern = pattern.lower()
|
|
255
|
+
|
|
256
|
+
return text == pattern
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ============================================================================
|
|
260
|
+
# 日志配置
|
|
261
|
+
# ============================================================================
|
|
262
|
+
|
|
263
|
+
def setup_logging(
|
|
264
|
+
level: int = logging.INFO,
|
|
265
|
+
log_file: Optional[str] = None,
|
|
266
|
+
format_string: Optional[str] = None
|
|
267
|
+
) -> None:
|
|
268
|
+
"""配置日志系统
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
level: 日志级别,默认 INFO
|
|
272
|
+
log_file: 日志文件路径(可选),如果指定则同时输出到文件
|
|
273
|
+
format_string: 自定义日志格式(可选)
|
|
274
|
+
"""
|
|
275
|
+
if format_string is None:
|
|
276
|
+
format_string = (
|
|
277
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# 获取根 logger
|
|
281
|
+
root_logger = logging.getLogger("yuehua_ziniao_webdriver")
|
|
282
|
+
root_logger.setLevel(level)
|
|
283
|
+
|
|
284
|
+
# 清除现有的 handlers
|
|
285
|
+
root_logger.handlers.clear()
|
|
286
|
+
|
|
287
|
+
# 控制台 handler
|
|
288
|
+
console_handler = logging.StreamHandler()
|
|
289
|
+
console_handler.setLevel(level)
|
|
290
|
+
console_handler.setFormatter(logging.Formatter(format_string))
|
|
291
|
+
root_logger.addHandler(console_handler)
|
|
292
|
+
|
|
293
|
+
# 文件 handler(如果指定)
|
|
294
|
+
if log_file:
|
|
295
|
+
try:
|
|
296
|
+
# 确保日志目录存在
|
|
297
|
+
log_path = Path(log_file)
|
|
298
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
299
|
+
|
|
300
|
+
file_handler = logging.FileHandler(
|
|
301
|
+
log_file,
|
|
302
|
+
encoding='utf-8'
|
|
303
|
+
)
|
|
304
|
+
file_handler.setLevel(level)
|
|
305
|
+
file_handler.setFormatter(logging.Formatter(format_string))
|
|
306
|
+
root_logger.addHandler(file_handler)
|
|
307
|
+
|
|
308
|
+
logger.info(f"日志文件:{log_file}")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"创建日志文件失败:{e}")
|