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.
Files changed (89) hide show
  1. hutool/__init__.py +174 -0
  2. hutool/cache/__init__.py +7 -0
  3. hutool/cache/cache_util.py +47 -0
  4. hutool/cache/fifo_cache.py +87 -0
  5. hutool/cache/lfu_cache.py +129 -0
  6. hutool/cache/lru_cache.py +93 -0
  7. hutool/cache/timed_cache.py +115 -0
  8. hutool/captcha/__init__.py +3 -0
  9. hutool/captcha/captcha_util.py +215 -0
  10. hutool/core/__init__.py +23 -0
  11. hutool/core/_base.py +61 -0
  12. hutool/core/bean.py +214 -0
  13. hutool/core/codec.py +111 -0
  14. hutool/core/coll.py +635 -0
  15. hutool/core/date.py +1024 -0
  16. hutool/core/exceptions.py +66 -0
  17. hutool/core/io/__init__.py +0 -0
  18. hutool/core/io/data_size_util.py +79 -0
  19. hutool/core/io/file_name_util.py +111 -0
  20. hutool/core/io/file_util.py +650 -0
  21. hutool/core/io/io_util.py +133 -0
  22. hutool/core/io/path_util.py +247 -0
  23. hutool/core/io/resource_util.py +137 -0
  24. hutool/core/map.py +933 -0
  25. hutool/core/math_util.py +105 -0
  26. hutool/core/net.py +288 -0
  27. hutool/core/text/__init__.py +0 -0
  28. hutool/core/text/csv_util.py +54 -0
  29. hutool/core/text/str_builder.py +224 -0
  30. hutool/core/text/unicode_util.py +58 -0
  31. hutool/core/tree.py +242 -0
  32. hutool/core/util/__init__.py +63 -0
  33. hutool/core/util/array_util.py +503 -0
  34. hutool/core/util/boolean_util.py +124 -0
  35. hutool/core/util/charset_util.py +60 -0
  36. hutool/core/util/class_util.py +136 -0
  37. hutool/core/util/coordinate_util.py +186 -0
  38. hutool/core/util/credit_code_util.py +110 -0
  39. hutool/core/util/desensitized_util.py +194 -0
  40. hutool/core/util/enum_util.py +94 -0
  41. hutool/core/util/escape_util.py +97 -0
  42. hutool/core/util/hash_util.py +243 -0
  43. hutool/core/util/hex_util.py +140 -0
  44. hutool/core/util/id_util.py +147 -0
  45. hutool/core/util/idcard_util.py +300 -0
  46. hutool/core/util/number_util.py +720 -0
  47. hutool/core/util/object_util.py +294 -0
  48. hutool/core/util/page_util.py +61 -0
  49. hutool/core/util/phone_util.py +140 -0
  50. hutool/core/util/random_util.py +112 -0
  51. hutool/core/util/re_util.py +231 -0
  52. hutool/core/util/reflect_util.py +135 -0
  53. hutool/core/util/runtime_util.py +89 -0
  54. hutool/core/util/str_util.py +2320 -0
  55. hutool/core/util/system_util.py +62 -0
  56. hutool/core/util/url_util.py +232 -0
  57. hutool/core/util/version_util.py +41 -0
  58. hutool/core/util/xml_util.py +158 -0
  59. hutool/core/util/zip_util.py +126 -0
  60. hutool/cron/__init__.py +4 -0
  61. hutool/cron/cron_pattern.py +123 -0
  62. hutool/cron/cron_util.py +115 -0
  63. hutool/crypto/__init__.py +5 -0
  64. hutool/crypto/digest_util.py +167 -0
  65. hutool/crypto/secure_util.py +311 -0
  66. hutool/crypto/sign_util.py +74 -0
  67. hutool/dfa/__init__.py +3 -0
  68. hutool/dfa/sensitive_util.py +114 -0
  69. hutool/extra/__init__.py +6 -0
  70. hutool/extra/emoji_util.py +90 -0
  71. hutool/extra/pinyin_util.py +44 -0
  72. hutool/extra/qr_code_util.py +58 -0
  73. hutool/extra/template_util.py +41 -0
  74. hutool/http/__init__.py +6 -0
  75. hutool/http/html_util.py +88 -0
  76. hutool/http/http_request.py +188 -0
  77. hutool/http/http_response.py +139 -0
  78. hutool/http/http_util.py +237 -0
  79. hutool/json_util.py +251 -0
  80. hutool/jwt_util.py +57 -0
  81. hutool/setting/__init__.py +5 -0
  82. hutool/setting/props_util.py +79 -0
  83. hutool/setting/setting_util.py +80 -0
  84. hutool/setting/yaml_util.py +45 -0
  85. hutool_python-1.0.0.dist-info/LICENSE +127 -0
  86. hutool_python-1.0.0.dist-info/METADATA +438 -0
  87. hutool_python-1.0.0.dist-info/RECORD +89 -0
  88. hutool_python-1.0.0.dist-info/WHEEL +5 -0
  89. hutool_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,311 @@
