wtfutil 1.2.17__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.
- wtfutil/__init__.py +268 -0
- wtfutil/_base.py +38 -0
- wtfutil/fileutil.py +366 -0
- wtfutil/httputil.py +1003 -0
- wtfutil/imgutil.py +205 -0
- wtfutil/notifyutil.py +1159 -0
- wtfutil/procutil.py +468 -0
- wtfutil/pykill.py +178 -0
- wtfutil/singleinstance.py +115 -0
- wtfutil/sqlutil.py +867 -0
- wtfutil/strutil.py +920 -0
- wtfutil/translateutil.py +42 -0
- wtfutil/util.py +141 -0
- wtfutil-1.2.17.dist-info/METADATA +172 -0
- wtfutil-1.2.17.dist-info/RECORD +19 -0
- wtfutil-1.2.17.dist-info/WHEEL +5 -0
- wtfutil-1.2.17.dist-info/entry_points.txt +2 -0
- wtfutil-1.2.17.dist-info/licenses/LICENSE +674 -0
- wtfutil-1.2.17.dist-info/top_level.txt +1 -0
wtfutil/__init__.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
wtfutil — 面向日常脚本与自动化场景的 Python 工具库。
|
|
5
|
+
|
|
6
|
+
推荐用法(按需导入,IDE 可自动补全):
|
|
7
|
+
from wtfutil import requests_session, read_text, send
|
|
8
|
+
from wtfutil.httputil import ChunkedConfig
|
|
9
|
+
from wtfutil.util import UniqueQueue, get_resource
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# ── 资源路径 ──────────────────────────────────────────────────────────────────
|
|
13
|
+
from ._base import (
|
|
14
|
+
get_resource,
|
|
15
|
+
get_resource_dir,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# ── 文件 ──────────────────────────────────────────────────────────────────────
|
|
19
|
+
from .fileutil import (
|
|
20
|
+
file_md5,
|
|
21
|
+
file_sha1,
|
|
22
|
+
file_sha256,
|
|
23
|
+
list_files,
|
|
24
|
+
list_directories,
|
|
25
|
+
touch,
|
|
26
|
+
read_text,
|
|
27
|
+
read_json,
|
|
28
|
+
read_lines,
|
|
29
|
+
write_text,
|
|
30
|
+
write_lines,
|
|
31
|
+
write_json,
|
|
32
|
+
JarAnalyzer,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ── HTTP ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
from .httputil import (
|
|
37
|
+
requests_session,
|
|
38
|
+
RequestsSession,
|
|
39
|
+
BaseUrlSession,
|
|
40
|
+
EnhancedResponse,
|
|
41
|
+
CustomSslContextHttpAdapter,
|
|
42
|
+
ChunkedConfig,
|
|
43
|
+
ChunkedAdapter,
|
|
44
|
+
DESAdapter,
|
|
45
|
+
httpraw,
|
|
46
|
+
get_redirect_target,
|
|
47
|
+
patch_redirect,
|
|
48
|
+
remove_ssl_verify,
|
|
49
|
+
patch_getproxies,
|
|
50
|
+
is_private_ip,
|
|
51
|
+
is_internal_url,
|
|
52
|
+
is_wildcard_dns,
|
|
53
|
+
is_wildcard_dns_batch,
|
|
54
|
+
is_valid_ip,
|
|
55
|
+
get_maindomain,
|
|
56
|
+
url2ip,
|
|
57
|
+
is_port_in_use,
|
|
58
|
+
get_base_url,
|
|
59
|
+
build_absolute_url,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# ── 字符串 / 加解密 ────────────────────────────────────────────────────────────
|
|
63
|
+
from .strutil import (
|
|
64
|
+
tobytes,
|
|
65
|
+
tostr,
|
|
66
|
+
tobool,
|
|
67
|
+
removesuffix,
|
|
68
|
+
removeprefix,
|
|
69
|
+
url_encode_all,
|
|
70
|
+
unicode_digit_hex_escape,
|
|
71
|
+
unicode_digit_hex_encode,
|
|
72
|
+
url_decode,
|
|
73
|
+
url_encode,
|
|
74
|
+
qp_encode_all,
|
|
75
|
+
uuencode,
|
|
76
|
+
base64decode,
|
|
77
|
+
base64encode,
|
|
78
|
+
base64_urlencode,
|
|
79
|
+
base64_urldecode,
|
|
80
|
+
urlsafe_base64encode,
|
|
81
|
+
urlsafe_base64decode,
|
|
82
|
+
base64pickle,
|
|
83
|
+
base64unpickle,
|
|
84
|
+
rsa_encrypt,
|
|
85
|
+
rsa_decrypt,
|
|
86
|
+
des_encrypt,
|
|
87
|
+
des_decrypt,
|
|
88
|
+
str_md5,
|
|
89
|
+
str_sha1,
|
|
90
|
+
str_sha256,
|
|
91
|
+
get_middle_text,
|
|
92
|
+
splitlines,
|
|
93
|
+
rand_base,
|
|
94
|
+
rand_case,
|
|
95
|
+
match1,
|
|
96
|
+
string_to_bash_variable,
|
|
97
|
+
normalize_spaces,
|
|
98
|
+
extract_dict,
|
|
99
|
+
format_bytes,
|
|
100
|
+
align_text,
|
|
101
|
+
utf8_overlong_encoding,
|
|
102
|
+
utf7_encode,
|
|
103
|
+
ghost_bits_byte,
|
|
104
|
+
ghost_bits_encode,
|
|
105
|
+
ghost_bits_decode_to_bytes,
|
|
106
|
+
ghost_bits_decode,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ── 数据库 ────────────────────────────────────────────────────────────────────
|
|
110
|
+
from .sqlutil import (
|
|
111
|
+
Dict,
|
|
112
|
+
Database,
|
|
113
|
+
SQLite,
|
|
114
|
+
MYSQL,
|
|
115
|
+
ScriptRunner,
|
|
116
|
+
next_id,
|
|
117
|
+
join_field_value,
|
|
118
|
+
join_field,
|
|
119
|
+
join_value,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# ── 进程(Windows)────────────────────────────────────────────────────────────
|
|
123
|
+
from .procutil import (
|
|
124
|
+
find_process_by_name,
|
|
125
|
+
suspend_process,
|
|
126
|
+
suspend_process_by_pid,
|
|
127
|
+
resume_process,
|
|
128
|
+
resume_process_by_pid,
|
|
129
|
+
find_python_process_by_script,
|
|
130
|
+
find_python_processes_by_script,
|
|
131
|
+
find_python_process_details_by_script,
|
|
132
|
+
kill_python_processes_by_script,
|
|
133
|
+
find_python_processes_by_cmdline,
|
|
134
|
+
find_python_process_details_by_cmdline,
|
|
135
|
+
kill_python_processes_by_cmdline,
|
|
136
|
+
list_all_python_process_details,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# ── 通知 ──────────────────────────────────────────────────────────────────────
|
|
140
|
+
from .notifyutil import (
|
|
141
|
+
push_config,
|
|
142
|
+
send,
|
|
143
|
+
one,
|
|
144
|
+
bark,
|
|
145
|
+
console,
|
|
146
|
+
dingding_bot,
|
|
147
|
+
feishu_bot,
|
|
148
|
+
feishu_text,
|
|
149
|
+
feishu_richtext,
|
|
150
|
+
go_cqhttp,
|
|
151
|
+
gotify,
|
|
152
|
+
iGot,
|
|
153
|
+
serverJ,
|
|
154
|
+
pushdeer,
|
|
155
|
+
chat,
|
|
156
|
+
pushplus_bot,
|
|
157
|
+
qmsg_bot,
|
|
158
|
+
wecom_app,
|
|
159
|
+
WeCom,
|
|
160
|
+
wecom_bot,
|
|
161
|
+
telegram_bot,
|
|
162
|
+
aibotk,
|
|
163
|
+
smtp,
|
|
164
|
+
pushme,
|
|
165
|
+
pipehub,
|
|
166
|
+
xtuis,
|
|
167
|
+
aiops_phone,
|
|
168
|
+
showdoc,
|
|
169
|
+
notifyx,
|
|
170
|
+
chronocat,
|
|
171
|
+
custom_notify,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# ── 翻译 ──────────────────────────────────────────────────────────────────────
|
|
175
|
+
from .translateutil import (
|
|
176
|
+
BaiduTranslateApi,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# ── 图片 ──────────────────────────────────────────────────────────────────────
|
|
180
|
+
from .imgutil import (
|
|
181
|
+
img_config,
|
|
182
|
+
ImageFetchError,
|
|
183
|
+
fetch_random_bytes,
|
|
184
|
+
random_avatar_bytes,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ── 单实例 ────────────────────────────────────────────────────────────────────
|
|
188
|
+
from .singleinstance import (
|
|
189
|
+
SingleInstance,
|
|
190
|
+
SingleInstanceException,
|
|
191
|
+
single_instance,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# ── 杂项工具 ──────────────────────────────────────────────────────────────────
|
|
195
|
+
from .util import (
|
|
196
|
+
UniqueQueue,
|
|
197
|
+
measure_time,
|
|
198
|
+
unique_items,
|
|
199
|
+
current_datetime,
|
|
200
|
+
format_datetime,
|
|
201
|
+
parse_datetime,
|
|
202
|
+
cut_list,
|
|
203
|
+
group_data,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
__all__ = [
|
|
207
|
+
# _base
|
|
208
|
+
'get_resource', 'get_resource_dir',
|
|
209
|
+
# fileutil
|
|
210
|
+
'file_md5', 'file_sha1', 'file_sha256',
|
|
211
|
+
'list_files', 'list_directories', 'touch',
|
|
212
|
+
'read_text', 'read_json', 'read_lines',
|
|
213
|
+
'write_text', 'write_lines', 'write_json',
|
|
214
|
+
'JarAnalyzer',
|
|
215
|
+
# httputil
|
|
216
|
+
'requests_session', 'RequestsSession', 'BaseUrlSession',
|
|
217
|
+
'EnhancedResponse', 'CustomSslContextHttpAdapter',
|
|
218
|
+
'ChunkedConfig', 'ChunkedAdapter', 'DESAdapter',
|
|
219
|
+
'httpraw',
|
|
220
|
+
'get_redirect_target', 'patch_redirect', 'remove_ssl_verify', 'patch_getproxies',
|
|
221
|
+
'is_private_ip', 'is_internal_url', 'is_wildcard_dns', 'is_wildcard_dns_batch',
|
|
222
|
+
'is_valid_ip', 'get_maindomain', 'url2ip', 'is_port_in_use',
|
|
223
|
+
'get_base_url', 'build_absolute_url',
|
|
224
|
+
# strutil
|
|
225
|
+
'tobytes', 'tostr', 'tobool',
|
|
226
|
+
'removesuffix', 'removeprefix',
|
|
227
|
+
'url_encode_all', 'unicode_digit_hex_escape', 'unicode_digit_hex_encode',
|
|
228
|
+
'url_decode', 'url_encode', 'qp_encode_all', 'uuencode',
|
|
229
|
+
'base64decode', 'base64encode', 'base64_urlencode', 'base64_urldecode',
|
|
230
|
+
'urlsafe_base64encode', 'urlsafe_base64decode', 'base64pickle', 'base64unpickle',
|
|
231
|
+
'rsa_encrypt', 'rsa_decrypt', 'des_encrypt', 'des_decrypt',
|
|
232
|
+
'str_md5', 'str_sha1', 'str_sha256',
|
|
233
|
+
'get_middle_text', 'splitlines', 'rand_base', 'rand_case',
|
|
234
|
+
'match1', 'string_to_bash_variable', 'normalize_spaces',
|
|
235
|
+
'extract_dict', 'format_bytes', 'align_text',
|
|
236
|
+
'utf8_overlong_encoding', 'utf7_encode',
|
|
237
|
+
'ghost_bits_byte', 'ghost_bits_encode', 'ghost_bits_decode_to_bytes', 'ghost_bits_decode',
|
|
238
|
+
# sqlutil
|
|
239
|
+
'Dict', 'Database', 'SQLite', 'MYSQL', 'ScriptRunner',
|
|
240
|
+
'next_id', 'join_field_value', 'join_field', 'join_value',
|
|
241
|
+
# procutil
|
|
242
|
+
'find_process_by_name',
|
|
243
|
+
'suspend_process', 'suspend_process_by_pid',
|
|
244
|
+
'resume_process', 'resume_process_by_pid',
|
|
245
|
+
'find_python_process_by_script', 'find_python_processes_by_script',
|
|
246
|
+
'find_python_process_details_by_script', 'kill_python_processes_by_script',
|
|
247
|
+
'find_python_processes_by_cmdline', 'find_python_process_details_by_cmdline',
|
|
248
|
+
'kill_python_processes_by_cmdline', 'list_all_python_process_details',
|
|
249
|
+
# notifyutil
|
|
250
|
+
'push_config', 'send', 'one',
|
|
251
|
+
'bark', 'console', 'dingding_bot',
|
|
252
|
+
'feishu_bot', 'feishu_text', 'feishu_richtext',
|
|
253
|
+
'go_cqhttp', 'gotify', 'iGot', 'serverJ',
|
|
254
|
+
'pushdeer', 'chat', 'pushplus_bot', 'qmsg_bot',
|
|
255
|
+
'wecom_app', 'WeCom', 'wecom_bot', 'telegram_bot',
|
|
256
|
+
'aibotk', 'smtp', 'pushme', 'pipehub', 'xtuis',
|
|
257
|
+
'aiops_phone', 'showdoc', 'notifyx', 'chronocat', 'custom_notify',
|
|
258
|
+
# translateutil
|
|
259
|
+
'BaiduTranslateApi',
|
|
260
|
+
# imgutil
|
|
261
|
+
'img_config', 'ImageFetchError', 'fetch_random_bytes', 'random_avatar_bytes',
|
|
262
|
+
# singleinstance
|
|
263
|
+
'SingleInstance', 'SingleInstanceException', 'single_instance',
|
|
264
|
+
# util (misc)
|
|
265
|
+
'UniqueQueue', 'measure_time', 'unique_items',
|
|
266
|
+
'current_datetime', 'format_datetime', 'parse_datetime',
|
|
267
|
+
'cut_list', 'group_data',
|
|
268
|
+
]
|
wtfutil/_base.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
基础工具:仅依赖标准库,供各子模块安全导入,不引用任何 wtfutil 内部模块。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
__all__ = ['get_resource_dir', 'get_resource']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_resource_dir(basedir: str | None = None) -> str:
|
|
13
|
+
"""向上遍历目录树,找到第一个包含 resource 子目录的路径。"""
|
|
14
|
+
if not basedir:
|
|
15
|
+
basedir = sys._getframe(1).f_code.co_filename
|
|
16
|
+
current_dir = getattr(sys, '_MEIPASS', os.path.dirname(basedir))
|
|
17
|
+
|
|
18
|
+
while True:
|
|
19
|
+
resource_folder = os.path.join(current_dir, "resource")
|
|
20
|
+
if os.path.exists(resource_folder) and os.path.isdir(resource_folder):
|
|
21
|
+
break
|
|
22
|
+
if len(current_dir) <= 3:
|
|
23
|
+
break
|
|
24
|
+
current_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
|
|
25
|
+
|
|
26
|
+
return resource_folder
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_resource(filename: str) -> str | None:
|
|
30
|
+
"""按优先级查找资源文件:当前路径 → resource 子目录 → 用户家目录。"""
|
|
31
|
+
if Path(filename).exists():
|
|
32
|
+
return filename
|
|
33
|
+
resource_path = get_resource_dir(sys._getframe(1).f_code.co_filename) + "/" + filename
|
|
34
|
+
if Path(resource_path).exists():
|
|
35
|
+
return str(Path(resource_path).absolute())
|
|
36
|
+
if Path('~/' + filename).expanduser().exists():
|
|
37
|
+
return str(Path('~/' + filename).expanduser().absolute())
|
|
38
|
+
return None
|
wtfutil/fileutil.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import subprocess
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import TemporaryDirectory
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def file_md5(file_path: str | Path) -> str:
|
|
14
|
+
md5lib = hashlib.md5()
|
|
15
|
+
with open(str(file_path), 'rb') as f:
|
|
16
|
+
md5lib.update(f.read())
|
|
17
|
+
return md5lib.hexdigest()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def file_sha1(file_path: str | Path) -> str:
|
|
21
|
+
sha1 = hashlib.sha1()
|
|
22
|
+
with open(str(file_path), 'rb') as f:
|
|
23
|
+
sha1.update(f.read())
|
|
24
|
+
return sha1.hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def file_sha256(file_path: str | Path) -> str:
|
|
28
|
+
sha1 = hashlib.sha256()
|
|
29
|
+
with open(str(file_path), 'rb') as f:
|
|
30
|
+
sha1.update(f.read())
|
|
31
|
+
return sha1.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def list_files(directory: str | Path) -> list[str]:
|
|
35
|
+
"""List all files in a directory."""
|
|
36
|
+
directory_str = str(directory)
|
|
37
|
+
return [
|
|
38
|
+
os.path.join(directory_str, f)
|
|
39
|
+
for f in os.listdir(directory_str)
|
|
40
|
+
if os.path.isfile(os.path.join(directory_str, f))
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def list_directories(directory: str | Path) -> list[str]:
|
|
45
|
+
"""List all directories in a directory."""
|
|
46
|
+
directory_str = str(directory)
|
|
47
|
+
return [
|
|
48
|
+
os.path.join(directory_str, d)
|
|
49
|
+
for d in os.listdir(directory_str)
|
|
50
|
+
if os.path.isdir(os.path.join(directory_str, d))
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def touch(filepath: str | Path, mode: int = 0o666, exist_ok: bool = True) -> None:
|
|
55
|
+
Path(filepath).touch(mode=mode, exist_ok=exist_ok)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_text(filepath: Union[Path, str], mode='r', encoding='utf-8', not_exists_ok: bool = False, errors=None) -> str:
|
|
59
|
+
"""
|
|
60
|
+
errors-->
|
|
61
|
+
'ignore':忽略无法解码的字符。直接跳过无法处理的字符,继续解码其他部分。
|
|
62
|
+
'replace':使用特定字符替代无法解码的字符,默认使用 '�' 代替。例如,b'\xe4\xb8\x96\xe7\x95\x8c'.decode('utf-8', errors='replace') 输出 '世界�'。
|
|
63
|
+
'strict':默认行为,如果遇到无法解码的字符,抛出 UnicodeDecodeError 异常。
|
|
64
|
+
'backslashreplace':使用 Unicode 转义序列替代无法解码的字符。例如,b'\xe4\xb8\x96\xe7\x95\x8c'.decode('ascii', errors='backslashreplace') 输出 '\\xe4\\xb8\\x96\\xe7\\x95\\x8c'。
|
|
65
|
+
'xmlcharrefreplace':使用 XML 实体替代无法解码的字符。例如,b'\xe4\xb8\x96\xe7\x95\x8c'.decode('ascii', errors='xmlcharrefreplace') 输出 '世界'。
|
|
66
|
+
'surrogateescape':将无法解码的字节转换为 Unicode 符号 '�' 的转义码。例如,当解码 Latin-1 字符串时,b'\xe9'.decode('latin-1', errors='surrogateescape') 输出 '\udce9'。
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(filepath, Path):
|
|
69
|
+
filepath = str(filepath)
|
|
70
|
+
if mode == 'rb':
|
|
71
|
+
encoding = None
|
|
72
|
+
if not_exists_ok and not Path(filepath).is_file():
|
|
73
|
+
return ''
|
|
74
|
+
with open(filepath, mode, encoding=encoding, errors=errors) as f:
|
|
75
|
+
content = f.read()
|
|
76
|
+
return content
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_json(filepath: Union[Path, str], encoding='utf-8', not_exists_ok: bool = False) -> dict:
|
|
80
|
+
if isinstance(filepath, Path):
|
|
81
|
+
filepath = str(filepath)
|
|
82
|
+
if not_exists_ok and not Path(filepath).is_file():
|
|
83
|
+
return {}
|
|
84
|
+
with open(filepath, 'r', encoding=encoding) as f:
|
|
85
|
+
return json.load(f)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_lines(filepath: Union[Path, str], encoding='utf-8', not_exists_ok: bool = False, unique: bool = False) -> list:
|
|
89
|
+
if isinstance(filepath, Path):
|
|
90
|
+
filepath = str(filepath)
|
|
91
|
+
lines = []
|
|
92
|
+
if not_exists_ok and not Path(filepath).is_file():
|
|
93
|
+
return lines
|
|
94
|
+
with open(filepath, 'r', encoding=encoding) as f:
|
|
95
|
+
# lines = f.readlines()
|
|
96
|
+
# lines = [line.rstrip() for line in lines] 只会创建一个生成器 不会有性能问题
|
|
97
|
+
for line in f:
|
|
98
|
+
line = line.rstrip()
|
|
99
|
+
if line:
|
|
100
|
+
if unique and line in lines:
|
|
101
|
+
# 去重
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
lines.append(line)
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def write_text(filepath: Union[Path, str], content, mode='w', encoding='utf-8', newline=''):
|
|
109
|
+
"""
|
|
110
|
+
写入文本内容到文件
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
filepath: 文件路径
|
|
114
|
+
content: 要写入的内容
|
|
115
|
+
mode: 文件打开模式,默认 'w'
|
|
116
|
+
encoding: 文件编码,默认 'utf-8'
|
|
117
|
+
newline: 换行符处理方式,默认 ''(不自动转换)
|
|
118
|
+
- '': 不进行换行符转换(推荐,保持原始内容)
|
|
119
|
+
- None: 启用平台相关的换行符转换(Windows: \n→\r\n)
|
|
120
|
+
- '\n': 强制使用 LF(Unix/Linux 风格)
|
|
121
|
+
- '\r\n': 强制使用 CRLF(Windows 风格)
|
|
122
|
+
- '\r': 强制使用 CR(旧 Mac 风格)
|
|
123
|
+
|
|
124
|
+
注意:
|
|
125
|
+
- 默认 newline='' 不会自动转换换行符,保持内容原样
|
|
126
|
+
- 如需平台相关的换行符转换,使用 newline=None
|
|
127
|
+
- 二进制模式 ('wb') 下 newline 参数无效
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(filepath, Path):
|
|
130
|
+
filepath = str(filepath)
|
|
131
|
+
if mode == 'wb':
|
|
132
|
+
encoding = None
|
|
133
|
+
newline = None # 二进制模式下 newline 无效
|
|
134
|
+
if content is None:
|
|
135
|
+
raise ValueError('content must not be None')
|
|
136
|
+
with open(filepath, mode, encoding=encoding, newline=newline) as f:
|
|
137
|
+
f.write(content)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def write_lines(filepath: Union[Path, str], lines, mode='w', encoding='utf-8', unique: bool = False, newline=''):
|
|
141
|
+
"""
|
|
142
|
+
按行写入文本内容到文件
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
filepath: 文件路径
|
|
146
|
+
lines: 要写入的行列表
|
|
147
|
+
mode: 文件打开模式,默认 'w'
|
|
148
|
+
encoding: 文件编码,默认 'utf-8'
|
|
149
|
+
unique: 是否去重,默认 False
|
|
150
|
+
newline: 换行符处理方式,默认 ''(不自动转换)
|
|
151
|
+
- '': 不进行换行符转换(推荐,保持原始内容)
|
|
152
|
+
- None: 启用平台相关的换行符转换(Windows: \n→\r\n)
|
|
153
|
+
- '\n': 强制使用 LF(Unix/Linux 风格)
|
|
154
|
+
- '\r\n': 强制使用 CRLF(Windows 风格)
|
|
155
|
+
- '\r': 强制使用 CR(旧 Mac 风格)
|
|
156
|
+
|
|
157
|
+
注意:
|
|
158
|
+
- 默认 newline='' 不会自动转换换行符,每行末尾添加 '\n'
|
|
159
|
+
- 如需平台相关的换行符转换,使用 newline=None
|
|
160
|
+
- 函数会在每行末尾添加 '\n',实际写入的换行符取决于 newline 参数
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(filepath, Path):
|
|
163
|
+
filepath = str(filepath)
|
|
164
|
+
if lines is None:
|
|
165
|
+
raise ValueError('lines must not be None')
|
|
166
|
+
if unique:
|
|
167
|
+
# 去重并且保持原本顺序
|
|
168
|
+
lines = list(dict.fromkeys(lines))
|
|
169
|
+
|
|
170
|
+
with open(filepath, mode, encoding=encoding, newline=newline) as f:
|
|
171
|
+
for line in lines:
|
|
172
|
+
f.write(line + '\n')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def write_json(filepath: Union[Path, str], json_obj: dict, encoding='utf-8'):
|
|
176
|
+
if isinstance(filepath, Path):
|
|
177
|
+
filepath = str(filepath)
|
|
178
|
+
if json_obj is None:
|
|
179
|
+
raise ValueError('json_obj must not be None')
|
|
180
|
+
with open(filepath, 'w', encoding=encoding) as f:
|
|
181
|
+
json.dump(json_obj, f, indent=4, ensure_ascii=False)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class JarAnalyzer:
|
|
185
|
+
# JDK 版本映射表,仅用于映射数字版本
|
|
186
|
+
JAVA_VERSION_MAP = {
|
|
187
|
+
45: 1, 46: 2, 47: 3, 48: 4, 49: 5,
|
|
188
|
+
50: 6, 51: 7, 52: 8, 53: 9, 54: 10,
|
|
189
|
+
55: 11, 56: 12, 57: 13, 58: 14, 59: 15,
|
|
190
|
+
60: 16, 61: 17, 62: 18, 63: 19, 64: 20,
|
|
191
|
+
65: 21
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
SPRING_BOOT_INDICATORS = ["org.springframework.", "Spring-Boot", "BOOT-INF/"]
|
|
195
|
+
GUI_INDICATORS = ["java/awt/", "javax/swing/", "javafx/application/"]
|
|
196
|
+
|
|
197
|
+
def __init__(self, jar_path: str):
|
|
198
|
+
self.jar_path = Path(jar_path).resolve()
|
|
199
|
+
self.jar_file = self.jar_path.name
|
|
200
|
+
self.jdk_version = 0 # 默认值为 0,表示未知
|
|
201
|
+
self.is_spring_boot = False
|
|
202
|
+
self.recommended_executable = "java"
|
|
203
|
+
self.main_class = None # 新增:保存 Main-Class
|
|
204
|
+
self._manifest_content = None
|
|
205
|
+
self._is_executable_jar = False # 新增:是否是可执行 JAR
|
|
206
|
+
|
|
207
|
+
# 检查文件是否存在,不存在则抛出异常
|
|
208
|
+
if not self.jar_path.exists():
|
|
209
|
+
raise FileNotFoundError(f"JAR 文件不存在: {self.jar_path}")
|
|
210
|
+
if not self.jar_path.is_file() or not self.jar_path.suffix == ".jar":
|
|
211
|
+
raise ValueError(f"路径不是有效的 JAR 文件: {self.jar_path}")
|
|
212
|
+
|
|
213
|
+
# 初始化时直接进行分析
|
|
214
|
+
self._get_java_version()
|
|
215
|
+
self._check_spring_boot()
|
|
216
|
+
self._check_gui_application()
|
|
217
|
+
|
|
218
|
+
def _read_manifest(self) -> None:
|
|
219
|
+
"""读取并缓存 META-INF/MANIFEST.MF 的内容,检查是否是可执行 JAR"""
|
|
220
|
+
if self._manifest_content is None:
|
|
221
|
+
try:
|
|
222
|
+
with zipfile.ZipFile(self.jar_path, 'r') as jar:
|
|
223
|
+
with jar.open("META-INF/MANIFEST.MF") as manifest:
|
|
224
|
+
self._manifest_content = manifest.read().decode("utf-8")
|
|
225
|
+
# 检查是否有 Main-Class 或 Start-Class(Spring Boot)
|
|
226
|
+
lines = self._manifest_content.splitlines()
|
|
227
|
+
for line in lines:
|
|
228
|
+
if line.startswith("Main-Class:") or line.startswith("Start-Class:"):
|
|
229
|
+
self._is_executable_jar = True
|
|
230
|
+
break
|
|
231
|
+
except Exception:
|
|
232
|
+
self._manifest_content = ""
|
|
233
|
+
self._is_executable_jar = False
|
|
234
|
+
|
|
235
|
+
def _get_main_class(self) -> Optional[str]:
|
|
236
|
+
"""获取 JAR 文件中的 Main-Class,支持 Spring Boot 的 Start-Class"""
|
|
237
|
+
self._read_manifest()
|
|
238
|
+
if self._manifest_content:
|
|
239
|
+
lines = self._manifest_content.splitlines()
|
|
240
|
+
# 优先检查 Spring Boot 的 Start-Class
|
|
241
|
+
for line in lines:
|
|
242
|
+
if line.startswith("Start-Class:"): # Spring Boot 的实际启动类
|
|
243
|
+
self.main_class = line.split(":", 1)[1].strip()
|
|
244
|
+
return self.main_class
|
|
245
|
+
# 再检查标准的 Main-Class
|
|
246
|
+
for line in lines:
|
|
247
|
+
if line.startswith("Main-Class:"):
|
|
248
|
+
self.main_class = line.split(":", 1)[1].strip()
|
|
249
|
+
return self.main_class
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def _get_java_version(self) -> None:
|
|
253
|
+
"""解析 JAR 文件中的 .class 文件,获取 JDK 版本,优先使用 Main-Class,并检查版本一致性"""
|
|
254
|
+
try:
|
|
255
|
+
with zipfile.ZipFile(self.jar_path, 'r') as jar:
|
|
256
|
+
class_files = [f for f in jar.namelist() if f.endswith(".class")]
|
|
257
|
+
if not class_files:
|
|
258
|
+
self.jdk_version = 0
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# 优先检查 Main-Class
|
|
262
|
+
main_class = self._get_main_class()
|
|
263
|
+
if main_class:
|
|
264
|
+
main_class_path = main_class.replace('.', '/') + ".class"
|
|
265
|
+
# Spring Boot 的路径
|
|
266
|
+
spring_boot_main_class_path = f"BOOT-INF/classes/{main_class_path}"
|
|
267
|
+
|
|
268
|
+
# 先检查 Spring Boot 路径
|
|
269
|
+
if spring_boot_main_class_path in class_files:
|
|
270
|
+
with jar.open(spring_boot_main_class_path) as class_file:
|
|
271
|
+
class_file.read(4) # 跳过魔数
|
|
272
|
+
_, major_version = struct.unpack(">HH", class_file.read(4))
|
|
273
|
+
self.jdk_version = self.JAVA_VERSION_MAP.get(major_version, 0)
|
|
274
|
+
return
|
|
275
|
+
# 再检查标准路径
|
|
276
|
+
elif main_class_path in class_files:
|
|
277
|
+
with jar.open(main_class_path) as class_file:
|
|
278
|
+
class_file.read(4) # 跳过魔数
|
|
279
|
+
_, major_version = struct.unpack(">HH", class_file.read(4))
|
|
280
|
+
self.jdk_version = self.JAVA_VERSION_MAP.get(major_version, 0)
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# 检查第一个 .class 文件
|
|
284
|
+
versions = set()
|
|
285
|
+
for file in class_files[:5]: # 限制检查前 5 个,避免性能问题
|
|
286
|
+
with jar.open(file) as class_file:
|
|
287
|
+
class_file.read(4) # 跳过魔数
|
|
288
|
+
_, major_version = struct.unpack(">HH", class_file.read(4))
|
|
289
|
+
versions.add(self.JAVA_VERSION_MAP.get(major_version, 0))
|
|
290
|
+
if len(versions) > 1:
|
|
291
|
+
self.jdk_version = max(versions) # 使用最高版本
|
|
292
|
+
return
|
|
293
|
+
self.jdk_version = versions.pop() if versions else 0
|
|
294
|
+
except Exception:
|
|
295
|
+
self.jdk_version = 0
|
|
296
|
+
|
|
297
|
+
def _check_spring_boot(self) -> None:
|
|
298
|
+
"""增强 Spring Boot 判断,结合 MANIFEST.MF 和文件结构"""
|
|
299
|
+
self._read_manifest()
|
|
300
|
+
if self._manifest_content:
|
|
301
|
+
# 检查 MANIFEST.MF 中的 Spring Boot 标志
|
|
302
|
+
self.is_spring_boot = any(
|
|
303
|
+
indicator in self._manifest_content
|
|
304
|
+
for indicator in self.SPRING_BOOT_INDICATORS
|
|
305
|
+
)
|
|
306
|
+
if not self.is_spring_boot:
|
|
307
|
+
# 检查文件结构中是否有 BOOT-INF/ 目录
|
|
308
|
+
try:
|
|
309
|
+
with zipfile.ZipFile(self.jar_path, 'r') as jar:
|
|
310
|
+
self.is_spring_boot = any(
|
|
311
|
+
f.startswith("BOOT-INF/") for f in jar.namelist()
|
|
312
|
+
)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
def _check_gui_application(self) -> None:
|
|
317
|
+
"""增强 GUI 判断,检查依赖和 Main-Class"""
|
|
318
|
+
if self.is_spring_boot:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
main_class = self._get_main_class()
|
|
322
|
+
if not main_class:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
with TemporaryDirectory() as temp_dir:
|
|
327
|
+
output = subprocess.run(
|
|
328
|
+
["javap", "-classpath", str(self.jar_path), "-s", main_class],
|
|
329
|
+
capture_output=True, text=True, check=True, cwd=temp_dir
|
|
330
|
+
)
|
|
331
|
+
stdout = output.stdout
|
|
332
|
+
if any(indicator in stdout for indicator in self.GUI_INDICATORS):
|
|
333
|
+
self.recommended_executable = "javaw"
|
|
334
|
+
# 额外检查是否有 CLI 相关标志
|
|
335
|
+
elif "main([Ljava/lang/String;)V" in stdout and "java/io/Console" not in stdout:
|
|
336
|
+
self.recommended_executable = "java"
|
|
337
|
+
except subprocess.CalledProcessError:
|
|
338
|
+
# 如果 javap 失败,尝试检查 JAR 中的依赖
|
|
339
|
+
try:
|
|
340
|
+
with zipfile.ZipFile(self.jar_path, 'r') as jar:
|
|
341
|
+
for file in jar.namelist():
|
|
342
|
+
if file.endswith(".class"):
|
|
343
|
+
with jar.open(file) as class_file:
|
|
344
|
+
content = class_file.read().decode("utf-8", errors="ignore")
|
|
345
|
+
if any(indicator in content for indicator in self.GUI_INDICATORS):
|
|
346
|
+
self.recommended_executable = "javaw"
|
|
347
|
+
return
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = [
|
|
353
|
+
'file_md5',
|
|
354
|
+
'file_sha1',
|
|
355
|
+
'file_sha256',
|
|
356
|
+
'list_files',
|
|
357
|
+
'list_directories',
|
|
358
|
+
'touch',
|
|
359
|
+
'read_text',
|
|
360
|
+
'read_json',
|
|
361
|
+
'read_lines',
|
|
362
|
+
'write_text',
|
|
363
|
+
'write_lines',
|
|
364
|
+
'write_json',
|
|
365
|
+
'JarAnalyzer'
|
|
366
|
+
]
|