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,136 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any, List, Type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ClassUtil:
|
|
6
|
+
"""类工具类
|
|
7
|
+
|
|
8
|
+
提供对Python类和对象的常用操作方法,包括获取类名、模块名、类型判断等。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def get_class_name(obj: Any, is_simple: bool = True) -> str:
|
|
13
|
+
"""获取类名
|
|
14
|
+
|
|
15
|
+
:param obj: 对象或类
|
|
16
|
+
:param is_simple: 是否返回简单类名(不含模块路径),默认True
|
|
17
|
+
:return: 类名字符串
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(obj, type):
|
|
20
|
+
cls = obj
|
|
21
|
+
else:
|
|
22
|
+
cls = type(obj)
|
|
23
|
+
|
|
24
|
+
if is_simple:
|
|
25
|
+
return cls.__name__
|
|
26
|
+
else:
|
|
27
|
+
module = cls.__module__
|
|
28
|
+
if module == "builtins":
|
|
29
|
+
return cls.__name__
|
|
30
|
+
return f"{module}.{cls.__name__}"
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_package_name(obj: Any) -> str:
|
|
34
|
+
"""获取模块名
|
|
35
|
+
|
|
36
|
+
:param obj: 对象或类
|
|
37
|
+
:return: 模块名字符串
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(obj, type):
|
|
40
|
+
cls = obj
|
|
41
|
+
else:
|
|
42
|
+
cls = type(obj)
|
|
43
|
+
|
|
44
|
+
return cls.__module__
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def is_basic_type(obj: Any) -> bool:
|
|
48
|
+
"""是否为基本类型
|
|
49
|
+
|
|
50
|
+
基本类型包括:int, float, str, bool, bytes, complex, type(None)
|
|
51
|
+
|
|
52
|
+
:param obj: 待检查的对象
|
|
53
|
+
:return: 是否为基本类型
|
|
54
|
+
"""
|
|
55
|
+
basic_types = (int, float, str, bool, bytes, complex, type(None))
|
|
56
|
+
return isinstance(obj, basic_types)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def is_instance(obj: Any, class_or_tuple: Any) -> bool:
|
|
60
|
+
"""是否为指定类型的实例
|
|
61
|
+
|
|
62
|
+
:param obj: 待检查的对象
|
|
63
|
+
:param class_or_tuple: 类或类的元组
|
|
64
|
+
:return: 是否为指定类型的实例
|
|
65
|
+
"""
|
|
66
|
+
return isinstance(obj, class_or_tuple)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def get_public_fields(obj: Any) -> List[str]:
|
|
70
|
+
"""获取公开字段
|
|
71
|
+
|
|
72
|
+
返回对象中不以单下划线开头的属性名称列表。
|
|
73
|
+
|
|
74
|
+
:param obj: 对象
|
|
75
|
+
:return: 公开字段名列表
|
|
76
|
+
"""
|
|
77
|
+
return [attr for attr in dir(obj) if not attr.startswith("_") and not callable(getattr(obj, attr, None))]
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def get_methods(obj: Any) -> List[str]:
|
|
81
|
+
"""获取方法列表
|
|
82
|
+
|
|
83
|
+
返回对象中不以单下划线开头的方法名称列表。
|
|
84
|
+
|
|
85
|
+
:param obj: 对象或类
|
|
86
|
+
:return: 方法名列表
|
|
87
|
+
"""
|
|
88
|
+
return [attr for attr in dir(obj) if not attr.startswith("_") and callable(getattr(obj, attr, None))]
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def has_method(obj: Any, method_name: str) -> bool:
|
|
92
|
+
"""是否有指定方法
|
|
93
|
+
|
|
94
|
+
:param obj: 对象或类
|
|
95
|
+
:param method_name: 方法名
|
|
96
|
+
:return: 是否存在该方法
|
|
97
|
+
"""
|
|
98
|
+
attr = getattr(obj, method_name, None)
|
|
99
|
+
return attr is not None and callable(attr)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def create_instance(class_path: str) -> Any:
|
|
103
|
+
"""通过完整类路径创建实例
|
|
104
|
+
|
|
105
|
+
支持模块级函数和类实例化两种方式。
|
|
106
|
+
例如: 'json.loads', 'datetime.datetime', 'collections.OrderedDict'
|
|
107
|
+
|
|
108
|
+
:param class_path: 完整类路径或函数路径,如 'datetime.datetime'
|
|
109
|
+
:return: 创建的实例或返回的函数结果
|
|
110
|
+
:raises ImportError: 模块导入失败
|
|
111
|
+
:raises AttributeError: 属性不存在
|
|
112
|
+
"""
|
|
113
|
+
parts = class_path.rsplit(".", 1)
|
|
114
|
+
if len(parts) != 2:
|
|
115
|
+
raise ValueError(f"无效的类路径: {class_path},需要包含模块和类名/函数名")
|
|
116
|
+
|
|
117
|
+
module_path, attr_name = parts
|
|
118
|
+
module = importlib.import_module(module_path)
|
|
119
|
+
obj = getattr(module, attr_name)
|
|
120
|
+
|
|
121
|
+
if isinstance(obj, type):
|
|
122
|
+
return obj()
|
|
123
|
+
return obj
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def is_subclass(cls: Type, parent_cls: Type) -> bool:
|
|
127
|
+
"""是否为子类
|
|
128
|
+
|
|
129
|
+
:param cls: 待检查的类
|
|
130
|
+
:param parent_cls: 父类
|
|
131
|
+
:return: 是否为父类的子类
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
return issubclass(cls, parent_cls)
|
|
135
|
+
except TypeError:
|
|
136
|
+
return False
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Coordinate(NamedTuple):
|
|
6
|
+
"""坐标点"""
|
|
7
|
+
|
|
8
|
+
longitude: float
|
|
9
|
+
latitude: float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CoordinateUtil:
|
|
13
|
+
"""坐标工具类,提供WGS84/GCJ02/BD09坐标系转换
|
|
14
|
+
|
|
15
|
+
WGS84: GPS全球定位系统使用的坐标系
|
|
16
|
+
GCJ02: 国测局坐标系(火星坐标系),国内地图服务使用
|
|
17
|
+
BD09: 百度坐标系,在GCJ02基础上进一步加密
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# 常量
|
|
21
|
+
_PI = math.pi
|
|
22
|
+
_A = 6378245.0 # 克拉索夫斯基椭球长半轴(米)
|
|
23
|
+
_EE = 0.00669342162296594 # 偏心率平方
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _out_of_china(lng: float, lat: float) -> bool:
|
|
27
|
+
"""判断是否在中国范围外
|
|
28
|
+
|
|
29
|
+
:param lng: 经度
|
|
30
|
+
:param lat: 纬度
|
|
31
|
+
:return: 是否在中国范围外
|
|
32
|
+
"""
|
|
33
|
+
return not (72.004 <= lng <= 137.8347 and 0.8293 <= lat <= 55.8271)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _transform_lat(lng: float, lat: float) -> float:
|
|
37
|
+
"""纬度转换偏移量计算
|
|
38
|
+
|
|
39
|
+
:param lng: 经度
|
|
40
|
+
:param lat: 纬度
|
|
41
|
+
:return: 纬度偏移量
|
|
42
|
+
"""
|
|
43
|
+
pi = CoordinateUtil._PI
|
|
44
|
+
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng))
|
|
45
|
+
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
|
46
|
+
ret += (20.0 * math.sin(lat * pi) + 40.0 * math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
|
|
47
|
+
ret += (160.0 * math.sin(lat / 12.0 * pi) + 320.0 * math.sin(lat * pi / 30.0)) * 2.0 / 3.0
|
|
48
|
+
return ret
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _transform_lng(lng: float, lat: float) -> float:
|
|
52
|
+
"""经度转换偏移量计算
|
|
53
|
+
|
|
54
|
+
:param lng: 经度
|
|
55
|
+
:param lat: 纬度
|
|
56
|
+
:return: 经度偏移量
|
|
57
|
+
"""
|
|
58
|
+
pi = CoordinateUtil._PI
|
|
59
|
+
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(abs(lng))
|
|
60
|
+
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
|
61
|
+
ret += (20.0 * math.sin(lng * pi) + 40.0 * math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
|
|
62
|
+
ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
|
|
63
|
+
return ret
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _delta(lng: float, lat: float) -> tuple:
|
|
67
|
+
"""计算GCJ-02偏移量
|
|
68
|
+
|
|
69
|
+
:param lng: WGS84经度
|
|
70
|
+
:param lat: WGS84纬度
|
|
71
|
+
:return: (经度偏移, 纬度偏移)
|
|
72
|
+
"""
|
|
73
|
+
pi = CoordinateUtil._PI
|
|
74
|
+
a = CoordinateUtil._A
|
|
75
|
+
ee = CoordinateUtil._EE
|
|
76
|
+
dlat = CoordinateUtil._transform_lat(lng - 105.0, lat - 35.0)
|
|
77
|
+
dlng = CoordinateUtil._transform_lng(lng - 105.0, lat - 35.0)
|
|
78
|
+
radlat = lat / 180.0 * pi
|
|
79
|
+
magic = math.sin(radlat)
|
|
80
|
+
magic = 1 - ee * magic * magic
|
|
81
|
+
sqrtmagic = math.sqrt(magic)
|
|
82
|
+
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
|
|
83
|
+
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
|
|
84
|
+
return dlng, dlat
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def wgs84_to_gcj02(lng: float, lat: float) -> Coordinate:
|
|
88
|
+
"""WGS84转GCJ02(GPS转国测局坐标)
|
|
89
|
+
|
|
90
|
+
:param lng: WGS84经度
|
|
91
|
+
:param lat: WGS84纬度
|
|
92
|
+
:return: GCJ02坐标
|
|
93
|
+
"""
|
|
94
|
+
if CoordinateUtil._out_of_china(lng, lat):
|
|
95
|
+
return Coordinate(lng, lat)
|
|
96
|
+
dlng, dlat = CoordinateUtil._delta(lng, lat)
|
|
97
|
+
return Coordinate(lng + dlng, lat + dlat)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def gcj02_to_wgs84(lng: float, lat: float) -> Coordinate:
|
|
101
|
+
"""GCJ02转WGS84
|
|
102
|
+
|
|
103
|
+
:param lng: GCJ02经度
|
|
104
|
+
:param lat: GCJ02纬度
|
|
105
|
+
:return: WGS84坐标
|
|
106
|
+
"""
|
|
107
|
+
if CoordinateUtil._out_of_china(lng, lat):
|
|
108
|
+
return Coordinate(lng, lat)
|
|
109
|
+
dlng, dlat = CoordinateUtil._delta(lng, lat)
|
|
110
|
+
return Coordinate(lng - dlng, lat - dlat)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def gcj02_to_bd09(lng: float, lat: float) -> Coordinate:
|
|
114
|
+
"""GCJ02转BD09(转百度坐标)
|
|
115
|
+
|
|
116
|
+
:param lng: GCJ02经度
|
|
117
|
+
:param lat: GCJ02纬度
|
|
118
|
+
:return: BD09坐标
|
|
119
|
+
"""
|
|
120
|
+
pi = CoordinateUtil._PI
|
|
121
|
+
z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * pi * 3000.0 / 180.0)
|
|
122
|
+
theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * pi * 3000.0 / 180.0)
|
|
123
|
+
bd_lng = z * math.cos(theta) + 0.0065
|
|
124
|
+
bd_lat = z * math.sin(theta) + 0.006
|
|
125
|
+
return Coordinate(bd_lng, bd_lat)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def bd09_to_gcj02(lng: float, lat: float) -> Coordinate:
|
|
129
|
+
"""BD09转GCJ02
|
|
130
|
+
|
|
131
|
+
:param lng: BD09经度
|
|
132
|
+
:param lat: BD09纬度
|
|
133
|
+
:return: GCJ02坐标
|
|
134
|
+
"""
|
|
135
|
+
pi = CoordinateUtil._PI
|
|
136
|
+
x = lng - 0.0065
|
|
137
|
+
y = lat - 0.006
|
|
138
|
+
z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * pi * 3000.0 / 180.0)
|
|
139
|
+
theta = math.atan2(y, x) - 0.000003 * math.cos(x * pi * 3000.0 / 180.0)
|
|
140
|
+
gcj_lng = z * math.cos(theta)
|
|
141
|
+
gcj_lat = z * math.sin(theta)
|
|
142
|
+
return Coordinate(gcj_lng, gcj_lat)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def wgs84_to_bd09(lng: float, lat: float) -> Coordinate:
|
|
146
|
+
"""WGS84转BD09
|
|
147
|
+
|
|
148
|
+
:param lng: WGS84经度
|
|
149
|
+
:param lat: WGS84纬度
|
|
150
|
+
:return: BD09坐标
|
|
151
|
+
"""
|
|
152
|
+
gcj = CoordinateUtil.wgs84_to_gcj02(lng, lat)
|
|
153
|
+
return CoordinateUtil.gcj02_to_bd09(gcj.longitude, gcj.latitude)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def bd09_to_wgs84(lng: float, lat: float) -> Coordinate:
|
|
157
|
+
"""BD09转WGS84
|
|
158
|
+
|
|
159
|
+
:param lng: BD09经度
|
|
160
|
+
:param lat: BD09纬度
|
|
161
|
+
:return: WGS84坐标
|
|
162
|
+
"""
|
|
163
|
+
gcj = CoordinateUtil.bd09_to_gcj02(lng, lat)
|
|
164
|
+
return CoordinateUtil.gcj02_to_wgs84(gcj.longitude, gcj.latitude)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def distance(lng1: float, lat1: float, lng2: float, lat2: float) -> float:
|
|
168
|
+
"""计算两点间距离(米),使用Haversine公式
|
|
169
|
+
|
|
170
|
+
:param lng1: 第一个点的经度
|
|
171
|
+
:param lat1: 第一个点的纬度
|
|
172
|
+
:param lng2: 第二个点的经度
|
|
173
|
+
:param lat2: 第二个点的纬度
|
|
174
|
+
:return: 两点间的距离(米)
|
|
175
|
+
"""
|
|
176
|
+
earth_radius = 6371000.0 # 地球平均半径(米)
|
|
177
|
+
|
|
178
|
+
lat1_rad = math.radians(lat1)
|
|
179
|
+
lat2_rad = math.radians(lat2)
|
|
180
|
+
dlat = math.radians(lat2 - lat1)
|
|
181
|
+
dlng = math.radians(lng2 - lng1)
|
|
182
|
+
|
|
183
|
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlng / 2) ** 2
|
|
184
|
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
185
|
+
|
|
186
|
+
return earth_radius * c
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class CreditCodeUtil:
|
|
4
|
+
"""统一社会信用代码工具类
|
|
5
|
+
|
|
6
|
+
统一社会信用代码共18位,由以下部分组成:
|
|
7
|
+
- 登记管理部门代码(1位)
|
|
8
|
+
- 机构类别代码(1位)
|
|
9
|
+
- 登记管理机关行政区划码(6位)
|
|
10
|
+
- 主体标识码(9位)
|
|
11
|
+
- 校验码(1位)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# 字符到数值的映射
|
|
15
|
+
_CHAR_MAP = "0123456789ABCDEFGHJKLMNPQRTUWXY"
|
|
16
|
+
# 加权因子(前17位)
|
|
17
|
+
_WEIGHT_FACTORS = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28]
|
|
18
|
+
|
|
19
|
+
# 登记管理部门代码
|
|
20
|
+
_DEPT_CODES = {
|
|
21
|
+
"1": "机构编制",
|
|
22
|
+
"2": "外交",
|
|
23
|
+
"3": "司法行政",
|
|
24
|
+
"4": "文化",
|
|
25
|
+
"5": "民政",
|
|
26
|
+
"6": "旅游",
|
|
27
|
+
"7": "宗教",
|
|
28
|
+
"8": "工会",
|
|
29
|
+
"9": "工商",
|
|
30
|
+
"A": "机构编制",
|
|
31
|
+
"B": "外交",
|
|
32
|
+
"C": "机构编制",
|
|
33
|
+
"D": "商务",
|
|
34
|
+
"E": "其他",
|
|
35
|
+
}
|
|
36
|
+
# 机构类别代码对应的管理部门
|
|
37
|
+
_TYPE_CODES = {
|
|
38
|
+
"1": {"1": "机关", "2": "事业单位", "3": "编办直接管理机构编制的群众团体", "9": "其他"},
|
|
39
|
+
"2": {"1": "外国常驻新闻机构", "9": "其他"},
|
|
40
|
+
"3": {
|
|
41
|
+
"1": "律师执业机构",
|
|
42
|
+
"2": "公证处",
|
|
43
|
+
"3": "基层法律服务所",
|
|
44
|
+
"4": "司法鉴定机构",
|
|
45
|
+
"5": "仲裁委员会",
|
|
46
|
+
"9": "其他",
|
|
47
|
+
},
|
|
48
|
+
"4": {"1": "外国在华文化中心", "9": "其他"},
|
|
49
|
+
"5": {"1": "社会团体", "2": "民办非企业单位", "3": "基金会", "9": "其他"},
|
|
50
|
+
"6": {"1": "外国旅游部门常驻代表机构", "2": "港澳台地区旅游部门常驻代表机构", "9": "其他"},
|
|
51
|
+
"7": {"1": "宗教活动场所", "2": "宗教院校", "9": "其他"},
|
|
52
|
+
"8": {"1": "基层工会", "9": "其他"},
|
|
53
|
+
"9": {"1": "企业", "2": "个体工商户", "3": "农民专业合作社"},
|
|
54
|
+
"A": {"1": "中央编办直接管理机构编制的群众团体", "9": "其他"},
|
|
55
|
+
"B": {"1": "外国常驻新闻机构", "9": "其他"},
|
|
56
|
+
"C": {"1": "律师事务所", "9": "其他"},
|
|
57
|
+
"D": {"1": "外国在华文化中心", "9": "其他"},
|
|
58
|
+
"E": {"1": "民政", "9": "其他"},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def is_valid_credit_code(code: str) -> bool:
|
|
63
|
+
"""校验统一社会信用代码是否有效
|
|
64
|
+
|
|
65
|
+
校验规则:
|
|
66
|
+
1. 长度必须为18位
|
|
67
|
+
2. 字符必须在允许的字符集内
|
|
68
|
+
3. 登记管理部门代码和机构类别代码必须合法
|
|
69
|
+
4. 校验码必须正确(使用ISO 7064:1983.MOD 37-2算法)
|
|
70
|
+
|
|
71
|
+
:param code: 待校验的统一社会信用代码
|
|
72
|
+
:return: 是否有效
|
|
73
|
+
"""
|
|
74
|
+
if not code or len(code) != 18:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
code = code.upper()
|
|
78
|
+
|
|
79
|
+
# 检查字符是否在允许的字符集中
|
|
80
|
+
for ch in code:
|
|
81
|
+
if ch not in CreditCodeUtil._CHAR_MAP:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# 检查登记管理部门代码
|
|
85
|
+
dept_code = code[0]
|
|
86
|
+
if dept_code not in CreditCodeUtil._DEPT_CODES:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# 检查机构类别代码
|
|
90
|
+
type_code = code[1]
|
|
91
|
+
if dept_code not in CreditCodeUtil._TYPE_CODES:
|
|
92
|
+
return False
|
|
93
|
+
if type_code not in CreditCodeUtil._TYPE_CODES[dept_code]:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
# 校验第18位校验码
|
|
97
|
+
# 使用ISO 7064:1983.MOD 37-2算法
|
|
98
|
+
check_sum = 0
|
|
99
|
+
for i in range(17):
|
|
100
|
+
char_index = CreditCodeUtil._CHAR_MAP.index(code[i])
|
|
101
|
+
check_sum += char_index * CreditCodeUtil._WEIGHT_FACTORS[i]
|
|
102
|
+
|
|
103
|
+
remainder = check_sum % 31
|
|
104
|
+
if remainder == 0:
|
|
105
|
+
remainder = 31
|
|
106
|
+
|
|
107
|
+
expected_index = 31 - remainder
|
|
108
|
+
expected_char = CreditCodeUtil._CHAR_MAP[expected_index]
|
|
109
|
+
|
|
110
|
+
return code[17] == expected_char
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
class DesensitizedUtil:
|
|
2
|
+
"""数据脱敏工具类"""
|
|
3
|
+
|
|
4
|
+
@staticmethod
|
|
5
|
+
def chinese_name(name: str) -> str:
|
|
6
|
+
"""中文姓名脱敏:保留姓,名替换为*
|
|
7
|
+
|
|
8
|
+
示例:张三 -> 张*,欧阳娜娜 -> 欧***
|
|
9
|
+
|
|
10
|
+
:param name: 中文姓名
|
|
11
|
+
:return: 脱敏后的姓名
|
|
12
|
+
"""
|
|
13
|
+
if not name:
|
|
14
|
+
return ""
|
|
15
|
+
name = name.strip()
|
|
16
|
+
if len(name) <= 1:
|
|
17
|
+
return name
|
|
18
|
+
elif len(name) == 2:
|
|
19
|
+
return name[0] + "*"
|
|
20
|
+
else:
|
|
21
|
+
return name[0] + "*" * (len(name) - 1)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def id_card(idcard: str, before: int = 3, after: int = 4) -> str:
|
|
25
|
+
"""身份证脱敏
|
|
26
|
+
|
|
27
|
+
保留前before位和后after位,中间用*替换
|
|
28
|
+
|
|
29
|
+
:param idcard: 身份证号
|
|
30
|
+
:param before: 保留前N位,默认3
|
|
31
|
+
:param after: 保留后N位,默认4
|
|
32
|
+
:return: 脱敏后的身份证号
|
|
33
|
+
"""
|
|
34
|
+
if not idcard:
|
|
35
|
+
return ""
|
|
36
|
+
idcard = idcard.strip()
|
|
37
|
+
length = len(idcard)
|
|
38
|
+
if before + after >= length:
|
|
39
|
+
return idcard
|
|
40
|
+
return idcard[:before] + "*" * (length - before - after) + idcard[length - after :]
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def mobile_phone(phone: str) -> str:
|
|
44
|
+
"""手机号脱敏:138****1234
|
|
45
|
+
|
|
46
|
+
:param phone: 手机号
|
|
47
|
+
:return: 脱敏后的手机号
|
|
48
|
+
"""
|
|
49
|
+
if not phone:
|
|
50
|
+
return ""
|
|
51
|
+
phone = phone.strip()
|
|
52
|
+
if len(phone) < 7:
|
|
53
|
+
return phone
|
|
54
|
+
return phone[:3] + "****" + phone[7:]
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def fixed_phone(phone: str) -> str:
|
|
58
|
+
"""固定电话脱敏
|
|
59
|
+
|
|
60
|
+
保留区号和后4位,中间用*替换
|
|
61
|
+
|
|
62
|
+
:param phone: 固定电话
|
|
63
|
+
:return: 脱敏后的固定电话
|
|
64
|
+
"""
|
|
65
|
+
if not phone:
|
|
66
|
+
return ""
|
|
67
|
+
phone = phone.strip()
|
|
68
|
+
# 查找分隔符位置(区号后的'-')
|
|
69
|
+
dash_pos = phone.find("-")
|
|
70
|
+
if dash_pos == -1:
|
|
71
|
+
# 无分隔符,按规则:前3位为区号保留,后4位保留
|
|
72
|
+
if len(phone) <= 7:
|
|
73
|
+
return phone[:3] + "****" if len(phone) > 3 else phone
|
|
74
|
+
return phone[:3] + "*" * (len(phone) - 7) + phone[-4:]
|
|
75
|
+
area_code = phone[:dash_pos]
|
|
76
|
+
number = phone[dash_pos + 1 :]
|
|
77
|
+
if len(number) <= 4:
|
|
78
|
+
return area_code + "-" + number
|
|
79
|
+
return area_code + "-" + "*" * (len(number) - 4) + number[-4:]
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def email(email: str) -> str:
|
|
83
|
+
"""邮箱脱敏:t***@example.com
|
|
84
|
+
|
|
85
|
+
保留@前的首字母和@后的完整域名
|
|
86
|
+
|
|
87
|
+
:param email: 邮箱地址
|
|
88
|
+
:return: 脱敏后的邮箱
|
|
89
|
+
"""
|
|
90
|
+
if not email:
|
|
91
|
+
return ""
|
|
92
|
+
email = email.strip()
|
|
93
|
+
at_pos = email.find("@")
|
|
94
|
+
if at_pos <= 0:
|
|
95
|
+
return email
|
|
96
|
+
prefix = email[:at_pos]
|
|
97
|
+
domain = email[at_pos:]
|
|
98
|
+
if len(prefix) <= 1:
|
|
99
|
+
return prefix + "***" + domain
|
|
100
|
+
return prefix[0] + "***" + domain
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def address(address: str, sensitive_size: int = 6) -> str:
|
|
104
|
+
"""地址脱敏
|
|
105
|
+
|
|
106
|
+
从敏感_size位之前开始替换为*
|
|
107
|
+
|
|
108
|
+
:param address: 地址
|
|
109
|
+
:param sensitive_size: 需要脱敏的字符数,默认6
|
|
110
|
+
:return: 脱敏后的地址
|
|
111
|
+
"""
|
|
112
|
+
if not address:
|
|
113
|
+
return ""
|
|
114
|
+
address = address.strip()
|
|
115
|
+
length = len(address)
|
|
116
|
+
if sensitive_size >= length:
|
|
117
|
+
return "*" * length
|
|
118
|
+
return address[: length - sensitive_size] + "*" * sensitive_size
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def bank_card(card: str) -> str:
|
|
122
|
+
"""银行卡脱敏
|
|
123
|
+
|
|
124
|
+
保留前4位和后4位,中间用*替换
|
|
125
|
+
|
|
126
|
+
:param card: 银行卡号
|
|
127
|
+
:return: 脱敏后的银行卡号
|
|
128
|
+
"""
|
|
129
|
+
if not card:
|
|
130
|
+
return ""
|
|
131
|
+
card = card.strip().replace(" ", "")
|
|
132
|
+
if len(card) <= 8:
|
|
133
|
+
return card
|
|
134
|
+
return card[:4] + "*" * (len(card) - 8) + card[-4:]
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def password(password: str) -> str:
|
|
138
|
+
"""密码脱敏:全部替换为*
|
|
139
|
+
|
|
140
|
+
:param password: 密码
|
|
141
|
+
:return: 脱敏后的密码(全为*)
|
|
142
|
+
"""
|
|
143
|
+
if not password:
|
|
144
|
+
return ""
|
|
145
|
+
return "*" * len(password)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def car_license(license_no: str) -> str:
|
|
149
|
+
"""车牌号脱敏
|
|
150
|
+
|
|
151
|
+
保留前2位和后1位,中间用*替换
|
|
152
|
+
|
|
153
|
+
:param license_no: 车牌号
|
|
154
|
+
:return: 脱敏后的车牌号
|
|
155
|
+
"""
|
|
156
|
+
if not license_no:
|
|
157
|
+
return ""
|
|
158
|
+
license_no = license_no.strip()
|
|
159
|
+
if len(license_no) <= 3:
|
|
160
|
+
return license_no
|
|
161
|
+
return license_no[:2] + "*" * (len(license_no) - 3) + license_no[-1:]
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def ipv4(ipv4: str) -> str:
|
|
165
|
+
"""IPv4脱敏
|
|
166
|
+
|
|
167
|
+
保留前两段,后两段替换为*
|
|
168
|
+
|
|
169
|
+
:param ipv4: IPv4地址
|
|
170
|
+
:return: 脱敏后的IPv4地址
|
|
171
|
+
"""
|
|
172
|
+
if not ipv4:
|
|
173
|
+
return ""
|
|
174
|
+
ipv4 = ipv4.strip()
|
|
175
|
+
parts = ipv4.split(".")
|
|
176
|
+
if len(parts) != 4:
|
|
177
|
+
return ipv4
|
|
178
|
+
return parts[0] + "." + parts[1] + ".*.*"
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def license_plate(plate: str) -> str:
|
|
182
|
+
"""车牌号脱敏
|
|
183
|
+
|
|
184
|
+
保留省份简称和地区代码,后面替换为*
|
|
185
|
+
|
|
186
|
+
:param plate: 车牌号
|
|
187
|
+
:return: 脱敏后的车牌号
|
|
188
|
+
"""
|
|
189
|
+
if not plate:
|
|
190
|
+
return ""
|
|
191
|
+
plate = plate.strip()
|
|
192
|
+
if len(plate) <= 2:
|
|
193
|
+
return plate
|
|
194
|
+
return plate[:2] + "*" * (len(plate) - 2)
|