1
+ import os
2
+ from typing import Optional, Tuple, Union
3
+
4
+ from cryptography.hazmat.backends import default_backend
5
+ from cryptography.hazmat.primitives import hashes, serialization
6
+ from cryptography.hazmat.primitives import padding as sym_padding
7
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+
10
+
11
+ class SecureUtil:
12
+ """加密工具类
13
+
14
+ 提供常用对称/非对称加密算法封装:
15
+ - AES(CBC/ECB/CTR)
16
+ - DES(CBC/ECB)
17
+ - RSA(加密/解密/签名/验签)
18
+ """
19
+
20
+ # ------------------------------------------------------------------ #
21
+ # 内部辅助
22
+ # ------------------------------------------------------------------ #
23
+
24
+ @staticmethod
25
+ def _ensure_bytes(data: Union[str, bytes]) -> bytes:
26
+ """将输入统一转为 bytes"""
27
+ if isinstance(data, str):
28
+ return data.encode("utf-8")
29
+ return data
30
+
31
+ # ------------------------------------------------------------------ #
32
+ # AES
33
+ # ------------------------------------------------------------------ #
34
+
35
+ @staticmethod
36
+ def generate_aes_key(key_size: int = 128) -> bytes:
37
+ """生成AES密钥
38
+
39
+ :param key_size: 密钥位数,支持 128、192、256
40
+ :return: 随机密钥 bytes
41
+ """
42
+ if key_size not in (128, 192, 256):
43
+ raise ValueError("AES密钥长度必须为128、192或256位")
44
+ return os.urandom(key_size // 8)
45
+
46
+ @staticmethod
47
+ def encrypt_aes(data: bytes, key: bytes, mode: str = "CBC", iv: Optional[bytes] = None) -> bytes:
48
+ """AES加密
49
+
50
+ :param data: 待加密数据(明文)
51
+ :param key: AES密钥(16/24/32字节)
52
+ :param mode: 加密模式,支持 CBC / ECB / CTR
53
+ :param iv: 初始化向量(CBC/CTR模式必填,ECB模式忽略)
54
+ :return: 密文 bytes(CBC/CTR模式前16字节为IV)
55
+ """
56
+ if len(key) not in (16, 24, 32):
57
+ raise ValueError("AES密钥长度必须为16、24或32字节")
58
+
59
+ mode_upper = mode.upper()
60
+ if mode_upper == "CBC":
61
+ if iv is None:
62
+ iv = os.urandom(16)
63
+ cipher_mode = modes.CBC(iv)
64
+ elif mode_upper == "ECB":
65
+ iv = b""
66
+ cipher_mode = modes.ECB()
67
+ elif mode_upper == "CTR":
68
+ if iv is None:
69
+ iv = os.urandom(16)
70
+ cipher_mode = modes.CTR(iv)
71
+ else:
72
+ raise ValueError(f"不支持的AES模式: {mode}")
73
+
74
+ # PKCS7 填充(CTR模式无需填充)
75
+ if mode_upper != "CTR":
76
+ padder = sym_padding.PKCS7(128).padder()
77
+ data = padder.update(data) + padder.finalize()
78
+
79
+ cipher = Cipher(algorithms.AES(key), cipher_mode, backend=default_backend())
80
+ encryptor = cipher.encryptor()
81
+ ciphertext = encryptor.update(data) + encryptor.finalize()
82
+
83
+ # 将IV/nonce拼接在密文前(ECB模式除外)
84
+ if mode_upper == "ECB":
85
+ return ciphertext
86
+ return iv + ciphertext
87
+
88
+ @staticmethod
89
+ def decrypt_aes(data: bytes, key: bytes, mode: str = "CBC", iv: Optional[bytes] = None) -> bytes:
90
+ """AES解密
91
+
92
+ :param data: 密文 bytes(如果未提供iv,CBC/CTR模式会从前16字节读取IV)
93
+ :param key: AES密钥
94
+ :param mode: 加密模式,支持 CBC / ECB / CTR
95
+ :param iv: 初始化向量(可选,不提供时从密文前缀读取)
96
+ :return: 解密后的明文 bytes
97
+ """
98
+ if len(key) not in (16, 24, 32):
99
+ raise ValueError("AES密钥长度必须为16、24或32字节")
100
+
101
+ mode_upper = mode.upper()
102
+ if mode_upper == "CBC":
103
+ if iv is None:
104
+ iv, data = data[:16], data[16:]
105
+ cipher_mode = modes.CBC(iv)
106
+ elif mode_upper == "ECB":
107
+ cipher_mode = modes.ECB()
108
+ elif mode_upper == "CTR":
109
+ if iv is None:
110
+ iv, data = data[:16], data[16:]
111
+ cipher_mode = modes.CTR(iv)
112
+ else:
113
+ raise ValueError(f"不支持的AES模式: {mode}")
114
+
115
+ cipher = Cipher(algorithms.AES(key), cipher_mode, backend=default_backend())
116
+ decryptor = cipher.decryptor()
117
+ plaintext = decryptor.update(data) + decryptor.finalize()
118
+
119
+ # 去除PKCS7填充(CTR模式无需去除)
120
+ if mode_upper != "CTR":
121
+ unpadder = sym_padding.PKCS7(128).unpadder()
122
+ plaintext = unpadder.update(plaintext) + unpadder.finalize()
123
+
124
+ return plaintext
125
+
126
+ # ------------------------------------------------------------------ #
127
+ # DES
128
+ # ------------------------------------------------------------------ #
129
+
130
+ @staticmethod
131
+ def generate_des_key() -> bytes:
132
+ """生成DES密钥(8字节)"""
133
+ return os.urandom(8)
134
+
135
+ @staticmethod
136
+ def encrypt_des(data: bytes, key: bytes, mode: str = "CBC", iv: Optional[bytes] = None) -> bytes:
137
+ """DES加密
138
+
139
+ :param data: 待加密数据
140
+ :param key: DES密钥(8字节)
141
+ :param mode: 加密模式,支持 CBC / ECB
142
+ :param iv: 初始化向量(CBC模式必填或自动生成)
143
+ :return: 密文 bytes(CBC模式前8字节为IV)
144
+ """
145
+ if len(key) != 8:
146
+ raise ValueError("DES密钥长度必须为8字节")
147
+
148
+ mode_upper = mode.upper()
149
+ if mode_upper == "CBC":
150
+ if iv is None:
151
+ iv = os.urandom(8)
152
+ cipher_mode = modes.CBC(iv)
153
+ elif mode_upper == "ECB":
154
+ iv = b""
155
+ cipher_mode = modes.ECB()
156
+ else:
157
+ raise ValueError(f"不支持的DES模式: {mode}")
158
+
159
+ # PKCS7 填充(DES块大小64位=8字节)
160
+ padder = sym_padding.PKCS7(64).padder()
161
+ data = padder.update(data) + padder.finalize()
162
+
163
+ cipher = Cipher(algorithms.TripleDES(key * 3 if len(key) == 8 else key), cipher_mode, backend=default_backend())
164
+ encryptor = cipher.encryptor()
165
+ ciphertext = encryptor.update(data) + encryptor.finalize()
166
+
167
+ if mode_upper == "ECB":
168
+ return ciphertext
169
+ return iv + ciphertext
170
+
171
+ @staticmethod
172
+ def decrypt_des(data: bytes, key: bytes, mode: str = "CBC", iv: Optional[bytes] = None) -> bytes:
173
+ """DES解密
174
+
175
+ :param data: 密文 bytes(如果未提供iv,CBC模式会从前8字节读取IV)
176
+ :param key: DES密钥(8字节)
177
+ :param mode: 加密模式,支持 CBC / ECB
178
+ :param iv: 初始化向量(可选)
179
+ :return: 解密后的明文 bytes
180
+ """
181
+ if len(key) != 8:
182
+ raise ValueError("DES密钥长度必须为8字节")
183
+
184
+ mode_upper = mode.upper()
185
+ if mode_upper == "CBC":
186
+ if iv is None:
187
+ iv, data = data[:8], data[8:]
188
+ cipher_mode = modes.CBC(iv)
189
+ elif mode_upper == "ECB":
190
+ cipher_mode = modes.ECB()
191
+ else:
192
+ raise ValueError(f"不支持的DES模式: {mode}")
193
+
194
+ cipher = Cipher(algorithms.TripleDES(key * 3 if len(key) == 8 else key), cipher_mode, backend=default_backend())
195
+ decryptor = cipher.decryptor()
196
+ plaintext = decryptor.update(data) + decryptor.finalize()
197
+
198
+ unpadder = sym_padding.PKCS7(64).unpadder()
199
+ plaintext = unpadder.update(plaintext) + unpadder.finalize()
200
+
201
+ return plaintext
202
+
203
+ # ------------------------------------------------------------------ #
204
+ # RSA
205
+ # ------------------------------------------------------------------ #
206
+
207
+ @staticmethod
208
+ def generate_rsa_key_pair(key_size: int = 2048) -> Tuple[bytes, bytes]:
209
+ """生成RSA密钥对
210
+
211
+ :param key_size: 密钥位数,默认2048
212
+ :return: (public_key_pem, private_key_pem) 元组
213
+ """
214
+ private_key = rsa.generate_private_key(
215
+ public_exponent=65537,
216
+ key_size=key_size,
217
+ backend=default_backend(),
218
+ )
219
+ public_key = private_key.public_key()
220
+
221
+ public_pem = public_key.public_bytes(
222
+ encoding=serialization.Encoding.PEM,
223
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
224
+ )
225
+ private_pem = private_key.private_bytes(
226
+ encoding=serialization.Encoding.PEM,
227
+ format=serialization.PrivateFormat.PKCS8,
228
+ encryption_algorithm=serialization.NoEncryption(),
229
+ )
230
+ return public_pem, private_pem
231
+
232
+ @staticmethod
233
+ def encrypt_rsa(data: bytes, public_key_pem: bytes) -> bytes:
234
+ """RSA公钥加密
235
+
236
+ :param data: 待加密数据(长度受密钥大小限制)
237
+ :param public_key_pem: PEM格式公钥
238
+ :return: 密文 bytes
239
+ """
240
+ public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
241
+ ciphertext = public_key.encrypt(
242
+ data,
243
+ padding.OAEP(
244
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
245
+ algorithm=hashes.SHA256(),
246
+ label=None,
247
+ ),
248
+ )
249
+ return ciphertext
250
+
251
+ @staticmethod
252
+ def decrypt_rsa(data: bytes, private_key_pem: bytes) -> bytes:
253
+ """RSA私钥解密
254
+
255
+ :param data: 密文 bytes
256
+ :param private_key_pem: PEM格式私钥
257
+ :return: 明文 bytes
258
+ """
259
+ private_key = serialization.load_pem_private_key(private_key_pem, password=None, backend=default_backend())
260
+ plaintext = private_key.decrypt(
261
+ data,
262
+ padding.OAEP(
263
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
264
+ algorithm=hashes.SHA256(),
265
+ label=None,
266
+ ),
267
+ )
268
+ return plaintext
269
+
270
+ @staticmethod
271
+ def sign_with_rsa(data: bytes, private_key_pem: bytes) -> bytes:
272
+ """RSA私钥签名
273
+
274
+ :param data: 待签名数据
275
+ :param private_key_pem: PEM格式私钥
276
+ :return: 签名 bytes
277
+ """
278
+ private_key = serialization.load_pem_private_key(private_key_pem, password=None, backend=default_backend())
279
+ signature = private_key.sign(
280
+ data,
281
+ padding.PSS(
282
+ mgf=padding.MGF1(hashes.SHA256()),
283
+ salt_length=padding.PSS.MAX_LENGTH,
284
+ ),
285
+ hashes.SHA256(),
286
+ )
287
+ return signature
288
+
289
+ @staticmethod
290
+ def verify_with_rsa(data: bytes, signature: bytes, public_key_pem: bytes) -> bool:
291
+ """RSA公钥验签
292
+
293
+ :param data: 原始数据
294
+ :param signature: 签名 bytes
295
+ :param public_key_pem: PEM格式公钥
296
+ :return: 验签是否通过
297
+ """
298
+ public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
299
+ try:
300
+ public_key.verify(
301
+ signature,
302
+ data,
303
+ padding.PSS(
304
+ mgf=padding.MGF1(hashes.SHA256()),
305
+ salt_length=padding.PSS.MAX_LENGTH,
306
+ ),
307
+ hashes.SHA256(),
308
+ )
309
+ return True
310
+ except Exception:
311
+ return False
@@ -0,0 +1,74 @@
1
+ from typing import Dict
2
+
3
+ from .digest_util import DigestUtil
4
+
5
+
6
+ class SignUtil:
7
+ """签名工具类
8
+
9
+ 提供常用的参数签名算法,适用于接口签名、防篡改等场景。
10
+ 签名流程:
11
+ 1. 将参数按 key 的字典序排序
12
+ 2. 拼接为 key1=value1&key2=value2&secret=xxx 格式
13
+ 3. 使用指定算法计算摘要
14
+ """
15
+
16
+ # 支持的算法映射
17
+ _ALGORITHM_MAP = {
18
+ "md5": (DigestUtil.md5_hex, False),
19
+ "sha1": (DigestUtil.sha1_hex, False),
20
+ "sha256": (DigestUtil.sha256_hex, False),
21
+ "hmac_md5": (DigestUtil.hmac_md5_hex, True),
22
+ "hmac_sha1": (DigestUtil.hmac_sha1_hex, True),
23
+ "hmac_sha256": (DigestUtil.hmac_sha256_hex, True),
24
+ }
25
+
26
+ @staticmethod
27
+ def sign_params(params: Dict[str, str], secret: str, algorithm: str = "md5") -> str:
28
+ """对参数进行签名
29
+
30
+ 签名步骤:
31
+ 1. 将参数按 key 字典序升序排序
32
+ 2. 拼接为 key1=value1&key2=value2&secret=xxx 形式
33
+ 3. 用指定算法计算摘要并返回十六进制字符串
34
+
35
+ :param params: 待签名的参数字典
36
+ :param secret: 密钥
37
+ :param algorithm: 签名算法,支持 md5 / sha1 / sha256 / hmac_md5 / hmac_sha1 / hmac_sha256
38
+ :return: 签名后的十六进制字符串
39
+ :raises ValueError: 不支持的算法时抛出
40
+ """
41
+ algo_lower = algorithm.lower()
42
+ if algo_lower not in SignUtil._ALGORITHM_MAP:
43
+ raise ValueError(f"不支持的签名算法: {algorithm},支持: {', '.join(SignUtil._ALGORITHM_MAP.keys())}")
44
+
45
+ # 1. 按 key 排序,过滤掉值为 None 的参数
46
+ sorted_keys = sorted(params.keys())
47
+
48
+ # 2. 拼接
49
+ parts = []
50
+ for key in sorted_keys:
51
+ value = params[key]
52
+ if value is not None:
53
+ parts.append(f"{key}={value}")
54
+ parts.append(f"secret={secret}")
55
+ sign_str = "&".join(parts)
56
+
57
+ # 3. 计算摘要
58
+ func, is_hmac = SignUtil._ALGORITHM_MAP[algo_lower]
59
+ if is_hmac:
60
+ return func(sign_str, secret)
61
+ return func(sign_str)
62
+
63
+ @staticmethod
64
+ def sort_sign(params: Dict[str, str], secret: str, algorithm: str = "md5") -> str:
65
+ """排序签名(功能同 sign_params)
66
+
67
+ 这是 sign_params 的别名,方便不同调用场景使用。
68
+
69
+ :param params: 待签名的参数字典
70
+ :param secret: 密钥
71
+ :param algorithm: 签名算法
72
+ :return: 签名后的十六进制字符串
73
+ """
74
+ return SignUtil.sign_params(params, secret, algorithm)
hutool/dfa/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .sensitive_util import SensitiveUtil
2
+
3
+ __all__ = ["SensitiveUtil"]
@@ -0,0 +1,114 @@
1
+ """敏感词过滤工具,基于DFA(确定性有限自动机)算法"""
2
+
3
+ from typing import List
4
+
5
+
6
+ class SensitiveUtil:
7
+ """敏感词过滤工具,基于DFA(确定性有限自动机)算法"""
8
+
9
+ _root: dict = {}
10
+ _initialized: bool = False
11
+
12
+ @classmethod
13
+ def init(cls, words: List[str]) -> None:
14
+ """初始化敏感词库
15
+
16
+ :param words: 敏感词列表
17
+ """
18
+ cls._root = {}
19
+ for word in words:
20
+ node = cls._root
21
+ for char in word:
22
+ if char not in node:
23
+ node[char] = {}
24
+ node = node[char]
25
+ # 标记词结束
26
+ node["__end__"] = True
27
+ cls._initialized = True
28
+
29
+ @classmethod
30
+ def contains(cls, text: str) -> bool:
31
+ """是否包含敏感词
32
+
33
+ :param text: 待检测文本
34
+ :return: 是否包含敏感词
35
+ """
36
+ if not cls._initialized:
37
+ return False
38
+ for i in range(len(text)):
39
+ node = cls._root
40
+ j = i
41
+ while j < len(text) and text[j] in node:
42
+ node = node[text[j]]
43
+ if node.get("__end__"):
44
+ return True
45
+ j += 1
46
+ return False
47
+
48
+ @classmethod
49
+ def find_first(cls, text: str) -> str:
50
+ """查找第一个敏感词
51
+
52
+ :param text: 待检测文本
53
+ :return: 第一个匹配的敏感词,未找到返回空字符串
54
+ """
55
+ if not cls._initialized:
56
+ return ""
57
+ for i in range(len(text)):
58
+ node = cls._root
59
+ j = i
60
+ while j < len(text) and text[j] in node:
61
+ node = node[text[j]]
62
+ if node.get("__end__"):
63
+ return text[i : j + 1]
64
+ j += 1
65
+ return ""
66
+
67
+ @classmethod
68
+ def find_all(cls, text: str) -> List[str]:
69
+ """查找所有敏感词
70
+
71
+ :param text: 待检测文本
72
+ :return: 所有匹配的敏感词列表
73
+ """
74
+ if not cls._initialized:
75
+ return []
76
+ result: List[str] = []
77
+ for i in range(len(text)):
78
+ node = cls._root
79
+ j = i
80
+ while j < len(text) and text[j] in node:
81
+ node = node[text[j]]
82
+ if node.get("__end__"):
83
+ result.append(text[i : j + 1])
84
+ j += 1
85
+ return result
86
+
87
+ @classmethod
88
+ def replace(cls, text: str, replace_char: str = "*") -> str:
89
+ """替换敏感词
90
+
91
+ :param text: 待处理文本
92
+ :param replace_char: 替换字符,默认为 '*'
93
+ :return: 替换后的文本
94
+ """
95
+ if not cls._initialized:
96
+ return text
97
+ chars = list(text)
98
+ i = 0
99
+ while i < len(chars):
100
+ node = cls._root
101
+ j = i
102
+ match_end = -1
103
+ while j < len(chars) and chars[j] in node:
104
+ node = node[chars[j]]
105
+ if node.get("__end__"):
106
+ match_end = j
107
+ j += 1
108
+ if match_end >= 0:
109
+ for k in range(i, match_end + 1):
110
+ chars[k] = replace_char
111
+ i = match_end + 1
112
+ else:
113
+ i += 1
114
+ return "".join(chars)
@@ -0,0 +1,6 @@
1
+ from .emoji_util import EmojiUtil
2
+ from .pinyin_util import PinyinUtil
3
+ from .qr_code_util import QrCodeUtil
4
+ from .template_util import TemplateUtil
5
+
6
+ __all__ = ["EmojiUtil", "PinyinUtil", "QrCodeUtil", "TemplateUtil"]
@@ -0,0 +1,90 @@
1
+ """Emoji工具类"""
2
+
3
+ import re
4
+
5
+ # 匹配emoji的正则表达式
6
+ _EMOJI_PATTERN = re.compile(
7
+ "["
8
+ "\U0001f600-\U0001f64f" # 表情符号
9
+ "\U0001f300-\U0001f5ff" # 符号和象形文字
10
+ "\U0001f680-\U0001f6ff" # 交通和地图符号
11
+ "\U0001f1e0-\U0001f1ff" # 旗帜
12
+ "\U00002702-\U000027b0" # 杂项符号
13
+ "\U000024c2-\U0001f251" # 封闭字符
14
+ "\U0001f926-\U0001f937" # 补充表情
15
+ "\U00010000-\U0010ffff" # 补充平面
16
+ "♀-♂" # 性别符号
17
+ "☀-⭕" # 杂项符号
18
+ "‍" # 零宽连接符
19
+ "⏏"
20
+ "⏩-⏳"
21
+ "⏸-⏺"
22
+ "️" # 变体选择符
23
+ "⤴-⤵"
24
+ "▪-◾"
25
+ "⬅-⬇"
26
+ "⬛-⬜"
27
+ "⭐"
28
+ "〰"
29
+ "〽"
30
+ "㊗"
31
+ "㊙"
32
+ "]+",
33
+ flags=re.UNICODE,
34
+ )
35
+
36
+
37
+ class EmojiUtil:
38
+ """Emoji工具类"""
39
+
40
+ @staticmethod
41
+ def contains_emoji(str_val: str) -> bool:
42
+ """判断是否包含emoji
43
+
44
+ :param str_val: 待检测字符串
45
+ :return: 是否包含emoji
46
+ """
47
+ return bool(_EMOJI_PATTERN.search(str_val))
48
+
49
+ @staticmethod
50
+ def emoji_to_unicode(emoji_str: str) -> str:
51
+ """将emoji转换为Unicode编码
52
+
53
+ :param emoji_str: 包含emoji的字符串
54
+ :return: Unicode编码字符串(格式:\\U0001F600)
55
+ """
56
+ result = []
57
+ for char in emoji_str:
58
+ code_point = ord(char)
59
+ if code_point > 0xFFFF:
60
+ result.append(f"\\U{code_point:08X}")
61
+ elif code_point > 0xFF:
62
+ result.append(f"\\u{code_point:04X}")
63
+ else:
64
+ result.append(char)
65
+ return "".join(result)
66
+
67
+ @staticmethod
68
+ def unicode_to_emoji(unicode_str: str) -> str:
69
+ """将Unicode编码转换为emoji
70
+
71
+ :param unicode_str: Unicode编码字符串(格式:\\U0001F600 或 \\u2764)
72
+ :return: 包含emoji的字符串
73
+ """
74
+
75
+ def _replace(match: re.Match) -> str:
76
+ code_point = int(match.group(1), 16)
77
+ return chr(code_point)
78
+
79
+ # 匹配 \\U 开头的8位和 \\u 开头的4位十六进制
80
+ result = re.sub(r"\\[Uu]([0-9a-fA-F]{4,8})", _replace, unicode_str)
81
+ return result
82
+
83
+ @staticmethod
84
+ def remove_emojis(str_val: str) -> str:
85
+ """移除字符串中的所有emoji
86
+
87
+ :param str_val: 待处理字符串
88
+ :return: 移除emoji后的字符串
89
+ """
90
+ return _EMOJI_PATTERN.sub("", str_val)
@@ -0,0 +1,44 @@
1
+ """拼音工具类,基于pypinyin"""
2
+
3
+
4
+
5
+ class PinyinUtil:
6
+ """拼音工具类,基于pypinyin"""
7
+
8
+ @staticmethod
9
+ def get_pinyin(str_val: str, separator: str = "") -> str:
10
+ """获取拼音
11
+
12
+ :param str_val: 中文字符串
13
+ :param separator: 分隔符,默认为空字符串
14
+ :return: 拼音字符串
15
+ """
16
+ from pypinyin import Style, pinyin
17
+
18
+ result = pinyin(str_val, style=Style.TONE, errors="ignore")
19
+ return separator.join([item[0] for item in result if item])
20
+
21
+ @staticmethod
22
+ def get_pinyin_first_letter(str_val: str) -> str:
23
+ """获取拼音首字母
24
+
25
+ :param str_val: 中文字符串
26
+ :return: 拼音首字母字符串
27
+ """
28
+ from pypinyin import Style, lazy_pinyin
29
+
30
+ result = lazy_pinyin(str_val, style=Style.FIRST_LETTER, errors="ignore")
31
+ return "".join(result)
32
+
33
+ @staticmethod
34
+ def get_full_pinyin(str_val: str, separator: str = " ") -> str:
35
+ """获取全拼
36
+
37
+ :param str_val: 中文字符串
38
+ :param separator: 分隔符,默认为空格
39
+ :return: 全拼字符串
40
+ """
41
+ from pypinyin import Style, lazy_pinyin
42
+
43
+ result = lazy_pinyin(str_val, style=Style.TONE, errors="ignore")
44
+ return separator.join(result)