hutool-python 1.0.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.
- hutool/__init__.py +174 -0
- hutool/cache/__init__.py +7 -0
- hutool/cache/cache_util.py +47 -0
- hutool/cache/fifo_cache.py +87 -0
- hutool/cache/lfu_cache.py +129 -0
- hutool/cache/lru_cache.py +93 -0
- hutool/cache/timed_cache.py +115 -0
- hutool/captcha/__init__.py +3 -0
- hutool/captcha/captcha_util.py +215 -0
- hutool/core/__init__.py +23 -0
- hutool/core/_base.py +61 -0
- hutool/core/bean.py +214 -0
- hutool/core/codec.py +111 -0
- hutool/core/coll.py +635 -0
- hutool/core/date.py +1024 -0
- hutool/core/exceptions.py +66 -0
- hutool/core/io/__init__.py +0 -0
- hutool/core/io/data_size_util.py +79 -0
- hutool/core/io/file_name_util.py +111 -0
- hutool/core/io/file_util.py +650 -0
- hutool/core/io/io_util.py +133 -0
- hutool/core/io/path_util.py +247 -0
- hutool/core/io/resource_util.py +137 -0
- hutool/core/map.py +933 -0
- hutool/core/math_util.py +105 -0
- hutool/core/net.py +288 -0
- hutool/core/text/__init__.py +0 -0
- hutool/core/text/csv_util.py +54 -0
- hutool/core/text/str_builder.py +224 -0
- hutool/core/text/unicode_util.py +58 -0
- hutool/core/tree.py +242 -0
- hutool/core/util/__init__.py +63 -0
- hutool/core/util/array_util.py +503 -0
- hutool/core/util/boolean_util.py +124 -0
- hutool/core/util/charset_util.py +60 -0
- hutool/core/util/class_util.py +136 -0
- hutool/core/util/coordinate_util.py +186 -0
- hutool/core/util/credit_code_util.py +110 -0
- hutool/core/util/desensitized_util.py +194 -0
- hutool/core/util/enum_util.py +94 -0
- hutool/core/util/escape_util.py +97 -0
- hutool/core/util/hash_util.py +243 -0
- hutool/core/util/hex_util.py +140 -0
- hutool/core/util/id_util.py +147 -0
- hutool/core/util/idcard_util.py +300 -0
- hutool/core/util/number_util.py +720 -0
- hutool/core/util/object_util.py +294 -0
- hutool/core/util/page_util.py +61 -0
- hutool/core/util/phone_util.py +140 -0
- hutool/core/util/random_util.py +112 -0
- hutool/core/util/re_util.py +231 -0
- hutool/core/util/reflect_util.py +135 -0
- hutool/core/util/runtime_util.py +89 -0
- hutool/core/util/str_util.py +2320 -0
- hutool/core/util/system_util.py +62 -0
- hutool/core/util/url_util.py +232 -0
- hutool/core/util/version_util.py +41 -0
- hutool/core/util/xml_util.py +158 -0
- hutool/core/util/zip_util.py +126 -0
- hutool/cron/__init__.py +4 -0
- hutool/cron/cron_pattern.py +123 -0
- hutool/cron/cron_util.py +115 -0
- hutool/crypto/__init__.py +5 -0
- hutool/crypto/digest_util.py +167 -0
- hutool/crypto/secure_util.py +311 -0
- hutool/crypto/sign_util.py +74 -0
- hutool/dfa/__init__.py +3 -0
- hutool/dfa/sensitive_util.py +114 -0
- hutool/extra/__init__.py +6 -0
- hutool/extra/emoji_util.py +90 -0
- hutool/extra/pinyin_util.py +44 -0
- hutool/extra/qr_code_util.py +58 -0
- hutool/extra/template_util.py +41 -0
- hutool/http/__init__.py +6 -0
- hutool/http/html_util.py +88 -0
- hutool/http/http_request.py +188 -0
- hutool/http/http_response.py +139 -0
- hutool/http/http_util.py +237 -0
- hutool/json_util.py +251 -0
- hutool/jwt_util.py +57 -0
- hutool/setting/__init__.py +5 -0
- hutool/setting/props_util.py +79 -0
- hutool/setting/setting_util.py +80 -0
- hutool/setting/yaml_util.py +45 -0
- hutool_python-1.0.0.dist-info/LICENSE +127 -0
- hutool_python-1.0.0.dist-info/METADATA +438 -0
- hutool_python-1.0.0.dist-info/RECORD +89 -0
- hutool_python-1.0.0.dist-info/WHEEL +5 -0
- hutool_python-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""IO工具模块"""
|
|
2
|
+
|
|
3
|
+
from typing import IO, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IoUtil:
|
|
7
|
+
"""IO工具类"""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def read(stream: IO, charset: str = "utf-8") -> str:
|
|
11
|
+
"""从流中读取字符串
|
|
12
|
+
|
|
13
|
+
:param stream: 输入流(需支持 read 方法)
|
|
14
|
+
:param charset: 字符编码,默认 utf-8
|
|
15
|
+
:return: 读取到的字符串内容
|
|
16
|
+
"""
|
|
17
|
+
raw = stream.read()
|
|
18
|
+
if isinstance(raw, bytes):
|
|
19
|
+
return raw.decode(charset)
|
|
20
|
+
return raw
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def read_bytes(stream: IO) -> bytes:
|
|
24
|
+
"""从流中读取字节数组
|
|
25
|
+
|
|
26
|
+
:param stream: 输入流(需支持 read 方法)
|
|
27
|
+
:return: 读取到的字节数组
|
|
28
|
+
"""
|
|
29
|
+
data = stream.read()
|
|
30
|
+
if isinstance(data, str):
|
|
31
|
+
return data.encode("utf-8")
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def read_lines(stream: IO, charset: str = "utf-8") -> List[str]:
|
|
36
|
+
"""从流中读取行列表
|
|
37
|
+
|
|
38
|
+
:param stream: 输入流(需支持 read 或 readlines 方法)
|
|
39
|
+
:param charset: 字符编码,默认 utf-8
|
|
40
|
+
:return: 行列表
|
|
41
|
+
"""
|
|
42
|
+
raw = stream.read()
|
|
43
|
+
if isinstance(raw, bytes):
|
|
44
|
+
raw = raw.decode(charset)
|
|
45
|
+
return raw.splitlines(keepends=False)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def write(stream: IO, data: Union[str, bytes], charset: str = "utf-8") -> int:
|
|
49
|
+
"""写入数据到流
|
|
50
|
+
|
|
51
|
+
:param stream: 输出流(需支持 write 方法)
|
|
52
|
+
:param data: 要写入的数据(str 或 bytes)
|
|
53
|
+
:param charset: 当 data 为 str 时使用的编码,默认 utf-8
|
|
54
|
+
:return: 写入的字节数
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(data, str):
|
|
57
|
+
encoded = data.encode(charset)
|
|
58
|
+
elif isinstance(data, (bytes, bytearray)):
|
|
59
|
+
encoded = bytes(data)
|
|
60
|
+
else:
|
|
61
|
+
encoded = str(data).encode(charset)
|
|
62
|
+
return stream.write(encoded)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def write_bytes(stream: IO, data: bytes) -> int:
|
|
66
|
+
"""写入字节数组到流
|
|
67
|
+
|
|
68
|
+
:param stream: 输出流(需支持 write 方法)
|
|
69
|
+
:param data: 要写入的字节数组
|
|
70
|
+
:return: 写入的字节数
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(data, bytearray):
|
|
73
|
+
data = bytes(data)
|
|
74
|
+
return stream.write(data)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def copy(
|
|
78
|
+
input_stream: IO,
|
|
79
|
+
output_stream: IO,
|
|
80
|
+
buffer_size: int = 8192,
|
|
81
|
+
) -> int:
|
|
82
|
+
"""拷贝流数据
|
|
83
|
+
|
|
84
|
+
:param input_stream: 输入流
|
|
85
|
+
:param output_stream: 输出流
|
|
86
|
+
:param buffer_size: 缓冲区大小,默认 8192 字节
|
|
87
|
+
:return: 拷贝的总字节数
|
|
88
|
+
"""
|
|
89
|
+
total = 0
|
|
90
|
+
while True:
|
|
91
|
+
chunk = input_stream.read(buffer_size)
|
|
92
|
+
if not chunk:
|
|
93
|
+
break
|
|
94
|
+
if isinstance(chunk, str):
|
|
95
|
+
chunk = chunk.encode("utf-8")
|
|
96
|
+
output_stream.write(chunk)
|
|
97
|
+
total += len(chunk)
|
|
98
|
+
return total
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def close(closable: Optional[IO]) -> None:
|
|
102
|
+
"""安全关闭可关闭对象
|
|
103
|
+
|
|
104
|
+
如果对象为 None 或已关闭,则不做任何操作。
|
|
105
|
+
|
|
106
|
+
:param closable: 可关闭对象(如文件流),可以为 None
|
|
107
|
+
"""
|
|
108
|
+
if closable is None:
|
|
109
|
+
return
|
|
110
|
+
try:
|
|
111
|
+
closable.close()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def to_bytes(obj: Union[str, bytes, bytearray, memoryview]) -> bytes:
|
|
117
|
+
"""将对象转为 bytes
|
|
118
|
+
|
|
119
|
+
支持 str / bytes / bytearray / memoryview 类型。
|
|
120
|
+
|
|
121
|
+
:param obj: 待转换的对象
|
|
122
|
+
:return: 字节数组
|
|
123
|
+
:raises TypeError: 不支持的对象类型
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(obj, bytes):
|
|
126
|
+
return obj
|
|
127
|
+
if isinstance(obj, str):
|
|
128
|
+
return obj.encode("utf-8")
|
|
129
|
+
if isinstance(obj, bytearray):
|
|
130
|
+
return bytes(obj)
|
|
131
|
+
if isinstance(obj, memoryview):
|
|
132
|
+
return obj.tobytes()
|
|
133
|
+
raise TypeError(f"不支持的对象类型: {type(obj).__name__}")
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""路径工具模块"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PathUtil:
|
|
10
|
+
"""路径工具类"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get_path_separator() -> str:
|
|
14
|
+
"""获取当前操作系统的路径分隔符
|
|
15
|
+
|
|
16
|
+
:return: 路径分隔符字符串(Windows 为 '\\',Linux/macOS 为 '/')
|
|
17
|
+
"""
|
|
18
|
+
return os.sep
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def normalize(path: Union[str, Path]) -> str:
|
|
22
|
+
"""标准化路径
|
|
23
|
+
|
|
24
|
+
解析符号链接、去除冗余分隔符和上级引用(..),返回绝对路径。
|
|
25
|
+
|
|
26
|
+
:param path: 原始路径
|
|
27
|
+
:return: 标准化后的路径字符串
|
|
28
|
+
"""
|
|
29
|
+
return str(Path(path).resolve())
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_parent(path: Union[str, Path]) -> str:
|
|
33
|
+
"""获取父目录路径
|
|
34
|
+
|
|
35
|
+
:param path: 文件或目录路径
|
|
36
|
+
:return: 父目录的路径字符串
|
|
37
|
+
"""
|
|
38
|
+
return str(Path(path).parent)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def get_name(path: Union[str, Path]) -> str:
|
|
42
|
+
"""获取路径中的文件名或最后一级目录名
|
|
43
|
+
|
|
44
|
+
:param path: 文件或目录路径
|
|
45
|
+
:return: 文件名或目录名
|
|
46
|
+
"""
|
|
47
|
+
return Path(path).name
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def sub_path(path: Union[str, Path], from_index: int, to_index: int) -> str:
|
|
51
|
+
"""获取路径的子段
|
|
52
|
+
|
|
53
|
+
按路径层级截取子路径。索引从 0 开始。
|
|
54
|
+
|
|
55
|
+
:param path: 原始路径
|
|
56
|
+
:param from_index: 起始索引(含)
|
|
57
|
+
:param to_index: 结束索引(不含)
|
|
58
|
+
:return: 子路径字符串
|
|
59
|
+
:raises ValueError: 索引越界时抛出
|
|
60
|
+
"""
|
|
61
|
+
p = Path(path)
|
|
62
|
+
parts = p.parts
|
|
63
|
+
if from_index < 0 or to_index > len(parts) or from_index > to_index:
|
|
64
|
+
raise ValueError(f"无效的索引范围: [{from_index}, {to_index}),路径共 {len(parts)} 层")
|
|
65
|
+
sub_parts = parts[from_index:to_index]
|
|
66
|
+
return str(Path(*sub_parts))
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def is_absolute(path: Union[str, Path]) -> bool:
|
|
70
|
+
"""判断路径是否为绝对路径
|
|
71
|
+
|
|
72
|
+
:param path: 文件路径
|
|
73
|
+
:return: 是绝对路径返回 True
|
|
74
|
+
"""
|
|
75
|
+
return Path(path).is_absolute()
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def exists(path: Union[str, Path]) -> bool:
|
|
79
|
+
"""判断路径是否存在
|
|
80
|
+
|
|
81
|
+
:param path: 文件或目录路径
|
|
82
|
+
:return: 存在返回 True
|
|
83
|
+
"""
|
|
84
|
+
return Path(path).exists()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def mkdir(path: Union[str, Path]) -> Path:
|
|
88
|
+
"""创建目录(包括所有必要的父目录)
|
|
89
|
+
|
|
90
|
+
如果目录已存在则不做任何操作。
|
|
91
|
+
|
|
92
|
+
:param path: 目录路径
|
|
93
|
+
:return: 创建的目录 Path 对象
|
|
94
|
+
"""
|
|
95
|
+
p = Path(path)
|
|
96
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
return p
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def mk_parent_dirs(path: Union[str, Path]) -> Path:
|
|
101
|
+
"""创建文件的父目录
|
|
102
|
+
|
|
103
|
+
:param path: 文件路径
|
|
104
|
+
:return: 父目录的 Path 对象
|
|
105
|
+
"""
|
|
106
|
+
p = Path(path)
|
|
107
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
return p.parent
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def loop_files(
|
|
112
|
+
path: Union[str, Path],
|
|
113
|
+
file_filter: Optional[Callable[[Path], bool]] = None,
|
|
114
|
+
) -> List[Path]:
|
|
115
|
+
"""递归遍历目录下的所有文件
|
|
116
|
+
|
|
117
|
+
:param path: 目录路径
|
|
118
|
+
:param file_filter: 可选的过滤函数,接受 Path 参数,返回 True 表示保留
|
|
119
|
+
:return: 文件 Path 列表
|
|
120
|
+
:raises FileNotFoundError: 路径不存在时抛出
|
|
121
|
+
"""
|
|
122
|
+
p = Path(path)
|
|
123
|
+
if not p.exists():
|
|
124
|
+
raise FileNotFoundError(f"路径不存在: {path}")
|
|
125
|
+
if p.is_file():
|
|
126
|
+
files = [p]
|
|
127
|
+
else:
|
|
128
|
+
files = sorted(p.rglob("*"))
|
|
129
|
+
files = [f for f in files if f.is_file()]
|
|
130
|
+
if file_filter is not None:
|
|
131
|
+
files = [f for f in files if file_filter(f)]
|
|
132
|
+
return files
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def copy(
|
|
136
|
+
src: Union[str, Path],
|
|
137
|
+
dest: Union[str, Path],
|
|
138
|
+
is_override: bool = True,
|
|
139
|
+
) -> Path:
|
|
140
|
+
"""复制文件或目录
|
|
141
|
+
|
|
142
|
+
:param src: 源路径
|
|
143
|
+
:param dest: 目标路径
|
|
144
|
+
:param is_override: 是否覆盖已存在的目标,默认 True
|
|
145
|
+
:return: 目标路径的 Path 对象
|
|
146
|
+
:raises FileExistsError: 目标已存在且 is_override 为 False 时抛出
|
|
147
|
+
:raises FileNotFoundError: 源路径不存在时抛出
|
|
148
|
+
"""
|
|
149
|
+
src_p = Path(src)
|
|
150
|
+
dest_p = Path(dest)
|
|
151
|
+
if not src_p.exists():
|
|
152
|
+
raise FileNotFoundError(f"源路径不存在: {src}")
|
|
153
|
+
if dest_p.exists() and not is_override:
|
|
154
|
+
raise FileExistsError(f"目标路径已存在: {dest}")
|
|
155
|
+
if src_p.is_dir():
|
|
156
|
+
if dest_p.exists() and is_override:
|
|
157
|
+
shutil.rmtree(dest_p)
|
|
158
|
+
shutil.copytree(src_p, dest_p)
|
|
159
|
+
else:
|
|
160
|
+
dest_p.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
shutil.copy2(src_p, dest_p)
|
|
162
|
+
return dest_p
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def move(
|
|
166
|
+
src: Union[str, Path],
|
|
167
|
+
dest: Union[str, Path],
|
|
168
|
+
is_override: bool = True,
|
|
169
|
+
) -> Path:
|
|
170
|
+
"""移动文件或目录
|
|
171
|
+
|
|
172
|
+
:param src: 源路径
|
|
173
|
+
:param dest: 目标路径
|
|
174
|
+
:param is_override: 是否覆盖已存在的目标,默认 True
|
|
175
|
+
:return: 目标路径的 Path 对象
|
|
176
|
+
:raises FileExistsError: 目标已存在且 is_override 为 False 时抛出
|
|
177
|
+
:raises FileNotFoundError: 源路径不存在时抛出
|
|
178
|
+
"""
|
|
179
|
+
src_p = Path(src)
|
|
180
|
+
dest_p = Path(dest)
|
|
181
|
+
if not src_p.exists():
|
|
182
|
+
raise FileNotFoundError(f"源路径不存在: {src}")
|
|
183
|
+
if dest_p.exists():
|
|
184
|
+
if not is_override:
|
|
185
|
+
raise FileExistsError(f"目标路径已存在: {dest}")
|
|
186
|
+
if dest_p.is_dir():
|
|
187
|
+
shutil.rmtree(dest_p)
|
|
188
|
+
else:
|
|
189
|
+
dest_p.unlink()
|
|
190
|
+
dest_p.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
shutil.move(str(src_p), str(dest_p))
|
|
192
|
+
return dest_p
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def del_path(path: Union[str, Path]) -> bool:
|
|
196
|
+
"""删除文件或目录
|
|
197
|
+
|
|
198
|
+
目录将递归删除。如果路径不存在则返回 False。
|
|
199
|
+
|
|
200
|
+
:param path: 文件或目录路径
|
|
201
|
+
:return: 删除成功返回 True,路径不存在返回 False
|
|
202
|
+
"""
|
|
203
|
+
p = Path(path)
|
|
204
|
+
if not p.exists():
|
|
205
|
+
return False
|
|
206
|
+
if p.is_dir():
|
|
207
|
+
shutil.rmtree(p)
|
|
208
|
+
else:
|
|
209
|
+
p.unlink()
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def equals(
|
|
214
|
+
path1: Union[str, Path],
|
|
215
|
+
path2: Union[str, Path],
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""比较两个路径是否指向同一位置
|
|
218
|
+
|
|
219
|
+
通过解析后的绝对路径进行比较。
|
|
220
|
+
|
|
221
|
+
:param path1: 第一个路径
|
|
222
|
+
:param path2: 第二个路径
|
|
223
|
+
:return: 相等返回 True
|
|
224
|
+
"""
|
|
225
|
+
return Path(path1).resolve() == Path(path2).resolve()
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def starts_with(path: Union[str, Path], prefix: str) -> bool:
|
|
229
|
+
"""判断路径是否以指定前缀开头
|
|
230
|
+
|
|
231
|
+
:param path: 文件路径
|
|
232
|
+
:param prefix: 路径前缀
|
|
233
|
+
:return: 匹配返回 True
|
|
234
|
+
"""
|
|
235
|
+
normalized_path = PathUtil.normalize(path)
|
|
236
|
+
normalized_prefix = PathUtil.normalize(prefix)
|
|
237
|
+
return normalized_path.startswith(normalized_prefix)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def ends_with(path: Union[str, Path], suffix: str) -> bool:
|
|
241
|
+
"""判断路径是否以指定后缀结尾
|
|
242
|
+
|
|
243
|
+
:param path: 文件路径
|
|
244
|
+
:param suffix: 路径后缀
|
|
245
|
+
:return: 匹配返回 True
|
|
246
|
+
"""
|
|
247
|
+
return str(path).replace("\\", "/").endswith(suffix.replace("\\", "/"))
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""资源工具模块"""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import IO, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResourceUtil:
|
|
10
|
+
"""资源工具类"""
|
|
11
|
+
|
|
12
|
+
# 默认资源搜索路径列表,可通过 add_resource_path 扩展
|
|
13
|
+
_resource_paths: List[str] = []
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def add_resource_path(cls, path: str) -> None:
|
|
17
|
+
"""添加资源搜索路径
|
|
18
|
+
|
|
19
|
+
:param path: 资源目录的绝对或相对路径
|
|
20
|
+
"""
|
|
21
|
+
abs_path = os.path.abspath(path)
|
|
22
|
+
if abs_path not in cls._resource_paths:
|
|
23
|
+
cls._resource_paths.append(abs_path)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_resource(path: str) -> str:
|
|
27
|
+
"""获取资源的绝对路径
|
|
28
|
+
|
|
29
|
+
按以下顺序查找资源:
|
|
30
|
+
1. 绝对路径直接返回
|
|
31
|
+
2. 当前工作目录
|
|
32
|
+
3. 已注册的资源搜索路径
|
|
33
|
+
|
|
34
|
+
:param path: 资源的相对或绝对路径
|
|
35
|
+
:return: 资源的绝对路径
|
|
36
|
+
:raises FileNotFoundError: 资源不存在时抛出
|
|
37
|
+
"""
|
|
38
|
+
# 如果是绝对路径且存在,直接返回
|
|
39
|
+
if os.path.isabs(path) and os.path.exists(path):
|
|
40
|
+
return os.path.abspath(path)
|
|
41
|
+
|
|
42
|
+
# 在当前工作目录查找
|
|
43
|
+
cwd_path = os.path.join(os.getcwd(), path)
|
|
44
|
+
if os.path.exists(cwd_path):
|
|
45
|
+
return os.path.abspath(cwd_path)
|
|
46
|
+
|
|
47
|
+
# 在已注册的资源路径中查找
|
|
48
|
+
for res_dir in ResourceUtil._resource_paths:
|
|
49
|
+
candidate = os.path.join(res_dir, path)
|
|
50
|
+
if os.path.exists(candidate):
|
|
51
|
+
return os.path.abspath(candidate)
|
|
52
|
+
|
|
53
|
+
raise FileNotFoundError(f"资源不存在: {path}")
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_resource_bytes(path: str) -> bytes:
|
|
57
|
+
"""读取资源为字节数组
|
|
58
|
+
|
|
59
|
+
:param path: 资源的相对或绝对路径
|
|
60
|
+
:return: 资源内容的字节数组
|
|
61
|
+
:raises FileNotFoundError: 资源不存在时抛出
|
|
62
|
+
"""
|
|
63
|
+
abs_path = ResourceUtil.get_resource(path)
|
|
64
|
+
with open(abs_path, "rb") as f:
|
|
65
|
+
return f.read()
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def get_resource_str(path: str, charset: str = "utf-8") -> str:
|
|
69
|
+
"""读取资源为字符串
|
|
70
|
+
|
|
71
|
+
:param path: 资源的相对或绝对路径
|
|
72
|
+
:param charset: 字符编码,默认 utf-8
|
|
73
|
+
:return: 资源内容的字符串
|
|
74
|
+
:raises FileNotFoundError: 资源不存在时抛出
|
|
75
|
+
"""
|
|
76
|
+
abs_path = ResourceUtil.get_resource(path)
|
|
77
|
+
with open(abs_path, encoding=charset) as f:
|
|
78
|
+
return f.read()
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_resource_stream(path: str) -> IO:
|
|
82
|
+
"""获取资源的文件流
|
|
83
|
+
|
|
84
|
+
调用方负责关闭返回的文件流。
|
|
85
|
+
|
|
86
|
+
:param path: 资源的相对或绝对路径
|
|
87
|
+
:return: 可读的文件对象
|
|
88
|
+
:raises FileNotFoundError: 资源不存在时抛出
|
|
89
|
+
"""
|
|
90
|
+
abs_path = ResourceUtil.get_resource(path)
|
|
91
|
+
return open(abs_path, "rb")
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def get_resources(pattern: str) -> List[str]:
|
|
95
|
+
"""根据模式匹配获取资源列表
|
|
96
|
+
|
|
97
|
+
支持通配符模式(如 "*.txt"、"data/*.csv")。
|
|
98
|
+
在当前工作目录和已注册的资源搜索路径中搜索。
|
|
99
|
+
|
|
100
|
+
:param pattern: 文件名匹配模式(支持 * 和 ? 通配符)
|
|
101
|
+
:return: 匹配的资源绝对路径列表
|
|
102
|
+
"""
|
|
103
|
+
results: List[str] = []
|
|
104
|
+
|
|
105
|
+
# 在当前工作目录中搜索
|
|
106
|
+
cwd = os.getcwd()
|
|
107
|
+
ResourceUtil._search_in_dir(cwd, pattern, results)
|
|
108
|
+
|
|
109
|
+
# 在已注册的资源路径中搜索
|
|
110
|
+
for res_dir in ResourceUtil._resource_paths:
|
|
111
|
+
ResourceUtil._search_in_dir(res_dir, pattern, results)
|
|
112
|
+
|
|
113
|
+
# 去重并排序
|
|
114
|
+
return sorted(set(results))
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _search_in_dir(base_dir: str, pattern: str, results: List[str]) -> None:
|
|
118
|
+
"""在指定目录中递归搜索匹配的文件
|
|
119
|
+
|
|
120
|
+
:param base_dir: 基础搜索目录
|
|
121
|
+
:pattern: 文件名匹配模式
|
|
122
|
+
:param results: 结果列表(原地修改)
|
|
123
|
+
"""
|
|
124
|
+
base = Path(base_dir)
|
|
125
|
+
if not base.is_dir():
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# 如果 pattern 包含路径分隔符,按路径层级匹配
|
|
129
|
+
if os.sep in pattern or "/" in pattern:
|
|
130
|
+
for match in base.glob(pattern):
|
|
131
|
+
if match.is_file():
|
|
132
|
+
results.append(str(match.resolve()))
|
|
133
|
+
else:
|
|
134
|
+
# 只按文件名匹配,递归搜索所有子目录
|
|
135
|
+
for file_path in base.rglob("*"):
|
|
136
|
+
if file_path.is_file() and fnmatch.fnmatch(file_path.name, pattern):
|
|
137
|
+
results.append(str(file_path.resolve()))
|