tea-tool 0.1.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.
tea_tool/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,40 @@
1
+ """日期时间 strftime/strptime 格式化模板常量。
2
+
3
+ 常量按粒度组织:DATE(日)、TIME(时间)、DATE_TIME(日期时间)、MONTH(月)与
4
+ YEAR 系压缩串(%Y%m%d…,常用于文件名/键)。同一粒度提供连字符(默认)、斜杠
5
+ (_SLASH)、中文(_CN)分隔变体;后缀 _ZONE 携带 %z 时区偏移、_ISO 为 ISO 8601
6
+ 扩展 T 分隔。注意:%f 恒为 6 位微秒且 strftime 不支持截宽,故含 %f 的模板按
7
+ 微秒(MICROSECOND)命名;%z 输出形如 +0800 无冒号(近似 RFC 3339,非严格)。
8
+ """
9
+
10
+ # 日期
11
+ DATE_FORMAT = "%Y-%m-%d"
12
+ DATE_FORMAT_SLASH = "%Y/%m/%d"
13
+ DATE_FORMAT_CN = "%Y年%m月%d日"
14
+
15
+ # 时间
16
+ TIME_FORMAT = "%H:%M:%S"
17
+ TIME_FORMAT_MINUTE = "%H:%M"
18
+ TIME_FORMAT_SLASH = "%H/%M/%S"
19
+ TIME_FORMAT_CN = "%H时%M分%S秒"
20
+
21
+ # 日期时间
22
+ DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
23
+ DATE_TIME_FORMAT_SLASH = "%Y/%m/%d %H:%M:%S"
24
+ DATE_TIME_FORMAT_CN = "%Y年%m月%d日 %H时%M分%S秒"
25
+ DATE_TIME_MICROSECOND = "%Y-%m-%d %H:%M:%S.%f"
26
+ DATE_TIME_ZONE = "%Y-%m-%d %H:%M:%S%z"
27
+ DATE_TIME_ISO = "%Y-%m-%dT%H:%M:%S"
28
+ DATE_TIME_ISO_ZONE = "%Y-%m-%dT%H:%M:%S%z"
29
+
30
+ # 月
31
+ MONTH_FORMAT = "%Y-%m"
32
+
33
+ # 年份系压缩串(无分隔符,常用于文件名/键)
34
+ YEAR = "%Y"
35
+ YEAR_TO_MONTH = "%Y%m"
36
+ YEAR_TO_DAY = "%Y%m%d"
37
+ YEAR_TO_HOUR = "%Y%m%d%H"
38
+ YEAR_TO_MINUTE = "%Y%m%d%H%M"
39
+ YEAR_TO_SECOND = "%Y%m%d%H%M%S"
40
+ YEAR_TO_MICROSECOND = "%Y%m%d%H%M%S.%f"
@@ -0,0 +1,58 @@
1
+ """常用时区常量与本地时区获取。
2
+
3
+ 常量覆盖 UTC 及主流业务城市的 IANA 时区(UTC-12 ~ UTC+14 各主要时区带),
4
+ 命名统一全大写、多词下划线分隔,可直接作为 tzinfo 参数传入 datetime 相关
5
+ 构造与 tea_tool.datetime.util 各函数。依赖 zoneinfo(Python 3.9+ 内置),
6
+ 具体时区数据取自已安装的 tzdata。
7
+ """
8
+
9
+ from datetime import datetime, tzinfo
10
+ from zoneinfo import ZoneInfo
11
+
12
+ UTC = ZoneInfo("UTC")
13
+
14
+ # 亚洲
15
+ SHANGHAI = ZoneInfo("Asia/Shanghai")
16
+ SINGAPORE = ZoneInfo("Asia/Singapore")
17
+ TOKYO = ZoneInfo("Asia/Tokyo")
18
+ SEOUL = ZoneInfo("Asia/Seoul")
19
+ HONG_KONG = ZoneInfo("Asia/Hong_Kong")
20
+ BANGKOK = ZoneInfo("Asia/Bangkok")
21
+ DUBAI = ZoneInfo("Asia/Dubai")
22
+ KOLKATA = ZoneInfo("Asia/Kolkata")
23
+
24
+ # 欧洲
25
+ LONDON = ZoneInfo("Europe/London")
26
+ PARIS = ZoneInfo("Europe/Paris")
27
+ BERLIN = ZoneInfo("Europe/Berlin")
28
+ AMSTERDAM = ZoneInfo("Europe/Amsterdam")
29
+ MOSCOW = ZoneInfo("Europe/Moscow")
30
+
31
+ # 美洲
32
+ NEW_YORK = ZoneInfo("America/New_York")
33
+ LOS_ANGELES = ZoneInfo("America/Los_Angeles")
34
+ CHICAGO = ZoneInfo("America/Chicago")
35
+ TORONTO = ZoneInfo("America/Toronto")
36
+ SAO_PAULO = ZoneInfo("America/Sao_Paulo")
37
+
38
+ # 大洋洲
39
+ SYDNEY = ZoneInfo("Australia/Sydney")
40
+ AUCKLAND = ZoneInfo("Pacific/Auckland")
41
+
42
+ # 非洲
43
+ JOHANNESBURG = ZoneInfo("Africa/Johannesburg")
44
+
45
+
46
+ def local_tz() -> tzinfo:
47
+ """返回系统本地时区。
48
+
49
+ 本地时区取自运行环境(TZ 环境变量或系统设置),与 datetime.now().astimezone()
50
+ 的时区归属一致;系统时区可被识别为具体区域时返回 ZoneInfo,否则退化为固定
51
+ 偏移时区。
52
+
53
+ Returns:
54
+ 当前系统本地时区对应的 tzinfo 对象。
55
+ """
56
+ local = datetime.now().astimezone().tzinfo
57
+ assert local is not None
58
+ return local
@@ -0,0 +1,194 @@
1
+ """datetime 日期时间通用工具。
2
+
3
+ 提供日历运算(月份首末日、逐日序列)与时刻获取(日零点、当日时间范围、
4
+ 本地/UTC 当前时间、字符串解析)。日历运算中的月份首末日与逐日序列
5
+ (get_month_start、get_month_end、list_days)仅接受 date;get_days_in_month
6
+ 接受 date/datetime(仅用其年月)。时刻类函数(get_day_start、get_day_range、
7
+ get_local_time、get_utc_time、parse_datetime)产出 datetime:date 输入按该日
8
+ 提升,datetime 输入保留 naive/aware 属性。时区语义:本模块只做钟面解释
9
+ (attach),不做跨时区换算;"本地时区"指系统时区,可用 timezone.local_tz()
10
+ 获取。
11
+ """
12
+
13
+ from calendar import monthrange
14
+ from datetime import UTC, date, datetime, time, timedelta, tzinfo
15
+
16
+ from .timezone import local_tz
17
+
18
+
19
+ def _shift_year_month(value: date | datetime, months: int) -> tuple[int, int]:
20
+ """计算 value 所在月偏移 months 个月后的 (年, 月)。
21
+
22
+ months 为负时同样正确(divmod 对负值取整向负无穷),例如 2026-01 偏移 -1
23
+ 得到 (2025, 12)。
24
+
25
+ Args:
26
+ value: 基准日期或时间,取其所在年与月。
27
+ months: 月份偏移量。
28
+
29
+ Returns:
30
+ 偏移后的 (年, 月) 二元组。
31
+ """
32
+ total = value.year * 12 + (value.month - 1) + months
33
+ year, month_index = divmod(total, 12)
34
+ return year, month_index + 1
35
+
36
+
37
+ def get_month_start(value: date | datetime, months: int = 0) -> date:
38
+ """返回 value 所在月偏移 months 个月后的首日。
39
+
40
+ months 为正表示偏移到之后的月份、为负表示之前的月份,0(默认)即 value 所在
41
+ 当月。value 无论 date 还是 datetime 均统一返回 date(datetime 仅取其年与月,
42
+ 不带时分秒与时区),例如 2026-01-31 10:30 偏移 1 个月返回 2026-02-01。
43
+
44
+ Args:
45
+ value: 基准日期或时间,取其所在月。
46
+ months: 月份偏移量,正为后负为前,默认当月。
47
+
48
+ Returns:
49
+ 目标月首日(date)。
50
+ """
51
+ year, month = _shift_year_month(value, months)
52
+ return date(year, month, 1)
53
+
54
+
55
+ def get_month_end(value: date | datetime, months: int = 0) -> date:
56
+ """返回 value 所在月偏移 months 个月后的末日。
57
+
58
+ 月份偏移语义与 get_month_start 一致(正后负前,0 为当月);返回目标月的最后
59
+ 一天,跨月取末日不受 value 原日影响,例如 2026-01-31 偏移 1 个月返回
60
+ 2026-02-28。value 无论 date 还是 datetime 均统一返回 date(datetime 仅取其
61
+ 年与月,不带时分秒与时区)。
62
+
63
+ Args:
64
+ value: 基准日期或时间,取其所在月。
65
+ months: 月份偏移量,正为后负为前,默认当月。
66
+
67
+ Returns:
68
+ 目标月末日(date)。
69
+ """
70
+ year, month = _shift_year_month(value, months)
71
+ last = monthrange(year, month)[1]
72
+ return date(year, month, last)
73
+
74
+
75
+ def get_days_in_month(value: date | datetime) -> int:
76
+ """返回 value 所在月的天数。
77
+
78
+ Args:
79
+ value: 基准日期或时间,取其所在月。
80
+
81
+ Returns:
82
+ 所在月的天数(平年 2 月为 28、闰年为 29)。
83
+ """
84
+ return monthrange(value.year, value.month)[1]
85
+
86
+
87
+ def get_day_start(value: date | datetime) -> datetime:
88
+ """返回 value 所在日的零点时刻。
89
+
90
+ 时刻域函数:date 输入提升为当日 00:00 的 naive datetime;naive datetime 清零
91
+ 时分秒与微秒;aware datetime 清零时刻但保留其 tzinfo(DST 切换日零点通常无
92
+ 歧义,不做 fold 处理)。
93
+
94
+ Args:
95
+ value: 基准日期或时间,取其所在日。
96
+
97
+ Returns:
98
+ 所在日零点对应的 datetime(naive 或保留输入的 aware 属性)。
99
+ """
100
+ if isinstance(value, datetime):
101
+ return value.replace(hour=0, minute=0, second=0, microsecond=0)
102
+ return datetime.combine(value, time.min)
103
+
104
+
105
+ def get_day_range(
106
+ value: date | datetime, tz: tzinfo | None = None
107
+ ) -> tuple[datetime, datetime]:
108
+ """返回 value 所在日的时间范围 [当日零点, 次日零点),左闭右开。
109
+
110
+ 时刻域函数,返回值恒为 datetime 对。时区规则:
111
+ - tz 显式给出时,两端钟面时间 attach 该时区(覆盖 value 自带时区,不换算);
112
+ - tz 缺省且 value 为 aware datetime 时,沿用 value 的时区;
113
+ - 其余情形(date / naive datetime)两端为 naive。
114
+ DST 切换日该范围的实际时长可能非 24 小时,但两端恒为该时区钟面零点。
115
+
116
+ Args:
117
+ value: 基准日期或时间,取其所在日。
118
+ tz: 目标时区;缺省时沿用输入时区或保持 naive。
119
+
120
+ Returns:
121
+ (当日零点, 次日零点) 元组。
122
+ """
123
+ if isinstance(value, datetime):
124
+ day = value.date()
125
+ resolved = tz if tz is not None else value.tzinfo
126
+ else:
127
+ day = value
128
+ resolved = tz
129
+ return (
130
+ datetime.combine(day, time.min, tzinfo=resolved),
131
+ datetime.combine(day + timedelta(days=1), time.min, tzinfo=resolved),
132
+ )
133
+
134
+
135
+ def list_days(start: date, end: date) -> list[date]:
136
+ """返回 [start, end) 区间内逐日 date 序列,左闭右开不含 end。
137
+
138
+ start 等于或晚于 end 时返回空列表。
139
+
140
+ Args:
141
+ start: 起始日期(含)。
142
+ end: 结束日期(不含)。
143
+
144
+ Returns:
145
+ 逐日 date 序列;start >= end 时为空列表。
146
+ """
147
+ result: list[date] = []
148
+ cursor = start
149
+ while cursor < end:
150
+ result.append(cursor)
151
+ cursor += timedelta(days=1)
152
+ return result
153
+
154
+
155
+ def get_local_time() -> datetime:
156
+ """返回系统本地时区的当前时刻(aware)。
157
+
158
+ Returns:
159
+ 带系统本地时区的当前 datetime。
160
+ """
161
+ return datetime.now().astimezone()
162
+
163
+
164
+ def get_utc_time() -> datetime:
165
+ """返回 UTC 的当前时刻(aware)。
166
+
167
+ Returns:
168
+ 带 UTC 时区的当前 datetime。
169
+ """
170
+ return datetime.now(UTC)
171
+
172
+
173
+ def parse_datetime(text: str, fmt: str, tz: tzinfo | None = None) -> datetime:
174
+ """按格式化模板将时间字符串解析为 aware datetime。
175
+
176
+ 结果为钟面解释:模板不含 %z 指令(字符串无时区信息)时,将解析出的钟面
177
+ 时间 attach 到 tz(缺省为系统本地时区);模板含 %z 指令时解析结果自带偏移
178
+ 并直接返回,此时忽略 tz 参数。DST 歧义时刻按钟面解释,不做特殊折叠处理。
179
+
180
+ Args:
181
+ text: 待解析的时间字符串。
182
+ fmt: strptime 格式化模板,可复用 tea_tool.datetime.formatter 常量。
183
+ tz: attach 目标时区,缺省为系统本地时区;模板含 %z 时无效。
184
+
185
+ Returns:
186
+ 解析得到的 aware datetime。
187
+
188
+ Raises:
189
+ ValueError: 当 text 与 fmt 不匹配时(strptime 原生异常)。
190
+ """
191
+ parsed = datetime.strptime(text, fmt) # noqa: DTZ007 解析结果 naive 时后续按 tz 参数 attach 时区
192
+ if parsed.tzinfo is not None:
193
+ return parsed
194
+ return parsed.replace(tzinfo=tz if tz is not None else local_tz())
File without changes
@@ -0,0 +1,39 @@
1
+ """通用脱敏工具:机制与内容分离。
2
+
3
+ 本包提供脱敏机制——策略(strategies)、文本发现规则(rules)与编排器
4
+ (Masker),不内置任何"哪些信息敏感、脱成什么样"的业务默认。规则与
5
+ 格式由使用方显式定义,可选用 presets 模块的预置规则集。
6
+
7
+ 典型用法(业务侧全局定义一次后复用)::
8
+
9
+ from tea_tool.masking import Masker, KeepStrategy
10
+ from tea_tool.masking.presets import CN_PII_RULES
11
+
12
+ phone_mask = KeepStrategy(prefix=3, suffix=4)
13
+ masker = Masker(rules=CN_PII_RULES)
14
+
15
+ masker.mask("13812345678", phone_mask) # 138****5678
16
+ masker.mask_text("联系 13812345678") # 联系 ***********
17
+ masker.mask_dict(data, fields={"phone": phone_mask})
18
+ """
19
+
20
+ from .core import Masker
21
+ from .rules import MaskMatch, MaskRule
22
+ from .strategies import (
23
+ HashStrategy,
24
+ KeepStrategy,
25
+ MaskStrategy,
26
+ RemoveStrategy,
27
+ ReplaceStrategy,
28
+ )
29
+
30
+ __all__ = [
31
+ "HashStrategy",
32
+ "KeepStrategy",
33
+ "MaskMatch",
34
+ "MaskRule",
35
+ "MaskStrategy",
36
+ "Masker",
37
+ "RemoveStrategy",
38
+ "ReplaceStrategy",
39
+ ]
@@ -0,0 +1,246 @@
1
+ """脱敏编排层:Masker 门面,提供单值、文本、结构化三种脱敏入口。
2
+
3
+ Masker 只持有使用方注入的规则列表,不含内置规则与类型注册表——何种文本
4
+ 是敏感信息、各类数据脱成什么样,均由使用方显式定义:可选用
5
+ tea_tool.masking.presets 的预置规则,或在业务侧建立脱敏配置常量后复用
6
+ 同一实例(如项目配置文件中的模块级 Masker)。
7
+ """
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ from .rules import MaskMatch, MaskRule
13
+ from .strategies import MaskStrategy
14
+
15
+
16
+ def _is_namedtuple(value: Any) -> bool:
17
+ """判断是否为具名元组(其构造器不接受序列重建)。
18
+
19
+ Args:
20
+ value: 待判断对象。
21
+
22
+ Returns:
23
+ value 为具名元组(含 _fields 的 tuple 子类)时返回 True。
24
+ """
25
+ return isinstance(value, tuple) and hasattr(type(value), "_fields")
26
+
27
+
28
+ class Masker:
29
+ """通用脱敏编排器。
30
+
31
+ 用法示例(文本自动识别,规则来自预置集)::
32
+
33
+ from tea_tool.masking.presets import CN_PII_RULES
34
+
35
+ masker = Masker(rules=CN_PII_RULES)
36
+ masker.mask_text("联系 13812345678") # 联系 ***********
37
+
38
+ 规则列表为空时 mask_text 不做自动识别;mask 与 mask_dict 不依赖规则,
39
+ 按调用处显式传入的策略工作。
40
+ """
41
+
42
+ def __init__(self, *, rules: list[MaskRule] | None = None) -> None:
43
+ """构造脱敏器。
44
+
45
+ Args:
46
+ rules: 初始文本识别规则;未传时为空列表(可稍后经
47
+ register_rule 追加)。
48
+ """
49
+ self._rules: list[MaskRule] = list(rules or [])
50
+
51
+ # ------------------------------------------------------------------
52
+ # 注册
53
+ # ------------------------------------------------------------------
54
+
55
+ def register_rule(self, rule: MaskRule) -> None:
56
+ """追加一条文本识别规则。
57
+
58
+ Args:
59
+ rule: 待追加的规则。
60
+ """
61
+ self._rules.append(rule)
62
+
63
+ # ------------------------------------------------------------------
64
+ # 单值
65
+ # ------------------------------------------------------------------
66
+
67
+ def mask(self, value: Any, strategy: MaskStrategy) -> str | None:
68
+ """对单个值按指定策略脱敏。
69
+
70
+ None 原样返回("无数据"无需脱敏);非字符串值先转字符串再脱敏,
71
+ 因此输出恒为字符串形态(int/float 等原始类型不保留)。
72
+
73
+ Args:
74
+ value: 待脱敏值,可为任意类型。
75
+ strategy: 采用的脱敏策略。
76
+
77
+ Returns:
78
+ 脱敏后的字符串;value 为 None 时返回 None。
79
+ """
80
+ if value is None:
81
+ return None
82
+
83
+ return strategy.mask(value if isinstance(value, str) else str(value))
84
+
85
+ # ------------------------------------------------------------------
86
+ # 文本
87
+ # ------------------------------------------------------------------
88
+
89
+ def mask_text(
90
+ self,
91
+ text: str,
92
+ *,
93
+ rules: list[MaskRule] | None = None,
94
+ ) -> str:
95
+ """对自由文本做自动识别脱敏。
96
+
97
+ 文本被全部规则扫描,命中区间先按优先级消解重叠,再按起始位置
98
+ 单趟线性拼接、原位替换为所属规则的策略输出——命中互不重叠,
99
+ 策略输出变长或删除时不影响其他命中的替换。
100
+
101
+ Args:
102
+ text: 待脱敏文本。
103
+ rules: 本次调用使用的规则列表;未传时使用构造时注入的规则
104
+ (可传空列表临时禁用识别)。
105
+
106
+ Returns:
107
+ 脱敏后的文本;text 为空时原样返回。
108
+ """
109
+ if not text:
110
+ return text
111
+
112
+ active_rules = self._rules if rules is None else rules
113
+
114
+ matches: list[MaskMatch] = []
115
+ for rule in active_rules:
116
+ matches.extend(rule.find(text))
117
+
118
+ resolved = self._resolve_overlaps(matches)
119
+ if not resolved:
120
+ return text
121
+
122
+ # 命中互不重叠且按起始位置升序:一次遍历拼接新文本,避免逐命中
123
+ # 重建文本的平方级开销(替换串长度变化不影响其他区间)
124
+ parts: list[str] = []
125
+ pos = 0
126
+ for match in resolved:
127
+ parts.append(text[pos : match.start])
128
+ parts.append(match.strategy.mask(match.value))
129
+ pos = match.end
130
+ parts.append(text[pos:])
131
+ return "".join(parts)
132
+
133
+ # ------------------------------------------------------------------
134
+ # 结构化数据
135
+ # ------------------------------------------------------------------
136
+
137
+ def mask_dict(
138
+ self,
139
+ data: Mapping[str, Any],
140
+ fields: Mapping[str, MaskStrategy],
141
+ *,
142
+ recursive: bool = True,
143
+ ) -> dict[str, Any]:
144
+ """对 dict 按字段映射脱敏,未声明字段原样保留。
145
+
146
+ fields 为字段名到脱敏策略的映射,例如::
147
+
148
+ masker.mask_dict(
149
+ {"phone": "13812345678", "remark": "无"},
150
+ fields={"phone": KeepStrategy(prefix=3, suffix=4)},
151
+ )
152
+
153
+ 同一份字段映射会应用到嵌套层级:recursive 为 True 时,嵌套 dict
154
+ 与 list/tuple 会被递归处理,嵌套 dict 中同名命中的字段同样脱敏;
155
+ 嵌套容器内未命中的字段原样保留。
156
+
157
+ Args:
158
+ data: 待脱敏的 dict,仅处理顶层字符串键。
159
+ fields: 字段名到脱敏策略的映射。
160
+ recursive: 是否递归处理嵌套 dict 与 list/tuple,默认 True。
161
+
162
+ Returns:
163
+ 脱敏后的新 dict;原始 data 不被修改。
164
+ """
165
+ result: dict[str, Any] = {}
166
+
167
+ for key, value in data.items():
168
+ if key in fields:
169
+ result[key] = self.mask(value, fields[key])
170
+ elif recursive and isinstance(value, Mapping):
171
+ result[key] = self.mask_dict(value, fields=fields)
172
+ elif recursive and isinstance(value, (list, tuple)):
173
+ if _is_namedtuple(value):
174
+ # 具名元组按字段结构存在,序列重建会破坏其形状,原样保留
175
+ result[key] = value
176
+ else:
177
+ result[key] = self._mask_collection(value, fields=fields)
178
+ else:
179
+ result[key] = value
180
+
181
+ return result
182
+
183
+ # ------------------------------------------------------------------
184
+ # 内部方法
185
+ # ------------------------------------------------------------------
186
+
187
+ def _mask_collection(
188
+ self,
189
+ value: list[Any] | tuple[Any, ...],
190
+ fields: Mapping[str, MaskStrategy],
191
+ ) -> list[Any] | tuple[Any, ...]:
192
+ """递归脱敏集合元素:dict 元素按字段映射,嵌套集合继续深入。
193
+
194
+ Args:
195
+ value: 待脱敏的列表或元组。
196
+ fields: 字段名到脱敏策略的映射(与 mask_dict 相同)。
197
+
198
+ Returns:
199
+ 与原容器同类型的新容器,元素已按字段映射脱敏。
200
+ """
201
+ result: list[Any] = []
202
+
203
+ for item in value:
204
+ if isinstance(item, Mapping):
205
+ result.append(self.mask_dict(item, fields=fields))
206
+ elif isinstance(item, (list, tuple)) and not _is_namedtuple(item):
207
+ result.append(self._mask_collection(item, fields=fields))
208
+ else:
209
+ result.append(item)
210
+
211
+ return type(value)(result)
212
+
213
+ @staticmethod
214
+ def _resolve_overlaps(matches: list[MaskMatch]) -> list[MaskMatch]:
215
+ """消除多规则命中的区间重叠。
216
+
217
+ 排序键依次为优先级高、起始靠前、区间长,再依序贪心选取不与已选
218
+ 命中重叠的区间——重叠时保留排序靠前者,保证身份证等长区间规则
219
+ 不被银行卡等子串规则拆分。
220
+
221
+ 候选起点不小于已选命中最大右端点时,与全部已选必然不重叠(其
222
+ 右端点均不超过该最大值),可直接接受:避免大文本大量命中时
223
+ 逐个比较的平方级开销。
224
+
225
+ Args:
226
+ matches: 全部规则产生的命中列表。
227
+
228
+ Returns:
229
+ 互不重叠的命中列表,按起始位置升序排列。
230
+ """
231
+ ordered = sorted(
232
+ matches,
233
+ key=lambda item: (-item.priority, item.start, -item.length),
234
+ )
235
+
236
+ selected: list[MaskMatch] = []
237
+ max_end = -1
238
+ for match in ordered:
239
+ if match.start >= max_end or not any(
240
+ match.start < current.end and match.end > current.start
241
+ for current in selected
242
+ ):
243
+ selected.append(match)
244
+ max_end = max(max_end, match.end)
245
+
246
+ return sorted(selected, key=lambda item: item.start)
@@ -0,0 +1,86 @@
1
+ """显式选用的预置规则集:中国大陆常见个人信息的文本识别规则。
2
+
3
+ 预置规则只做"识别 + 全星"这一最安全的默认:每条规则绑定无参
4
+ KeepStrategy()(整段掩码),不含保留位等业务格式断言——具体格式由使用方
5
+ 按需派生,例如::
6
+
7
+ phone_rule = PHONE_RULE.with_strategy(
8
+ KeepStrategy(prefix=3, suffix=4)
9
+ )
10
+
11
+ 本模块不自动生效,须显式传入 Masker::
12
+
13
+ from tea_tool.masking import Masker
14
+ from tea_tool.masking.presets import CN_PII_RULES
15
+
16
+ masker = Masker(rules=CN_PII_RULES)
17
+
18
+ 已知局限:
19
+
20
+ - IP_RULE 无法区分 IPv4 与四段点分十进制数字,版本号等(如 "1.2.3.4")
21
+ 会被误判为 IP;
22
+ - BANK_CARD_RULE 只按 13~19 位纯数字串识别,不做 Luhn 校验,生产环境建议
23
+ 叠加 Luhn 或更严格的上下文约束;
24
+ - ID_CARD_RULE 仅覆盖中国大陆 18 位身份证格式(含末位 X/x)。
25
+ """
26
+
27
+ from .rules import MaskRule
28
+ from .strategies import KeepStrategy
29
+
30
+ # 中国大陆手机号:1[3-9] 开头共 11 位。
31
+ PHONE_RULE = MaskRule(
32
+ pattern=r"(?<!\d)1[3-9]\d{9}(?!\d)",
33
+ strategy=KeepStrategy(),
34
+ priority=100,
35
+ )
36
+
37
+ # 18 位身份证:优先级高于银行卡,避免纯数字身份证被银行卡规则拆分命中。
38
+ ID_CARD_RULE = MaskRule(
39
+ pattern=r"(?<!\d)\d{17}[\dXx](?!\d)",
40
+ strategy=KeepStrategy(),
41
+ priority=200,
42
+ )
43
+
44
+ # 邮箱。
45
+ EMAIL_RULE = MaskRule(
46
+ pattern=(
47
+ r"(?<![\w.+-])"
48
+ r"[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
49
+ r"@"
50
+ r"[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+"
51
+ r"(?![\w.-])"
52
+ ),
53
+ strategy=KeepStrategy(),
54
+ priority=80,
55
+ )
56
+
57
+ # IPv4。
58
+ IP_RULE = MaskRule(
59
+ pattern=(
60
+ r"(?<![\d.])"
61
+ r"(?:"
62
+ r"(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\."
63
+ r"){3}"
64
+ r"(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
65
+ r"(?![\d.])"
66
+ ),
67
+ strategy=KeepStrategy(),
68
+ priority=50,
69
+ )
70
+
71
+ # 银行卡:只负责发现 13~19 位数字串,不做 Luhn 校验(见模块 docstring)。
72
+ BANK_CARD_RULE = MaskRule(
73
+ pattern=r"(?<!\d)\d{13,19}(?!\d)",
74
+ strategy=KeepStrategy(),
75
+ priority=150,
76
+ )
77
+
78
+ # 中国大陆常见个人信息规则组合,按优先级降序:
79
+ # 身份证(200) > 银行卡(150) > 手机号(100) > 邮箱(80) > IP(50)。
80
+ CN_PII_RULES = [
81
+ ID_CARD_RULE,
82
+ BANK_CARD_RULE,
83
+ PHONE_RULE,
84
+ EMAIL_RULE,
85
+ IP_RULE,
86
+ ]