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
hutool/http/http_util.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
from urllib.parse import parse_qs, quote, unquote
|
|
4
|
+
|
|
5
|
+
from .http_request import HttpRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpUtil:
|
|
9
|
+
"""HTTP工具类,提供常用的HTTP操作方法"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def is_https(url: str) -> bool:
|
|
13
|
+
"""判断URL是否为HTTPS协议
|
|
14
|
+
|
|
15
|
+
:param url: 待判断的URL字符串
|
|
16
|
+
:return: 如果URL以 https:// 开头返回 True,否则返回 False
|
|
17
|
+
"""
|
|
18
|
+
return url is not None and url.lower().startswith("https://")
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def is_http(url: str) -> bool:
|
|
22
|
+
"""判断URL是否为HTTP协议
|
|
23
|
+
|
|
24
|
+
:param url: 待判断的URL字符串
|
|
25
|
+
:return: 如果URL以 http:// 开头返回 True,否则返回 False
|
|
26
|
+
"""
|
|
27
|
+
return url is not None and url.lower().startswith("http://")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def get(
|
|
31
|
+
url: str,
|
|
32
|
+
params: Optional[dict] = None,
|
|
33
|
+
timeout: int = 30000,
|
|
34
|
+
headers: Optional[dict] = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
"""发送GET请求并返回响应体字符串
|
|
37
|
+
|
|
38
|
+
:param url: 请求URL
|
|
39
|
+
:param params: 查询参数
|
|
40
|
+
:param timeout: 超时时间(毫秒)
|
|
41
|
+
:param headers: 请求头
|
|
42
|
+
:return: 响应体字符串
|
|
43
|
+
"""
|
|
44
|
+
request = HttpRequest.get(url).timeout(timeout)
|
|
45
|
+
if headers:
|
|
46
|
+
request.headers(headers)
|
|
47
|
+
if params:
|
|
48
|
+
request._params = params
|
|
49
|
+
response = request.execute()
|
|
50
|
+
return response.to_str()
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def post(
|
|
54
|
+
url: str,
|
|
55
|
+
data=None,
|
|
56
|
+
json_data=None,
|
|
57
|
+
timeout: int = 30000,
|
|
58
|
+
headers: Optional[dict] = None,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""发送POST请求并返回响应体字符串
|
|
61
|
+
|
|
62
|
+
:param url: 请求URL
|
|
63
|
+
:param data: 表单数据
|
|
64
|
+
:param json_data: JSON数据
|
|
65
|
+
:param timeout: 超时时间(毫秒)
|
|
66
|
+
:param headers: 请求头
|
|
67
|
+
:return: 响应体字符串
|
|
68
|
+
"""
|
|
69
|
+
request = HttpRequest.post(url).timeout(timeout)
|
|
70
|
+
if headers:
|
|
71
|
+
request.headers(headers)
|
|
72
|
+
if data is not None:
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
for k, v in data.items():
|
|
75
|
+
request.form(k, str(v))
|
|
76
|
+
else:
|
|
77
|
+
request.body(str(data))
|
|
78
|
+
if json_data is not None:
|
|
79
|
+
request.json(json_data)
|
|
80
|
+
response = request.execute()
|
|
81
|
+
return response.to_str()
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def create_get(url: str) -> HttpRequest:
|
|
85
|
+
"""创建GET请求对象
|
|
86
|
+
|
|
87
|
+
:param url: 请求URL
|
|
88
|
+
:return: HttpRequest对象
|
|
89
|
+
"""
|
|
90
|
+
return HttpRequest.get(url)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def create_post(url: str) -> HttpRequest:
|
|
94
|
+
"""创建POST请求对象
|
|
95
|
+
|
|
96
|
+
:param url: 请求URL
|
|
97
|
+
:return: HttpRequest对象
|
|
98
|
+
"""
|
|
99
|
+
return HttpRequest.post(url)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def download_string(url: str, charset: str = "utf-8") -> str:
|
|
103
|
+
"""下载URL内容为字符串
|
|
104
|
+
|
|
105
|
+
:param url: 下载URL
|
|
106
|
+
:param charset: 字符集,默认utf-8
|
|
107
|
+
:return: 下载的字符串内容
|
|
108
|
+
"""
|
|
109
|
+
response = HttpRequest.get(url).charset(charset).execute()
|
|
110
|
+
return response.to_str()
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def download_file(url: str, dest: str) -> int:
|
|
114
|
+
"""下载URL内容到文件
|
|
115
|
+
|
|
116
|
+
:param url: 下载URL
|
|
117
|
+
:param dest: 目标文件路径
|
|
118
|
+
:return: 下载的字节数
|
|
119
|
+
"""
|
|
120
|
+
import httpx
|
|
121
|
+
|
|
122
|
+
with httpx.stream("GET", url, follow_redirects=True, timeout=60) as response:
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
total = 0
|
|
125
|
+
with open(dest, "wb") as f:
|
|
126
|
+
for chunk in response.iter_bytes():
|
|
127
|
+
f.write(chunk)
|
|
128
|
+
total += len(chunk)
|
|
129
|
+
return total
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def download_bytes(url: str) -> bytes:
|
|
133
|
+
"""下载URL内容为字节数组
|
|
134
|
+
|
|
135
|
+
:param url: 下载URL
|
|
136
|
+
:return: 下载的字节数组
|
|
137
|
+
"""
|
|
138
|
+
response = HttpRequest.get(url).execute()
|
|
139
|
+
return response.to_bytes()
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def to_params(param_map: dict, charset: str = "utf-8") -> str:
|
|
143
|
+
"""将参数Map转换为URL查询字符串
|
|
144
|
+
|
|
145
|
+
:param param_map: 参数字典
|
|
146
|
+
:param charset: 字符集,默认utf-8
|
|
147
|
+
:return: URL编码后的查询字符串,如 "key1=value1&key2=value2"
|
|
148
|
+
"""
|
|
149
|
+
if not param_map:
|
|
150
|
+
return ""
|
|
151
|
+
encoded_pairs = []
|
|
152
|
+
for key, value in param_map.items():
|
|
153
|
+
encoded_key = quote(str(key), encoding=charset)
|
|
154
|
+
encoded_value = quote(str(value), encoding=charset) if value is not None else ""
|
|
155
|
+
encoded_pairs.append(f"{encoded_key}={encoded_value}")
|
|
156
|
+
return "&".join(encoded_pairs)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def decode_param_map(params_str: str) -> Dict[str, str]:
|
|
160
|
+
"""将URL查询字符串解码为单值参数字典
|
|
161
|
+
|
|
162
|
+
如果某个键对应多个值,只取第一个值。
|
|
163
|
+
|
|
164
|
+
:param params_str: URL查询字符串,如 "key1=value1&key2=value2"
|
|
165
|
+
:return: 参数字典,值为单个字符串
|
|
166
|
+
"""
|
|
167
|
+
if not params_str:
|
|
168
|
+
return {}
|
|
169
|
+
result: Dict[str, str] = {}
|
|
170
|
+
parsed = parse_qs(params_str, keep_blank_values=True)
|
|
171
|
+
for key, values in parsed.items():
|
|
172
|
+
result[key] = values[0] if values else ""
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def decode_params(params_str: str) -> Dict[str, List[str]]:
|
|
177
|
+
"""将URL查询字符串解码为多值参数字典
|
|
178
|
+
|
|
179
|
+
:param params_str: URL查询字符串,如 "key=value1&key=value2"
|
|
180
|
+
:return: 参数字典,值为字符串列表
|
|
181
|
+
"""
|
|
182
|
+
if not params_str:
|
|
183
|
+
return {}
|
|
184
|
+
return parse_qs(params_str, keep_blank_values=True)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def url_with_form(url: str, form: dict) -> str:
|
|
188
|
+
"""将表单参数附加到URL上
|
|
189
|
+
|
|
190
|
+
:param url: 基础URL
|
|
191
|
+
:param form: 表单参数字典
|
|
192
|
+
:return: 附加参数后的完整URL
|
|
193
|
+
"""
|
|
194
|
+
if not form:
|
|
195
|
+
return url
|
|
196
|
+
params_str = HttpUtil.to_params(form)
|
|
197
|
+
if "?" in url:
|
|
198
|
+
if url.endswith("&") or url.endswith("?"):
|
|
199
|
+
return url + params_str
|
|
200
|
+
return url + "&" + params_str
|
|
201
|
+
return url + "?" + params_str
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def get_charset(content_type: str) -> str:
|
|
205
|
+
"""从Content-Type中提取字符集
|
|
206
|
+
|
|
207
|
+
:param content_type: Content-Type头值,如 "text/html; charset=utf-8"
|
|
208
|
+
:return: 字符集名称,如 "utf-8";如果未指定则返回 "utf-8"
|
|
209
|
+
"""
|
|
210
|
+
if not content_type:
|
|
211
|
+
return "utf-8"
|
|
212
|
+
match = re.search(r"charset\s*=\s*([^\s;]+)", content_type, re.IGNORECASE)
|
|
213
|
+
if match:
|
|
214
|
+
return match.group(1).strip().strip('"').strip("'")
|
|
215
|
+
return "utf-8"
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def encode_url(url_str: str) -> str:
|
|
219
|
+
"""URL编码
|
|
220
|
+
|
|
221
|
+
:param url_str: 待编码的URL字符串
|
|
222
|
+
:return: 编码后的URL字符串
|
|
223
|
+
"""
|
|
224
|
+
if not url_str:
|
|
225
|
+
return ""
|
|
226
|
+
return quote(url_str, safe=":/?#[]@!$&'()*+,;=-._~%")
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def decode_url(url_str: str) -> str:
|
|
230
|
+
"""URL解码
|
|
231
|
+
|
|
232
|
+
:param url_str: 待解码的URL字符串
|
|
233
|
+
:return: 解码后的URL字符串
|
|
234
|
+
"""
|
|
235
|
+
if not url_str:
|
|
236
|
+
return ""
|
|
237
|
+
return unquote(url_str)
|
hutool/json_util.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, List, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JSONUtil:
|
|
7
|
+
"""JSON工具类"""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def create_obj() -> dict:
|
|
11
|
+
"""创建空JSON对象"""
|
|
12
|
+
return {}
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def create_array() -> list:
|
|
16
|
+
"""创建空JSON数组"""
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------------ #
|
|
20
|
+
# 解析
|
|
21
|
+
# ------------------------------------------------------------------ #
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def parse_obj(json_str: str) -> dict:
|
|
25
|
+
"""解析JSON字符串为字典"""
|
|
26
|
+
result = json.loads(json_str)
|
|
27
|
+
if not isinstance(result, dict):
|
|
28
|
+
raise ValueError("JSON字符串不是对象类型")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def parse_array(json_str: str) -> list:
|
|
33
|
+
"""解析JSON字符串为列表"""
|
|
34
|
+
result = json.loads(json_str)
|
|
35
|
+
if not isinstance(result, list):
|
|
36
|
+
raise ValueError("JSON字符串不是数组类型")
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def parse(json_str: str):
|
|
41
|
+
"""解析JSON字符串,自动返回对应Python类型"""
|
|
42
|
+
return json.loads(json_str)
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------------ #
|
|
45
|
+
# 序列化
|
|
46
|
+
# ------------------------------------------------------------------ #
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def to_json_str(obj: Any, indent: Optional[int] = None) -> str:
|
|
50
|
+
"""对象转JSON字符串
|
|
51
|
+
|
|
52
|
+
:param obj: 待序列化的对象
|
|
53
|
+
:param indent: 缩进空格数,None表示不缩进
|
|
54
|
+
"""
|
|
55
|
+
return json.dumps(obj, ensure_ascii=False, indent=indent)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def to_json_pretty_str(obj: Any) -> str:
|
|
59
|
+
"""对象转格式化JSON字符串(缩进2空格)"""
|
|
60
|
+
return json.dumps(obj, ensure_ascii=False, indent=2)
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------ #
|
|
63
|
+
# Bean 转换(简易实现,基于类构造器)
|
|
64
|
+
# ------------------------------------------------------------------ #
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def to_bean(json_str: str, bean_class: Type):
|
|
68
|
+
"""JSON字符串转对象
|
|
69
|
+
|
|
70
|
+
将JSON解析为字典后,通过 bean_class(\\*\\*dict) 构造实例。
|
|
71
|
+
如果 bean_class 提供了 from_dict 类方法,则优先调用。
|
|
72
|
+
"""
|
|
73
|
+
data = json.loads(json_str)
|
|
74
|
+
if hasattr(bean_class, "from_dict") and callable(bean_class.from_dict):
|
|
75
|
+
return bean_class.from_dict(data)
|
|
76
|
+
if isinstance(data, dict):
|
|
77
|
+
return bean_class(**data)
|
|
78
|
+
return bean_class(data)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def to_bean_list(json_str: str, element_class: Type) -> list:
|
|
82
|
+
"""JSON字符串转对象列表"""
|
|
83
|
+
data = json.loads(json_str)
|
|
84
|
+
if not isinstance(data, list):
|
|
85
|
+
raise ValueError("JSON字符串不是数组类型")
|
|
86
|
+
if hasattr(element_class, "from_dict") and callable(element_class.from_dict):
|
|
87
|
+
return [element_class.from_dict(item) for item in data]
|
|
88
|
+
return [element_class(**item) if isinstance(item, dict) else element_class(item) for item in data]
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def from_bean(obj: Any) -> str:
|
|
92
|
+
"""对象转JSON字符串
|
|
93
|
+
|
|
94
|
+
如果对象具有 to_dict 方法则先调用,否则直接序列化。
|
|
95
|
+
"""
|
|
96
|
+
if hasattr(obj, "to_dict") and callable(obj.to_dict):
|
|
97
|
+
return json.dumps(obj.to_dict(), ensure_ascii=False)
|
|
98
|
+
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------ #
|
|
101
|
+
# 文件读写
|
|
102
|
+
# ------------------------------------------------------------------ #
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def read_json(path: str, charset: str = "utf-8"):
|
|
106
|
+
"""读取JSON文件并解析"""
|
|
107
|
+
with open(path, encoding=charset) as f:
|
|
108
|
+
return json.load(f)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def read_json_object(path: str, charset: str = "utf-8") -> dict:
|
|
112
|
+
"""读取JSON文件为字典"""
|
|
113
|
+
result = JSONUtil.read_json(path, charset)
|
|
114
|
+
if not isinstance(result, dict):
|
|
115
|
+
raise ValueError("JSON文件内容不是对象类型")
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def read_json_array(path: str, charset: str = "utf-8") -> list:
|
|
120
|
+
"""读取JSON文件为列表"""
|
|
121
|
+
result = JSONUtil.read_json(path, charset)
|
|
122
|
+
if not isinstance(result, list):
|
|
123
|
+
raise ValueError("JSON文件内容不是数组类型")
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def write_json(path: str, obj: Any, charset: str = "utf-8", indent: Optional[int] = None) -> None:
|
|
128
|
+
"""写入JSON文件
|
|
129
|
+
|
|
130
|
+
:param path: 文件路径
|
|
131
|
+
:param obj: 待写入对象
|
|
132
|
+
:param charset: 文件编码
|
|
133
|
+
:param indent: 缩进空格数,None表示不缩进
|
|
134
|
+
"""
|
|
135
|
+
with open(path, "w", encoding=charset) as f:
|
|
136
|
+
json.dump(obj, f, ensure_ascii=False, indent=indent)
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------ #
|
|
139
|
+
# 校验
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def is_json(str_val: str) -> bool:
|
|
144
|
+
"""是否为有效JSON"""
|
|
145
|
+
try:
|
|
146
|
+
json.loads(str_val)
|
|
147
|
+
return True
|
|
148
|
+
except (json.JSONDecodeError, TypeError):
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def is_json_obj(str_val: str) -> bool:
|
|
153
|
+
"""是否为JSON对象"""
|
|
154
|
+
try:
|
|
155
|
+
return isinstance(json.loads(str_val), dict)
|
|
156
|
+
except (json.JSONDecodeError, TypeError):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def is_json_array(str_val: str) -> bool:
|
|
161
|
+
"""是否为JSON数组"""
|
|
162
|
+
try:
|
|
163
|
+
return isinstance(json.loads(str_val), list)
|
|
164
|
+
except (json.JSONDecodeError, TypeError):
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------ #
|
|
168
|
+
# 格式化 / 压缩
|
|
169
|
+
# ------------------------------------------------------------------ #
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def format_json(json_str: str, indent: int = 2) -> str:
|
|
173
|
+
"""格式化JSON字符串"""
|
|
174
|
+
obj = json.loads(json_str)
|
|
175
|
+
return json.dumps(obj, ensure_ascii=False, indent=indent)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def compress(json_str: str) -> str:
|
|
179
|
+
"""压缩JSON(去除所有空白)"""
|
|
180
|
+
obj = json.loads(json_str)
|
|
181
|
+
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------ #
|
|
184
|
+
# 路径操作(支持 'a.b.c' 和 'a[0].b')
|
|
185
|
+
# ------------------------------------------------------------------ #
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _parse_path_keys(path: str) -> List[Union[str, int]]:
|
|
189
|
+
"""将路径字符串解析为键列表
|
|
190
|
+
|
|
191
|
+
例如 'a.b[0].c' -> ['a', 'b', 0, 'c']
|
|
192
|
+
"""
|
|
193
|
+
keys: List[Union[str, int]] = []
|
|
194
|
+
for part in re.split(r"\.(?![^\[]*\])", path):
|
|
195
|
+
# 处理 key[index] 形式
|
|
196
|
+
match = re.match(r"^(\w+)((?:\[\d+\])*)$", part)
|
|
197
|
+
if match:
|
|
198
|
+
keys.append(match.group(1))
|
|
199
|
+
for idx in re.findall(r"\[(\d+)\]", match.group(2)):
|
|
200
|
+
keys.append(int(idx))
|
|
201
|
+
elif part:
|
|
202
|
+
keys.append(part)
|
|
203
|
+
return keys
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def get_by_path(json_data: Any, path: str) -> Any:
|
|
207
|
+
"""按路径获取值
|
|
208
|
+
|
|
209
|
+
:param json_data: JSON数据(字典或列表)
|
|
210
|
+
:param path: 路径,如 'a.b.c' 或 'a[0].b'
|
|
211
|
+
:return: 对应路径的值,路径不存在时返回 None
|
|
212
|
+
"""
|
|
213
|
+
keys = JSONUtil._parse_path_keys(path)
|
|
214
|
+
current = json_data
|
|
215
|
+
for key in keys:
|
|
216
|
+
if current is None:
|
|
217
|
+
return None
|
|
218
|
+
try:
|
|
219
|
+
current = current[key]
|
|
220
|
+
except (KeyError, IndexError, TypeError):
|
|
221
|
+
return None
|
|
222
|
+
return current
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def put_by_path(json_data: Any, path: str, value: Any) -> None:
|
|
226
|
+
"""按路径设置值
|
|
227
|
+
|
|
228
|
+
:param json_data: JSON数据(字典或列表)
|
|
229
|
+
:param path: 路径,如 'a.b.c' 或 'a[0].b'
|
|
230
|
+
:param value: 要设置的值
|
|
231
|
+
"""
|
|
232
|
+
keys = JSONUtil._parse_path_keys(path)
|
|
233
|
+
if not keys:
|
|
234
|
+
raise ValueError("路径不能为空")
|
|
235
|
+
current = json_data
|
|
236
|
+
for key in keys[:-1]:
|
|
237
|
+
next_key = keys[keys.index(key) + 1] if keys.index(key) + 1 < len(keys) else None
|
|
238
|
+
if isinstance(current, dict):
|
|
239
|
+
if key not in current:
|
|
240
|
+
current[key] = {} if isinstance(next_key, str) else []
|
|
241
|
+
current = current[key]
|
|
242
|
+
elif isinstance(current, list):
|
|
243
|
+
while len(current) <= key:
|
|
244
|
+
current.append(None)
|
|
245
|
+
if current[key] is None:
|
|
246
|
+
current[key] = {} if isinstance(next_key, str) else []
|
|
247
|
+
current = current[key]
|
|
248
|
+
else:
|
|
249
|
+
raise TypeError(f"无法在路径 '{path}' 上设置值:中间节点类型不支持")
|
|
250
|
+
last_key = keys[-1]
|
|
251
|
+
current[last_key] = value
|
hutool/jwt_util.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""JWT工具类"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import jwt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JWTUtil:
|
|
8
|
+
"""JWT工具类"""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def create_token(payload: dict, secret: str, algorithm: str = "HS256") -> str:
|
|
12
|
+
"""创建JWT token
|
|
13
|
+
|
|
14
|
+
:param payload: 载荷数据
|
|
15
|
+
:param secret: 密钥
|
|
16
|
+
:param algorithm: 加密算法,默认为 'HS256'
|
|
17
|
+
:return: JWT token字符串
|
|
18
|
+
"""
|
|
19
|
+
return jwt.encode(payload, secret, algorithm=algorithm)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def parse_token(token: str, secret: str, algorithm: str = "HS256") -> dict:
|
|
23
|
+
"""解析并验证JWT token
|
|
24
|
+
|
|
25
|
+
:param token: JWT token字符串
|
|
26
|
+
:param secret: 密钥
|
|
27
|
+
:param algorithm: 加密算法,默认为 'HS256'
|
|
28
|
+
:return: 解析后的载荷数据
|
|
29
|
+
:raises jwt.ExpiredSignatureError: token已过期
|
|
30
|
+
:raises jwt.InvalidTokenError: token无效
|
|
31
|
+
"""
|
|
32
|
+
return jwt.decode(token, secret, algorithms=[algorithm])
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def verify(token: str, secret: str, algorithm: str = "HS256") -> bool:
|
|
36
|
+
"""验证token是否有效
|
|
37
|
+
|
|
38
|
+
:param token: JWT token字符串
|
|
39
|
+
:param secret: 密钥
|
|
40
|
+
:param algorithm: 加密算法,默认为 'HS256'
|
|
41
|
+
:return: token是否有效
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
jwt.decode(token, secret, algorithms=[algorithm])
|
|
45
|
+
return True
|
|
46
|
+
except jwt.PyJWTError:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_payload(token: str) -> dict:
|
|
51
|
+
"""获取token的payload(不验证签名)
|
|
52
|
+
|
|
53
|
+
:param token: JWT token字符串
|
|
54
|
+
:return: 解析后的载荷数据
|
|
55
|
+
:raises jwt.DecodeError: token格式无效
|
|
56
|
+
"""
|
|
57
|
+
return jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256", "HS384", "HS512"])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Properties文件工具类"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PropsUtil:
|
|
9
|
+
"""Properties文件工具类
|
|
10
|
+
|
|
11
|
+
加载和解析标准 .properties 格式的配置文件。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def load(path: str, charset: str = "utf-8") -> dict:
|
|
16
|
+
"""加载.properties文件
|
|
17
|
+
|
|
18
|
+
支持的格式:
|
|
19
|
+
- key=value
|
|
20
|
+
- key: value
|
|
21
|
+
- key value
|
|
22
|
+
- # 注释行
|
|
23
|
+
- ! 注释行
|
|
24
|
+
- 续行符(行尾 \\)
|
|
25
|
+
|
|
26
|
+
:param path: .properties文件路径
|
|
27
|
+
:param charset: 文件编码,默认为 'utf-8'
|
|
28
|
+
:return: 解析后的属性字典
|
|
29
|
+
"""
|
|
30
|
+
path = os.path.abspath(path)
|
|
31
|
+
props: dict = {}
|
|
32
|
+
with open(path, encoding=charset) as f:
|
|
33
|
+
lines = f.readlines()
|
|
34
|
+
|
|
35
|
+
continued_line = ""
|
|
36
|
+
for line in lines:
|
|
37
|
+
line = line.strip()
|
|
38
|
+
# 跳过空行和注释
|
|
39
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# 处理续行
|
|
43
|
+
if continued_line:
|
|
44
|
+
line = continued_line + line
|
|
45
|
+
continued_line = ""
|
|
46
|
+
|
|
47
|
+
if line.endswith("\\"):
|
|
48
|
+
continued_line = line[:-1]
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# 解析 key=value 或 key: value 或 key value
|
|
52
|
+
match = re.match(r"^([^=: \t]+)[ \t]*[=: \t](.*)$", line)
|
|
53
|
+
if match:
|
|
54
|
+
key = match.group(1).strip()
|
|
55
|
+
value = match.group(2).strip()
|
|
56
|
+
props[key] = PropsUtil._unescape(value)
|
|
57
|
+
|
|
58
|
+
return props
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def get(props: dict, key: str, default: Any = None) -> Any:
|
|
62
|
+
"""获取属性值
|
|
63
|
+
|
|
64
|
+
:param props: 属性字典
|
|
65
|
+
:param key: 属性键
|
|
66
|
+
:param default: 默认值
|
|
67
|
+
:return: 属性值,不存在时返回默认值
|
|
68
|
+
"""
|
|
69
|
+
return props.get(key, default)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _unescape(value: str) -> str:
|
|
73
|
+
"""反转义properties值中的特殊字符
|
|
74
|
+
|
|
75
|
+
:param value: 原始值
|
|
76
|
+
:return: 反转义后的值
|
|
77
|
+
"""
|
|
78
|
+
result = value.replace("\\n", "\n").replace("\\t", "\t").replace("\\\\", "\\")
|
|
79
|
+
return result
|