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,62 @@
|
|
|
1
|
+
"""系统属性工具类"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import tempfile
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SystemUtil:
|
|
12
|
+
"""系统属性工具类,对应 Java SystemPropsUtil"""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def get(name: str, default: Optional[str] = None) -> Optional[str]:
|
|
16
|
+
"""获取系统环境变量"""
|
|
17
|
+
return os.environ.get(name, default)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def get_all() -> dict:
|
|
21
|
+
"""获取所有环境变量"""
|
|
22
|
+
return dict(os.environ)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def get_os_name() -> str:
|
|
26
|
+
"""获取操作系统名称"""
|
|
27
|
+
return platform.system()
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def get_os_arch() -> str:
|
|
31
|
+
"""获取操作系统架构"""
|
|
32
|
+
return platform.machine()
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_user_dir() -> str:
|
|
36
|
+
"""获取用户当前工作目录"""
|
|
37
|
+
return os.getcwd()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def get_user_home() -> str:
|
|
41
|
+
"""获取用户主目录"""
|
|
42
|
+
return os.path.expanduser("~")
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def get_tmp_dir() -> str:
|
|
46
|
+
"""获取临时目录"""
|
|
47
|
+
return tempfile.gettempdir()
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_file_separator() -> str:
|
|
51
|
+
"""获取文件分隔符"""
|
|
52
|
+
return os.sep
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def get_line_separator() -> str:
|
|
56
|
+
"""获取行分隔符"""
|
|
57
|
+
return os.linesep
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def is_windows() -> bool:
|
|
61
|
+
"""是否Windows系统"""
|
|
62
|
+
return platform.system().lower() == "windows"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
from urllib.parse import parse_qs, parse_qsl, quote, unquote, urlencode, urlparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class URLUtil:
|
|
7
|
+
"""URL工具类,对应 Java cn.hutool.core.util.URLUtil"""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def normalize(url_str: str) -> str:
|
|
11
|
+
"""标准化URL,补齐协议头
|
|
12
|
+
|
|
13
|
+
如果URL缺少协议头(http:// 或 https://),自动补充 http://。
|
|
14
|
+
|
|
15
|
+
:param url_str: 原始URL字符串
|
|
16
|
+
:return: 标准化后的URL字符串
|
|
17
|
+
"""
|
|
18
|
+
if not url_str:
|
|
19
|
+
return url_str
|
|
20
|
+
url_str = url_str.strip()
|
|
21
|
+
if not re.match(r"^[a-zA-Z]+://", url_str):
|
|
22
|
+
url_str = "http://" + url_str
|
|
23
|
+
return url_str
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def encode(url_str: str, charset: str = "utf-8") -> str:
|
|
27
|
+
"""URL编码
|
|
28
|
+
|
|
29
|
+
对整个URL字符串进行编码。
|
|
30
|
+
|
|
31
|
+
:param url_str: 待编码的URL字符串
|
|
32
|
+
:param charset: 字符编码,默认utf-8
|
|
33
|
+
:return: 编码后的URL字符串
|
|
34
|
+
"""
|
|
35
|
+
if not url_str:
|
|
36
|
+
return url_str
|
|
37
|
+
return quote(url_str, encoding=charset, safe=":/?#[]@!$&'()*+,;=-._~%")
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def decode(url_str: str, charset: str = "utf-8") -> str:
|
|
41
|
+
"""URL解码
|
|
42
|
+
|
|
43
|
+
对URL编码的字符串进行解码。
|
|
44
|
+
|
|
45
|
+
:param url_str: 待解码的URL字符串
|
|
46
|
+
:param charset: 字符编码,默认utf-8
|
|
47
|
+
:return: 解码后的URL字符串
|
|
48
|
+
"""
|
|
49
|
+
if not url_str:
|
|
50
|
+
return url_str
|
|
51
|
+
return unquote(url_str, encoding=charset)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def encode_params(params_str: str, charset: str = "utf-8") -> str:
|
|
55
|
+
"""编码URL参数部分
|
|
56
|
+
|
|
57
|
+
对URL查询参数字符串中的值部分进行编码。
|
|
58
|
+
|
|
59
|
+
:param params_str: 参数字符串,如 "key1=value1&key2=value2"
|
|
60
|
+
:param charset: 字符编码,默认utf-8
|
|
61
|
+
:return: 编码后的参数字符串
|
|
62
|
+
"""
|
|
63
|
+
if not params_str:
|
|
64
|
+
return params_str
|
|
65
|
+
pairs = parse_qsl(params_str, keep_blank_values=True)
|
|
66
|
+
encoded_pairs = []
|
|
67
|
+
for key, value in pairs:
|
|
68
|
+
encoded_key = quote(key, encoding=charset, safe="")
|
|
69
|
+
encoded_value = quote(value, encoding=charset, safe="")
|
|
70
|
+
encoded_pairs.append(f"{encoded_key}={encoded_value}")
|
|
71
|
+
return "&".join(encoded_pairs)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def get_path(url_str: str) -> str:
|
|
75
|
+
"""获取URL的路径部分
|
|
76
|
+
|
|
77
|
+
:param url_str: URL字符串
|
|
78
|
+
:return: URL的路径部分,解析失败返回空字符串
|
|
79
|
+
"""
|
|
80
|
+
if not url_str:
|
|
81
|
+
return ""
|
|
82
|
+
try:
|
|
83
|
+
parsed = urlparse(url_str)
|
|
84
|
+
return parsed.path
|
|
85
|
+
except Exception:
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def get_host(url_str: str) -> str:
|
|
90
|
+
"""获取URL的主机部分
|
|
91
|
+
|
|
92
|
+
:param url_str: URL字符串
|
|
93
|
+
:return: URL的主机名,解析失败返回空字符串
|
|
94
|
+
"""
|
|
95
|
+
if not url_str:
|
|
96
|
+
return ""
|
|
97
|
+
try:
|
|
98
|
+
parsed = urlparse(url_str)
|
|
99
|
+
return parsed.hostname or ""
|
|
100
|
+
except Exception:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def get_port(url_str: str) -> int:
|
|
105
|
+
"""获取URL的端口号,无端口返回-1
|
|
106
|
+
|
|
107
|
+
:param url_str: URL字符串
|
|
108
|
+
:return: 端口号,无端口或解析失败返回-1
|
|
109
|
+
"""
|
|
110
|
+
if not url_str:
|
|
111
|
+
return -1
|
|
112
|
+
try:
|
|
113
|
+
parsed = urlparse(url_str)
|
|
114
|
+
return parsed.port if parsed.port is not None else -1
|
|
115
|
+
except Exception:
|
|
116
|
+
return -1
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def get_query(url_str: str) -> str:
|
|
120
|
+
"""获取URL的查询参数部分
|
|
121
|
+
|
|
122
|
+
:param url_str: URL字符串
|
|
123
|
+
:return: URL的查询参数字符串(不含?),解析失败返回空字符串
|
|
124
|
+
"""
|
|
125
|
+
if not url_str:
|
|
126
|
+
return ""
|
|
127
|
+
try:
|
|
128
|
+
parsed = urlparse(url_str)
|
|
129
|
+
return parsed.query
|
|
130
|
+
except Exception:
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def build_url(base: str, params: Dict[str, str]) -> str:
|
|
135
|
+
"""构建带参数的URL
|
|
136
|
+
|
|
137
|
+
在基础URL上附加参数,如果原URL已有参数则追加。
|
|
138
|
+
|
|
139
|
+
:param base: 基础URL
|
|
140
|
+
:param params: 参数字典
|
|
141
|
+
:return: 带参数的完整URL
|
|
142
|
+
"""
|
|
143
|
+
if not base:
|
|
144
|
+
return ""
|
|
145
|
+
if not params:
|
|
146
|
+
return base
|
|
147
|
+
|
|
148
|
+
param_str = urlencode(params, encoding="utf-8")
|
|
149
|
+
separator = "&" if "?" in base else "?"
|
|
150
|
+
return f"{base}{separator}{param_str}"
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def url_with_form(url: str, form: Dict[str, str]) -> str:
|
|
154
|
+
"""在URL后附加表单参数
|
|
155
|
+
|
|
156
|
+
与build_url类似,将表单参数附加到URL后。
|
|
157
|
+
|
|
158
|
+
:param url: 原始URL
|
|
159
|
+
:param form: 表单参数字典
|
|
160
|
+
:return: 附加参数后的URL
|
|
161
|
+
"""
|
|
162
|
+
return URLUtil.build_url(url, form)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def to_params(param_map: Dict[str, str], charset: str = "utf-8") -> str:
|
|
166
|
+
"""参数Map转URL参数字符串
|
|
167
|
+
|
|
168
|
+
:param param_map: 参数字典
|
|
169
|
+
:param charset: 字符编码,默认utf-8
|
|
170
|
+
:return: URL参数字符串,如 "key1=value1&key2=value2"
|
|
171
|
+
"""
|
|
172
|
+
if not param_map:
|
|
173
|
+
return ""
|
|
174
|
+
return urlencode(param_map, encoding=charset)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def decode_param_map(params_str: str, charset: str = "utf-8") -> Dict[str, str]:
|
|
178
|
+
"""URL参数字符串转Map(每个key只保留最后一个值)
|
|
179
|
+
|
|
180
|
+
:param params_str: 参数字符串,如 "key1=value1&key2=value2"
|
|
181
|
+
:param charset: 字符编码,默认utf-8
|
|
182
|
+
:return: 参数字典,重复key时保留最后一个值
|
|
183
|
+
"""
|
|
184
|
+
if not params_str:
|
|
185
|
+
return {}
|
|
186
|
+
pairs = parse_qsl(params_str, keep_blank_values=True, encoding=charset)
|
|
187
|
+
result: Dict[str, str] = {}
|
|
188
|
+
for key, value in pairs:
|
|
189
|
+
result[key] = value
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def decode_params(params_str: str, charset: str = "utf-8") -> Dict[str, List[str]]:
|
|
194
|
+
"""URL参数字符串转Map(每个key对应值列表)
|
|
195
|
+
|
|
196
|
+
:param params_str: 参数字符串,如 "key=value1&key=value2"
|
|
197
|
+
:param charset: 字符编码,默认utf-8
|
|
198
|
+
:return: 参数字典,每个key对应一个值列表
|
|
199
|
+
"""
|
|
200
|
+
if not params_str:
|
|
201
|
+
return {}
|
|
202
|
+
return parse_qs(params_str, keep_blank_values=True, encoding=charset)
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def is_https(url_str: str) -> bool:
|
|
206
|
+
"""是否为HTTPS
|
|
207
|
+
|
|
208
|
+
:param url_str: URL字符串
|
|
209
|
+
:return: 是HTTPS返回True,否则返回False
|
|
210
|
+
"""
|
|
211
|
+
if not url_str:
|
|
212
|
+
return False
|
|
213
|
+
try:
|
|
214
|
+
parsed = urlparse(url_str)
|
|
215
|
+
return parsed.scheme.lower() == "https"
|
|
216
|
+
except Exception:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def is_http(url_str: str) -> bool:
|
|
221
|
+
"""是否为HTTP
|
|
222
|
+
|
|
223
|
+
:param url_str: URL字符串
|
|
224
|
+
:return: 是HTTP返回True,否则返回False
|
|
225
|
+
"""
|
|
226
|
+
if not url_str:
|
|
227
|
+
return False
|
|
228
|
+
try:
|
|
229
|
+
parsed = urlparse(url_str)
|
|
230
|
+
return parsed.scheme.lower() == "http"
|
|
231
|
+
except Exception:
|
|
232
|
+
return False
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""版本比较工具类"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VersionUtil:
|
|
7
|
+
"""版本比较工具类"""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def compare(version1: str, version2: str) -> int:
|
|
11
|
+
"""比较两个版本号,1: v1>v2, 0: 相等, -1: v1<v2
|
|
12
|
+
支持 "1.2.3" 格式
|
|
13
|
+
"""
|
|
14
|
+
v1_parts = version1.split(".")
|
|
15
|
+
v2_parts = version2.split(".")
|
|
16
|
+
max_len = max(len(v1_parts), len(v2_parts))
|
|
17
|
+
|
|
18
|
+
for i in range(max_len):
|
|
19
|
+
v1 = int(v1_parts[i]) if i < len(v1_parts) else 0
|
|
20
|
+
v2 = int(v2_parts[i]) if i < len(v2_parts) else 0
|
|
21
|
+
if v1 > v2:
|
|
22
|
+
return 1
|
|
23
|
+
if v1 < v2:
|
|
24
|
+
return -1
|
|
25
|
+
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def is_greater(version1: str, version2: str) -> bool:
|
|
30
|
+
"""version1是否大于version2"""
|
|
31
|
+
return VersionUtil.compare(version1, version2) == 1
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def is_lower(version1: str, version2: str) -> bool:
|
|
35
|
+
"""version1是否小于version2"""
|
|
36
|
+
return VersionUtil.compare(version1, version2) == -1
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_main_version(version: str) -> str:
|
|
40
|
+
"""获取主版本号,如 "1.2.3" -> "1" """
|
|
41
|
+
return version.split(".")[0]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from xml.dom import minidom
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class XmlUtil:
|
|
8
|
+
"""XML工具类"""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def read_xml(xml_str_or_path: str) -> ET.Element:
|
|
12
|
+
"""读取XML(字符串或文件路径)
|
|
13
|
+
|
|
14
|
+
支持传入XML字符串或文件路径,自动判断并解析。
|
|
15
|
+
|
|
16
|
+
:param xml_str_or_path: XML字符串或文件路径
|
|
17
|
+
:return: 解析后的XML根元素
|
|
18
|
+
"""
|
|
19
|
+
xml_str_or_path = xml_str_or_path.strip()
|
|
20
|
+
if xml_str_or_path.startswith("<"):
|
|
21
|
+
return ET.fromstring(xml_str_or_path)
|
|
22
|
+
else:
|
|
23
|
+
tree = ET.parse(xml_str_or_path)
|
|
24
|
+
return tree.getroot()
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def parse_to_map(xml_str: str) -> dict:
|
|
28
|
+
"""XML转字典
|
|
29
|
+
|
|
30
|
+
将XML字符串递归转换为嵌套字典结构。如果同名子元素出现多次,值为列表。
|
|
31
|
+
|
|
32
|
+
:param xml_str: XML字符串
|
|
33
|
+
:return: 转换后的字典
|
|
34
|
+
"""
|
|
35
|
+
root = ET.fromstring(xml_str) if xml_str.strip().startswith("<") else ET.parse(xml_str).getroot()
|
|
36
|
+
|
|
37
|
+
def _element_to_dict(element: ET.Element) -> Any:
|
|
38
|
+
children = list(element)
|
|
39
|
+
if not children:
|
|
40
|
+
text = element.text
|
|
41
|
+
if text is not None:
|
|
42
|
+
text = text.strip()
|
|
43
|
+
return text if text else ""
|
|
44
|
+
|
|
45
|
+
result: Dict[str, Any] = {}
|
|
46
|
+
for child in children:
|
|
47
|
+
child_data = _element_to_dict(child)
|
|
48
|
+
if child.tag in result:
|
|
49
|
+
existing = result[child.tag]
|
|
50
|
+
if isinstance(existing, list):
|
|
51
|
+
existing.append(child_data)
|
|
52
|
+
else:
|
|
53
|
+
result[child.tag] = [existing, child_data]
|
|
54
|
+
else:
|
|
55
|
+
result[child.tag] = child_data
|
|
56
|
+
|
|
57
|
+
# 添加属性
|
|
58
|
+
if element.attrib:
|
|
59
|
+
for key, value in element.attrib.items():
|
|
60
|
+
result[f"@{key}"] = value
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
tag = root.tag
|
|
65
|
+
return {tag: _element_to_dict(root)}
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def to_xml_str(root_name: str, data: dict) -> str:
|
|
69
|
+
"""字典转XML字符串
|
|
70
|
+
|
|
71
|
+
:param root_name: 根元素名称
|
|
72
|
+
:param data: 待转换的字典数据
|
|
73
|
+
:return: XML字符串
|
|
74
|
+
"""
|
|
75
|
+
return XmlUtil.map_to_xml(data, root_name)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def map_to_xml(data: dict, root_name: str = "root") -> str:
|
|
79
|
+
"""字典转XML
|
|
80
|
+
|
|
81
|
+
将字典递归转换为XML字符串。
|
|
82
|
+
|
|
83
|
+
:param data: 待转换的字典数据
|
|
84
|
+
:param root_name: 根元素名称,默认为"root"
|
|
85
|
+
:return: XML字符串
|
|
86
|
+
"""
|
|
87
|
+
root = ET.Element(root_name)
|
|
88
|
+
XmlUtil._build_xml(root, data)
|
|
89
|
+
return ET.tostring(root, encoding="unicode")
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _build_xml(parent: ET.Element, data: Any) -> None:
|
|
93
|
+
"""递归构建XML元素
|
|
94
|
+
|
|
95
|
+
:param parent: 父元素
|
|
96
|
+
:param data: 当前数据(字典或值)
|
|
97
|
+
"""
|
|
98
|
+
if isinstance(data, dict):
|
|
99
|
+
for key, value in data.items():
|
|
100
|
+
if key.startswith("@"):
|
|
101
|
+
# XML属性
|
|
102
|
+
parent.set(key[1:], str(value))
|
|
103
|
+
elif isinstance(value, list):
|
|
104
|
+
for item in value:
|
|
105
|
+
child = ET.SubElement(parent, key)
|
|
106
|
+
XmlUtil._build_xml(child, item)
|
|
107
|
+
else:
|
|
108
|
+
child = ET.SubElement(parent, key)
|
|
109
|
+
XmlUtil._build_xml(child, value)
|
|
110
|
+
else:
|
|
111
|
+
parent.text = str(data) if data is not None else ""
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def format_xml(xml_str: str) -> str:
|
|
115
|
+
"""格式化XML
|
|
116
|
+
|
|
117
|
+
将XML字符串格式化为带缩进的可读格式。
|
|
118
|
+
|
|
119
|
+
:param xml_str: 待格式化的XML字符串
|
|
120
|
+
:return: 格式化后的XML字符串
|
|
121
|
+
"""
|
|
122
|
+
dom = minidom.parseString(xml_str)
|
|
123
|
+
pretty_xml = dom.toprettyxml(indent=" ", encoding=None)
|
|
124
|
+
# 去除minidom添加的多余空行
|
|
125
|
+
lines = [line for line in pretty_xml.split("\n") if line.strip()]
|
|
126
|
+
return "\n".join(lines)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def get_element_text(element: ET.Element, xpath: str) -> Optional[str]:
|
|
130
|
+
"""获取元素文本
|
|
131
|
+
|
|
132
|
+
通过xpath查找子元素并返回其文本内容。
|
|
133
|
+
|
|
134
|
+
:param element: XML元素
|
|
135
|
+
:param xpath: 子元素的标签路径,如 "child/subchild"
|
|
136
|
+
:return: 元素文本内容,未找到则返回None
|
|
137
|
+
"""
|
|
138
|
+
target = element.find(xpath)
|
|
139
|
+
if target is not None:
|
|
140
|
+
return target.text
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def clean_invalid(xml_str: str) -> str:
|
|
145
|
+
"""清理XML中的非法字符
|
|
146
|
+
|
|
147
|
+
移除XML规范中不允许的控制字符(除了制表符、换行符、回车符)。
|
|
148
|
+
|
|
149
|
+
:param xml_str: 原始XML字符串
|
|
150
|
+
:return: 清理后的XML字符串
|
|
151
|
+
"""
|
|
152
|
+
# 移除非法字符:XML合法字符为 #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
|
153
|
+
valid_xml_chars = re.compile(
|
|
154
|
+
r"[^\x09\x0A\x0D\x20-\x7E\x80-\xFF"
|
|
155
|
+
r"Ā--�"
|
|
156
|
+
r"\U00010000-\U0010FFFF]"
|
|
157
|
+
)
|
|
158
|
+
return valid_xml_chars.sub("", xml_str)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import os
|
|
3
|
+
import zipfile
|
|
4
|
+
import zlib
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ZipUtil:
|
|
9
|
+
"""压缩工具类"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def zip(src_path: str, dest_path: Optional[str] = None, charset: str = "utf-8") -> str:
|
|
13
|
+
"""压缩文件或目录为zip
|
|
14
|
+
|
|
15
|
+
:param src_path: 源文件或目录路径
|
|
16
|
+
:param dest_path: 目标zip文件路径,默认为源路径加.zip后缀
|
|
17
|
+
:param charset: 文件名编码字符集,默认utf-8
|
|
18
|
+
:return: 生成的zip文件路径
|
|
19
|
+
"""
|
|
20
|
+
if not os.path.exists(src_path):
|
|
21
|
+
raise FileNotFoundError(f"源路径不存在: {src_path}")
|
|
22
|
+
|
|
23
|
+
if dest_path is None:
|
|
24
|
+
dest_path = src_path + ".zip"
|
|
25
|
+
|
|
26
|
+
dest_path = os.path.abspath(dest_path)
|
|
27
|
+
src_path = os.path.abspath(src_path)
|
|
28
|
+
|
|
29
|
+
with zipfile.ZipFile(dest_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
30
|
+
if os.path.isfile(src_path):
|
|
31
|
+
zf.write(src_path, os.path.basename(src_path))
|
|
32
|
+
elif os.path.isdir(src_path):
|
|
33
|
+
base_dir = os.path.basename(src_path)
|
|
34
|
+
for root, dirs, files in os.walk(src_path):
|
|
35
|
+
for file in files:
|
|
36
|
+
file_path = os.path.join(root, file)
|
|
37
|
+
arcname = os.path.join(base_dir, os.path.relpath(file_path, src_path))
|
|
38
|
+
zf.write(file_path, arcname)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f"不支持的路径类型: {src_path}")
|
|
41
|
+
|
|
42
|
+
return dest_path
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def unzip(zip_file: str, dest_path: str, charset: str = "utf-8") -> str:
|
|
46
|
+
"""解压zip文件
|
|
47
|
+
|
|
48
|
+
:param zip_file: zip文件路径
|
|
49
|
+
:param dest_path: 解压目标目录
|
|
50
|
+
:param charset: 文件名编码字符集,默认utf-8
|
|
51
|
+
:return: 解压目标目录路径
|
|
52
|
+
"""
|
|
53
|
+
if not os.path.exists(zip_file):
|
|
54
|
+
raise FileNotFoundError(f"zip文件不存在: {zip_file}")
|
|
55
|
+
|
|
56
|
+
dest_path = os.path.abspath(dest_path)
|
|
57
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
with zipfile.ZipFile(zip_file, "r") as zf:
|
|
60
|
+
for info in zf.infolist():
|
|
61
|
+
# 尝试用指定编码解码文件名
|
|
62
|
+
try:
|
|
63
|
+
filename = info.filename.encode("cp437").decode(charset)
|
|
64
|
+
except (UnicodeDecodeError, UnicodeEncodeError):
|
|
65
|
+
filename = info.filename
|
|
66
|
+
info.filename = filename
|
|
67
|
+
zf.extract(info, dest_path)
|
|
68
|
+
|
|
69
|
+
return dest_path
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def gzip(data: bytes) -> bytes:
|
|
73
|
+
"""Gzip压缩
|
|
74
|
+
|
|
75
|
+
:param data: 待压缩的字节数据
|
|
76
|
+
:return: 压缩后的字节数据
|
|
77
|
+
"""
|
|
78
|
+
return gzip.compress(data)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def gzip_str(data: str, charset: str = "utf-8") -> bytes:
|
|
82
|
+
"""字符串Gzip压缩
|
|
83
|
+
|
|
84
|
+
:param data: 待压缩的字符串
|
|
85
|
+
:param charset: 字符编码,默认utf-8
|
|
86
|
+
:return: 压缩后的字节数据
|
|
87
|
+
"""
|
|
88
|
+
return gzip.compress(data.encode(charset))
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def ungzip(data: bytes) -> bytes:
|
|
92
|
+
"""Gzip解压
|
|
93
|
+
|
|
94
|
+
:param data: 待解压的字节数据
|
|
95
|
+
:return: 解压后的字节数据
|
|
96
|
+
"""
|
|
97
|
+
return gzip.decompress(data)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def ungzip_str(data: bytes, charset: str = "utf-8") -> str:
|
|
101
|
+
"""Gzip解压为字符串
|
|
102
|
+
|
|
103
|
+
:param data: 待解压的字节数据
|
|
104
|
+
:param charset: 字符编码,默认utf-8
|
|
105
|
+
:return: 解压后的字符串
|
|
106
|
+
"""
|
|
107
|
+
return gzip.decompress(data).decode(charset)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def zlib_compress(data: bytes, level: int = -1) -> bytes:
|
|
111
|
+
"""Zlib压缩
|
|
112
|
+
|
|
113
|
+
:param data: 待压缩的字节数据
|
|
114
|
+
:param level: 压缩级别,-1为默认,0为不压缩,1-9为压缩级别(1最快,9最小)
|
|
115
|
+
:return: 压缩后的字节数据
|
|
116
|
+
"""
|
|
117
|
+
return zlib.compress(data, level)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def zlib_uncompress(data: bytes) -> bytes:
|
|
121
|
+
"""Zlib解压
|
|
122
|
+
|
|
123
|
+
:param data: 待解压的字节数据
|
|
124
|
+
:return: 解压后的字节数据
|
|
125
|
+
"""
|
|
126
|
+
return zlib.decompress(data)
|
hutool/cron/__init__.py
ADDED