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,650 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件工具类,对应 Java cn.hutool.core.io.FileUtil
|
|
3
|
+
|
|
4
|
+
基于 pathlib.Path 实现,辅以 os 和 shutil。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileUtil:
|
|
16
|
+
"""文件工具类,对应 Java cn.hutool.core.io.FileUtil"""
|
|
17
|
+
|
|
18
|
+
FILE_SEPARATOR: str = os.sep
|
|
19
|
+
|
|
20
|
+
# ------------------------------------------------------------------
|
|
21
|
+
# 判断
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def is_windows() -> bool:
|
|
26
|
+
"""判断当前操作系统是否为 Windows"""
|
|
27
|
+
return os.name == "nt"
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def exist(path: Union[str, Path]) -> bool:
|
|
31
|
+
"""判断文件或目录是否存在"""
|
|
32
|
+
return Path(path).exists()
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def is_dir(path: Union[str, Path]) -> bool:
|
|
36
|
+
"""判断是否为目录"""
|
|
37
|
+
return Path(path).is_dir()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def is_file(path: Union[str, Path]) -> bool:
|
|
41
|
+
"""判断是否为文件"""
|
|
42
|
+
return Path(path).is_file()
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def is_absolute(path: Union[str, Path]) -> bool:
|
|
46
|
+
"""判断路径是否为绝对路径"""
|
|
47
|
+
return Path(path).is_absolute()
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def is_empty(path: Union[str, Path]) -> bool:
|
|
51
|
+
"""文件或目录是否为空
|
|
52
|
+
|
|
53
|
+
- 文件:大小为 0 视为空。
|
|
54
|
+
- 目录:不含任何子项视为空。
|
|
55
|
+
- 路径不存在视为空。
|
|
56
|
+
"""
|
|
57
|
+
p = Path(path)
|
|
58
|
+
if not p.exists():
|
|
59
|
+
return True
|
|
60
|
+
if p.is_file():
|
|
61
|
+
return p.stat().st_size == 0
|
|
62
|
+
if p.is_dir():
|
|
63
|
+
# 如果能列出任何子项则非空
|
|
64
|
+
return not any(p.iterdir())
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# 创建
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def file(*names: str) -> Path:
|
|
73
|
+
"""根据多个名称段构建文件路径
|
|
74
|
+
|
|
75
|
+
例如: FileUtil.file("home", "user", "test.txt")
|
|
76
|
+
"""
|
|
77
|
+
if not names:
|
|
78
|
+
raise ValueError("至少需要一个路径段")
|
|
79
|
+
result = Path(names[0])
|
|
80
|
+
for name in names[1:]:
|
|
81
|
+
result = result / name
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def touch(path: Union[str, Path]) -> Path:
|
|
86
|
+
"""创建文件(包括父目录)
|
|
87
|
+
|
|
88
|
+
如果文件已存在则只更新访问/修改时间。
|
|
89
|
+
"""
|
|
90
|
+
p = Path(path)
|
|
91
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
p.touch()
|
|
93
|
+
return p
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def mkdir(path: Union[str, Path]) -> Path:
|
|
97
|
+
"""创建目录
|
|
98
|
+
|
|
99
|
+
如果目录已存在则直接返回。
|
|
100
|
+
"""
|
|
101
|
+
p = Path(path)
|
|
102
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
return p
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def mkdirs(path: Union[str, Path]) -> Path:
|
|
107
|
+
"""创建多级目录(与 mkdir 行为一致,支持多级创建)"""
|
|
108
|
+
p = Path(path)
|
|
109
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
return p
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def create_temp_file(
|
|
114
|
+
prefix: str = "hutool",
|
|
115
|
+
suffix: str = ".tmp",
|
|
116
|
+
parent_dir: Optional[str] = None,
|
|
117
|
+
) -> Path:
|
|
118
|
+
"""创建临时文件
|
|
119
|
+
|
|
120
|
+
:param prefix: 文件名前缀
|
|
121
|
+
:param suffix: 文件名后缀(含点号)
|
|
122
|
+
:param parent_dir: 临时文件所在目录,为 None 时使用系统临时目录
|
|
123
|
+
:return: 创建的临时文件路径
|
|
124
|
+
"""
|
|
125
|
+
dir_path: Optional[str] = parent_dir
|
|
126
|
+
fd, tmp_path = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir_path)
|
|
127
|
+
os.close(fd)
|
|
128
|
+
return Path(tmp_path)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# 删除
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def del_file(path: Union[str, Path]) -> bool:
|
|
136
|
+
"""删除文件或目录
|
|
137
|
+
|
|
138
|
+
- 文件:直接删除。
|
|
139
|
+
- 目录:递归删除整个目录树。
|
|
140
|
+
- 路径不存在时返回 True。
|
|
141
|
+
|
|
142
|
+
:return: 是否删除成功
|
|
143
|
+
"""
|
|
144
|
+
p = Path(path)
|
|
145
|
+
if not p.exists():
|
|
146
|
+
return True
|
|
147
|
+
if p.is_file() or p.is_symlink():
|
|
148
|
+
p.unlink()
|
|
149
|
+
return True
|
|
150
|
+
if p.is_dir():
|
|
151
|
+
shutil.rmtree(p)
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def clean(path: Union[str, Path]) -> bool:
|
|
157
|
+
"""清空目录内容(不删除目录本身)
|
|
158
|
+
|
|
159
|
+
:return: 是否清空成功
|
|
160
|
+
"""
|
|
161
|
+
p = Path(path)
|
|
162
|
+
if not p.is_dir():
|
|
163
|
+
return False
|
|
164
|
+
for child in p.iterdir():
|
|
165
|
+
if child.is_dir():
|
|
166
|
+
shutil.rmtree(child)
|
|
167
|
+
else:
|
|
168
|
+
child.unlink()
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# 复制 / 移动
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def copy(
|
|
177
|
+
src: Union[str, Path],
|
|
178
|
+
dest: Union[str, Path],
|
|
179
|
+
is_override: bool = True,
|
|
180
|
+
) -> Path:
|
|
181
|
+
"""复制文件或目录
|
|
182
|
+
|
|
183
|
+
:param src: 源路径
|
|
184
|
+
:param dest: 目标路径
|
|
185
|
+
:param is_override: 是否覆盖已存在的目标
|
|
186
|
+
:return: 目标路径
|
|
187
|
+
"""
|
|
188
|
+
src_path = Path(src)
|
|
189
|
+
dest_path = Path(dest)
|
|
190
|
+
|
|
191
|
+
if not src_path.exists():
|
|
192
|
+
raise FileNotFoundError(f"源路径不存在: {src_path}")
|
|
193
|
+
|
|
194
|
+
if src_path.is_dir():
|
|
195
|
+
# 复制目录
|
|
196
|
+
if dest_path.exists() and is_override:
|
|
197
|
+
shutil.rmtree(dest_path)
|
|
198
|
+
shutil.copytree(src_path, dest_path)
|
|
199
|
+
else:
|
|
200
|
+
# 复制文件
|
|
201
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
if not is_override and dest_path.exists():
|
|
203
|
+
return dest_path
|
|
204
|
+
shutil.copy2(src_path, dest_path)
|
|
205
|
+
return dest_path
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def copy_file(
|
|
209
|
+
src: Union[str, Path],
|
|
210
|
+
dest: Union[str, Path],
|
|
211
|
+
) -> Path:
|
|
212
|
+
"""复制文件
|
|
213
|
+
|
|
214
|
+
如果目标目录不存在则自动创建。
|
|
215
|
+
"""
|
|
216
|
+
src_path = Path(src)
|
|
217
|
+
dest_path = Path(dest)
|
|
218
|
+
|
|
219
|
+
if not src_path.is_file():
|
|
220
|
+
raise FileNotFoundError(f"源文件不存在或不是文件: {src_path}")
|
|
221
|
+
|
|
222
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
shutil.copy2(src_path, dest_path)
|
|
224
|
+
return dest_path
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def move(
|
|
228
|
+
src: Union[str, Path],
|
|
229
|
+
dest: Union[str, Path],
|
|
230
|
+
is_override: bool = True,
|
|
231
|
+
) -> Path:
|
|
232
|
+
"""移动文件或目录
|
|
233
|
+
|
|
234
|
+
:param src: 源路径
|
|
235
|
+
:param dest: 目标路径
|
|
236
|
+
:param is_override: 是否覆盖已存在的目标
|
|
237
|
+
:return: 目标路径
|
|
238
|
+
"""
|
|
239
|
+
src_path = Path(src)
|
|
240
|
+
dest_path = Path(dest)
|
|
241
|
+
|
|
242
|
+
if not src_path.exists():
|
|
243
|
+
raise FileNotFoundError(f"源路径不存在: {src_path}")
|
|
244
|
+
|
|
245
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
if dest_path.exists():
|
|
248
|
+
if not is_override:
|
|
249
|
+
return dest_path
|
|
250
|
+
if dest_path.is_dir():
|
|
251
|
+
shutil.rmtree(dest_path)
|
|
252
|
+
else:
|
|
253
|
+
dest_path.unlink()
|
|
254
|
+
|
|
255
|
+
shutil.move(str(src_path), str(dest_path))
|
|
256
|
+
return dest_path
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def rename(file: Union[str, Path], new_name: str) -> Path:
|
|
260
|
+
"""重命名文件或目录
|
|
261
|
+
|
|
262
|
+
:param file: 源文件或目录路径
|
|
263
|
+
:param new_name: 新名称(仅名称部分,不含路径)
|
|
264
|
+
:return: 重命名后的路径
|
|
265
|
+
"""
|
|
266
|
+
p = Path(file)
|
|
267
|
+
if not p.exists():
|
|
268
|
+
raise FileNotFoundError(f"路径不存在: {p}")
|
|
269
|
+
new_path = p.parent / new_name
|
|
270
|
+
p.rename(new_path)
|
|
271
|
+
return new_path
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
# 读写
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def read_string(path: Union[str, Path], charset: str = "utf-8") -> str:
|
|
279
|
+
"""读取文件内容为字符串
|
|
280
|
+
|
|
281
|
+
:param path: 文件路径
|
|
282
|
+
:param charset: 字符编码
|
|
283
|
+
:return: 文件内容字符串
|
|
284
|
+
"""
|
|
285
|
+
return Path(path).read_text(encoding=charset)
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def read_bytes(path: Union[str, Path]) -> bytes:
|
|
289
|
+
"""读取文件内容为字节数组"""
|
|
290
|
+
return Path(path).read_bytes()
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def read_lines(path: Union[str, Path], charset: str = "utf-8") -> List[str]:
|
|
294
|
+
"""读取文件内容为字符串列表(每行一个元素,保留换行符)"""
|
|
295
|
+
with open(path, encoding=charset) as f:
|
|
296
|
+
return f.readlines()
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def read_utf8_lines(path: Union[str, Path]) -> List[str]:
|
|
300
|
+
"""以 UTF-8 编码读取文件的每一行(去除行尾换行符)"""
|
|
301
|
+
return FileUtil.read_lines_str(path, charset="utf-8")
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def read_lines_str(path: Union[str, Path], charset: str = "utf-8") -> List[str]:
|
|
305
|
+
"""读取文件的每一行,去除行尾换行符"""
|
|
306
|
+
with open(path, encoding=charset) as f:
|
|
307
|
+
return f.read().splitlines()
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def write_string(
|
|
311
|
+
path: Union[str, Path],
|
|
312
|
+
content: str,
|
|
313
|
+
charset: str = "utf-8",
|
|
314
|
+
is_append: bool = False,
|
|
315
|
+
) -> Path:
|
|
316
|
+
"""写入字符串到文件
|
|
317
|
+
|
|
318
|
+
:param path: 文件路径
|
|
319
|
+
:param content: 写入内容
|
|
320
|
+
:param charset: 字符编码
|
|
321
|
+
:param is_append: 是否追加模式
|
|
322
|
+
:return: 文件路径
|
|
323
|
+
"""
|
|
324
|
+
p = Path(path)
|
|
325
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
mode = "a" if is_append else "w"
|
|
327
|
+
with open(p, mode, encoding=charset) as f:
|
|
328
|
+
f.write(content)
|
|
329
|
+
return p
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def write_bytes(
|
|
333
|
+
path: Union[str, Path],
|
|
334
|
+
data: bytes,
|
|
335
|
+
is_append: bool = False,
|
|
336
|
+
) -> Path:
|
|
337
|
+
"""写字节数组到文件
|
|
338
|
+
|
|
339
|
+
:param path: 文件路径
|
|
340
|
+
:param data: 字节数据
|
|
341
|
+
:param is_append: 是否追加模式
|
|
342
|
+
:return: 文件路径
|
|
343
|
+
"""
|
|
344
|
+
p = Path(path)
|
|
345
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
mode = "ab" if is_append else "wb"
|
|
347
|
+
with open(p, mode) as f:
|
|
348
|
+
f.write(data)
|
|
349
|
+
return p
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def write_lines(
|
|
353
|
+
path: Union[str, Path],
|
|
354
|
+
lines: list,
|
|
355
|
+
charset: str = "utf-8",
|
|
356
|
+
is_append: bool = False,
|
|
357
|
+
) -> Path:
|
|
358
|
+
"""写入行列表到文件
|
|
359
|
+
|
|
360
|
+
每个元素后自动追加换行符。
|
|
361
|
+
|
|
362
|
+
:param path: 文件路径
|
|
363
|
+
:param lines: 行列表
|
|
364
|
+
:param charset: 字符编码
|
|
365
|
+
:param is_append: 是否追加模式
|
|
366
|
+
:return: 文件路径
|
|
367
|
+
"""
|
|
368
|
+
p = Path(path)
|
|
369
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
370
|
+
mode = "a" if is_append else "w"
|
|
371
|
+
with open(p, mode, encoding=charset) as f:
|
|
372
|
+
for line in lines:
|
|
373
|
+
f.write(str(line))
|
|
374
|
+
f.write("\n")
|
|
375
|
+
return p
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def append_string(
|
|
379
|
+
path: Union[str, Path],
|
|
380
|
+
content: str,
|
|
381
|
+
charset: str = "utf-8",
|
|
382
|
+
) -> Path:
|
|
383
|
+
"""追加字符串到文件末尾
|
|
384
|
+
|
|
385
|
+
:param path: 文件路径
|
|
386
|
+
:param content: 追加内容
|
|
387
|
+
:param charset: 字符编码
|
|
388
|
+
:return: 文件路径
|
|
389
|
+
"""
|
|
390
|
+
return FileUtil.write_string(path, content, charset=charset, is_append=True)
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def append_lines(
|
|
394
|
+
path: Union[str, Path],
|
|
395
|
+
lines: list,
|
|
396
|
+
charset: str = "utf-8",
|
|
397
|
+
) -> Path:
|
|
398
|
+
"""追加行列表到文件末尾
|
|
399
|
+
|
|
400
|
+
:param path: 文件路径
|
|
401
|
+
:param lines: 行列表
|
|
402
|
+
:param charset: 字符编码
|
|
403
|
+
:return: 文件路径
|
|
404
|
+
"""
|
|
405
|
+
return FileUtil.write_lines(path, lines, charset=charset, is_append=True)
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------------------------
|
|
408
|
+
# 遍历
|
|
409
|
+
# ------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def loop_files(
|
|
413
|
+
path: Union[str, Path],
|
|
414
|
+
max_depth: Optional[int] = None,
|
|
415
|
+
file_filter: Optional[Callable[[Path], bool]] = None,
|
|
416
|
+
) -> List[Path]:
|
|
417
|
+
"""递归遍历目录下的所有文件
|
|
418
|
+
|
|
419
|
+
:param path: 根目录路径
|
|
420
|
+
:param max_depth: 最大递归深度,None 表示不限制
|
|
421
|
+
:param file_filter: 文件过滤函数,返回 True 保留
|
|
422
|
+
:return: 符合条件的文件路径列表
|
|
423
|
+
"""
|
|
424
|
+
root = Path(path)
|
|
425
|
+
if not root.is_dir():
|
|
426
|
+
return []
|
|
427
|
+
|
|
428
|
+
result: List[Path] = []
|
|
429
|
+
FileUtil._walk_files(root, 0, max_depth, file_filter, result)
|
|
430
|
+
return result
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _walk_files(
|
|
434
|
+
directory: Path,
|
|
435
|
+
current_depth: int,
|
|
436
|
+
max_depth: Optional[int],
|
|
437
|
+
file_filter: Optional[Callable[[Path], bool]],
|
|
438
|
+
result: List[Path],
|
|
439
|
+
) -> None:
|
|
440
|
+
"""递归遍历的内部实现"""
|
|
441
|
+
if max_depth is not None and current_depth > max_depth:
|
|
442
|
+
return
|
|
443
|
+
try:
|
|
444
|
+
for child in sorted(directory.iterdir()):
|
|
445
|
+
if child.is_file():
|
|
446
|
+
if file_filter is None or file_filter(child):
|
|
447
|
+
result.append(child)
|
|
448
|
+
elif child.is_dir():
|
|
449
|
+
FileUtil._walk_files(child, current_depth + 1, max_depth, file_filter, result)
|
|
450
|
+
except PermissionError:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def list_file_names(path: Union[str, Path]) -> List[str]:
|
|
455
|
+
"""列出目录下的文件名(仅直接子文件,不含子目录内的文件)
|
|
456
|
+
|
|
457
|
+
:param path: 目录路径
|
|
458
|
+
:return: 文件名列表(不含路径前缀)
|
|
459
|
+
"""
|
|
460
|
+
p = Path(path)
|
|
461
|
+
if not p.is_dir():
|
|
462
|
+
return []
|
|
463
|
+
return [child.name for child in sorted(p.iterdir()) if child.is_file()]
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def walk_files(
|
|
467
|
+
path: Union[str, Path],
|
|
468
|
+
consumer: Callable[[Path], None],
|
|
469
|
+
) -> None:
|
|
470
|
+
"""递归遍历文件并对每个文件执行操作
|
|
471
|
+
|
|
472
|
+
:param path: 根目录路径
|
|
473
|
+
:param consumer: 对每个文件执行的回调函数
|
|
474
|
+
"""
|
|
475
|
+
root = Path(path)
|
|
476
|
+
if not root.exists():
|
|
477
|
+
return
|
|
478
|
+
if root.is_file():
|
|
479
|
+
consumer(root)
|
|
480
|
+
return
|
|
481
|
+
for file_path in FileUtil.loop_files(root):
|
|
482
|
+
consumer(file_path)
|
|
483
|
+
|
|
484
|
+
# ------------------------------------------------------------------
|
|
485
|
+
# 信息
|
|
486
|
+
# ------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
def size(path: Union[str, Path]) -> int:
|
|
490
|
+
"""获取文件大小(字节)
|
|
491
|
+
|
|
492
|
+
对于目录,返回整个目录树的总大小。
|
|
493
|
+
"""
|
|
494
|
+
p = Path(path)
|
|
495
|
+
if p.is_file():
|
|
496
|
+
return p.stat().st_size
|
|
497
|
+
if p.is_dir():
|
|
498
|
+
total = 0
|
|
499
|
+
for child in p.rglob("*"):
|
|
500
|
+
if child.is_file():
|
|
501
|
+
total += child.stat().st_size
|
|
502
|
+
return total
|
|
503
|
+
raise FileNotFoundError(f"路径不存在: {p}")
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def last_modified_time(path: Union[str, Path]) -> datetime:
|
|
507
|
+
"""获取文件最后修改时间
|
|
508
|
+
|
|
509
|
+
:return: 最后修改时间的 datetime 对象
|
|
510
|
+
"""
|
|
511
|
+
p = Path(path)
|
|
512
|
+
if not p.exists():
|
|
513
|
+
raise FileNotFoundError(f"路径不存在: {p}")
|
|
514
|
+
mtime = p.stat().st_mtime
|
|
515
|
+
return datetime.fromtimestamp(mtime)
|
|
516
|
+
|
|
517
|
+
@staticmethod
|
|
518
|
+
def get_total_lines(path: Union[str, Path]) -> int:
|
|
519
|
+
"""获取文件总行数"""
|
|
520
|
+
count = 0
|
|
521
|
+
with open(path, "rb") as f:
|
|
522
|
+
for _ in f:
|
|
523
|
+
count += 1
|
|
524
|
+
return count
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def get_name(path: Union[str, Path]) -> str:
|
|
528
|
+
"""获取文件名(含扩展名)
|
|
529
|
+
|
|
530
|
+
例如: /home/user/test.txt -> test.txt
|
|
531
|
+
"""
|
|
532
|
+
return Path(path).name
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def get_suffix(path: Union[str, Path]) -> str:
|
|
536
|
+
"""获取文件扩展名(不带点号)
|
|
537
|
+
|
|
538
|
+
例如: /home/user/test.txt -> txt
|
|
539
|
+
|
|
540
|
+
如果没有扩展名则返回空字符串。
|
|
541
|
+
"""
|
|
542
|
+
name = Path(path).name
|
|
543
|
+
dot_index = name.rfind(".")
|
|
544
|
+
if dot_index <= 0:
|
|
545
|
+
return ""
|
|
546
|
+
return name[dot_index + 1 :]
|
|
547
|
+
|
|
548
|
+
@staticmethod
|
|
549
|
+
def get_prefix(path: Union[str, Path]) -> str:
|
|
550
|
+
"""获取文件名前缀(不含扩展名)
|
|
551
|
+
|
|
552
|
+
例如: /home/user/test.txt -> test
|
|
553
|
+
"""
|
|
554
|
+
name = Path(path).name
|
|
555
|
+
dot_index = name.rfind(".")
|
|
556
|
+
if dot_index <= 0:
|
|
557
|
+
return name
|
|
558
|
+
return name[:dot_index]
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def main_name(path: Union[str, Path]) -> str:
|
|
562
|
+
"""获取主文件名(同 get_prefix)"""
|
|
563
|
+
return FileUtil.get_prefix(path)
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def normalize(path: Union[str, Path]) -> str:
|
|
567
|
+
"""标准化路径,解析 ~ 和相对路径符号
|
|
568
|
+
|
|
569
|
+
:return: 标准化后的路径字符串
|
|
570
|
+
"""
|
|
571
|
+
p = Path(path).expanduser()
|
|
572
|
+
try:
|
|
573
|
+
return str(p.resolve())
|
|
574
|
+
except OSError:
|
|
575
|
+
return str(p)
|
|
576
|
+
|
|
577
|
+
# ------------------------------------------------------------------
|
|
578
|
+
# 系统路径
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
582
|
+
def get_tmp_dir_path() -> str:
|
|
583
|
+
"""获取系统临时目录路径(字符串)"""
|
|
584
|
+
return tempfile.gettempdir()
|
|
585
|
+
|
|
586
|
+
@staticmethod
|
|
587
|
+
def get_tmp_dir() -> Path:
|
|
588
|
+
"""获取系统临时目录(Path 对象)"""
|
|
589
|
+
return Path(tempfile.gettempdir())
|
|
590
|
+
|
|
591
|
+
@staticmethod
|
|
592
|
+
def get_user_home_path() -> str:
|
|
593
|
+
"""获取用户主目录路径(字符串)"""
|
|
594
|
+
return str(Path.home())
|
|
595
|
+
|
|
596
|
+
@staticmethod
|
|
597
|
+
def get_user_home_dir() -> Path:
|
|
598
|
+
"""获取用户主目录(Path 对象)"""
|
|
599
|
+
return Path.home()
|
|
600
|
+
|
|
601
|
+
# ------------------------------------------------------------------
|
|
602
|
+
# 工具
|
|
603
|
+
# ------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
@staticmethod
|
|
606
|
+
def newer_than(file: Union[str, Path], reference: Union[str, Path]) -> bool:
|
|
607
|
+
"""判断文件是否比参考文件更新
|
|
608
|
+
|
|
609
|
+
:param file: 待比较的文件
|
|
610
|
+
:param reference: 参考文件
|
|
611
|
+
:return: 如果 file 的修改时间晚于 reference 则返回 True
|
|
612
|
+
"""
|
|
613
|
+
file_path = Path(file)
|
|
614
|
+
ref_path = Path(reference)
|
|
615
|
+
|
|
616
|
+
if not file_path.exists():
|
|
617
|
+
return False
|
|
618
|
+
if not ref_path.exists():
|
|
619
|
+
return True
|
|
620
|
+
|
|
621
|
+
file_mtime = file_path.stat().st_mtime
|
|
622
|
+
ref_mtime = ref_path.stat().st_mtime
|
|
623
|
+
return file_mtime > ref_mtime
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def is_symlink(path: Union[str, Path]) -> bool:
|
|
627
|
+
"""判断是否为符号链接"""
|
|
628
|
+
return Path(path).is_symlink()
|
|
629
|
+
|
|
630
|
+
@staticmethod
|
|
631
|
+
def sub_path(path: Union[str, Path], start: int, end: int) -> str:
|
|
632
|
+
"""获取子路径
|
|
633
|
+
|
|
634
|
+
:param path: 源路径
|
|
635
|
+
:param start: 起始索引(含)
|
|
636
|
+
:param end: 结束索引(不含)
|
|
637
|
+
:return: 子路径字符串
|
|
638
|
+
|
|
639
|
+
例如: sub_path("/home/user/test.txt", 1, 3) -> "user/test.txt"
|
|
640
|
+
"""
|
|
641
|
+
p = Path(path)
|
|
642
|
+
parts = p.parts
|
|
643
|
+
if start < 0:
|
|
644
|
+
start = 0
|
|
645
|
+
if end > len(parts):
|
|
646
|
+
end = len(parts)
|
|
647
|
+
if start >= end:
|
|
648
|
+
return ""
|
|
649
|
+
sub_parts = parts[start:end]
|
|
650
|
+
return str(Path(*sub_parts))
|