datamask-core 1.0.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: datamask-core
3
+ Version: 1.0.0
4
+ Summary: DataMask 核心规则引擎 — 正则脱敏、实体识别、NER Pipeline,零外部依赖
5
+ Author-email: TianluAudit <contact@datamask.cn>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://datamask.cn
8
+ Project-URL: Documentation, https://datamask.cn/docs
9
+ Keywords: data-masking,privacy,NER,entity-recognition,FPE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.9
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "datamask-core"
7
+ version = "1.0.0"
8
+ description = "DataMask 核心规则引擎 — 正则脱敏、实体识别、NER Pipeline,零外部依赖"
9
+ requires-python = ">=3.9"
10
+ license = {text = "Proprietary"}
11
+ authors = [{name = "TianluAudit", email = "contact@datamask.cn"}]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ ]
20
+ keywords = ["data-masking", "privacy", "NER", "entity-recognition", "FPE"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://datamask.cn"
24
+ Documentation = "https://datamask.cn/docs"
25
+
26
+ [tool.setuptools.package-dir]
27
+ "" = "src"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """datamask-core — DataMask 核心规则引擎(零外部依赖)"""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,103 @@
1
+ """
2
+ DataMask 误识别黑名单
3
+ 从格力电器2024年报模型原始输出统计提取(2026-05-16)
4
+ """
5
+ import re
6
+
7
+ # ══ 误识别黑名单(出现频次 >= 3 的高频误识别词)═══
8
+ FP_BLACKLIST = set([
9
+ # 多字通用词/碎片(ORG 高频误识别)
10
+ '有限公司', '展有限公', '公司有', '份有限', '网科技', '力电器',
11
+ '广东省', '年度报', '珠海市', '监事会', '董事会', '有限公',
12
+ '器有限', '投资者', '人民币', '管理层', '有关方', '上市',
13
+ '股东', '上线', '格信', '失控', '科学', '年度', '全国',
14
+ '师事', '全省', '网络', '风险', '中国', '全市', '相关',
15
+ '港元', '所有', '有限', '有关', '缩机', '全区', '市股',
16
+ '并成', '企业', '器建', '市场', '展多', '京海', '线机',
17
+ '技能', '日文', '能力', '集团', '主要', '事会', '中华',
18
+ '能源', '国家', '欧元', '重要', '美元', '中文', '珠海',
19
+ '技术', '广东', '电机', '多个', '公司', '节能', '英文',
20
+ # 2026-05-16 全量测试模型碎片TOP(年报目录/短语碎片)
21
+ '动有', '上市公司股', '上市公司股东', '普通股股东', '普通股股', '普通股',
22
+ '除限', '简介', '及股', '展机', '中有', ':中', '金融机', '联中',
23
+ '母公司所有', '母公', '讨论与分', '和社会责', ' 格力电', '上能',
24
+ # 2026-05-16 人工抽检发现的通用名词碎片
25
+ '再生资源基地', '各基地', '第三方电商平台', '自建线上平台', '助力集团',
26
+ '助力', '会计准则', '会计数据', '境内外会计准则下会计数据',
27
+ # 2026-05-16 第二批人工抽检误识别
28
+ '浮电机系统', '鉴定委员会', '中国电工技术学会鉴定委员会',
29
+ '以用户需求为中心', '卓越级智能', '打造行业领先的智能', '行业领先的智能',
30
+ '加速智能', '股票上市证券交易所', '光照五大系统的智能', '五大系统的智能',
31
+ # 单字碎片(模型 WordPiece 边界导致的高频误识别)
32
+ '线', '外', '份', '现', '场', '有', '罗', '峰', '高', '速',
33
+ '红', '东', '路', '小', '何', '广', '监', '波', '区', '技',
34
+ '讯', '林', '百', '北', '大', '中', '超', '重', '军', '格',
35
+ '曾', '电', '公', '失', '网', '易', '新', '李', '家', '业',
36
+ '创', '局', '所', '险', '督', '立', '安', '行', '器', '师',
37
+ '人', '学', '术', '无', '上', '国', '深', '普', '末', '名',
38
+ '张', '股', '海', '集', '空', '珠', '节', '府', '任', '机',
39
+ '多', '年', '世', '控', '周', '事', '强', '理', '美', '市',
40
+ '能', '敏', '胡', '会', '刘', '为', '司', '省', '平', '限',
41
+ '健', '建', '展', '王', '力', '基', '委', '动', '华', '金',
42
+ '解',
43
+ # 数字/标点碎片
44
+ '00', ',0', ',', '、', ',', '3', '0', '1', '6', '4', '5', '7', '"',
45
+ # ── W1.5 P1-1 新增:ORG FP 高频误识词(基于 455 条评估集分析)──
46
+ # 通用后缀误识
47
+ '投资有限公司', # 4次 - "沃尔玛(中国)投资有限公司" 上下文无中国时被误识
48
+ '高级工程', # 4次 - "高级工程师" 截断(应识别为 PER,不是 ORG)
49
+ '增值税电子', # 3次 - "增值税电子普通发票" 错位
50
+ '人工智能', # 3次 - "人工智能算法" 误识
51
+ '信用等', # 2次 - "信用等级" 截断
52
+ '税务代理', # 1次 - "税务代理" 误识
53
+ '缴金额', # 1次 - "缴金额" 误识
54
+ '例2', # 1次 - 数字+字 误识
55
+ ',您', # 1次 - 标点+字 误识
56
+ '581209号\n纳税', # 1次 - 数字+换行+税词
57
+ # ORG 行业词误识("高级工程师"前出现的行业名词被误吞)
58
+ '东莞数据', '佛山数据', '武汉数据', '杭州数据', # "X市数据" 截断
59
+ '苏州市工业', '深圳市南山区数据', '北京市海淀区电力', # 城市+行业截断
60
+ '包括东莞市松山湖科技', # "包括" 引导词 + 城市
61
+ '东莞市松山湖科技', # 城市+科技截断
62
+ '武汉市洪山区光谷软件', # 城市+软件截断
63
+ '深圳市网鹏科技', # 城市+科技公司截断
64
+ # 行业通用词误识(不能单独成 ORG)
65
+ '数据', '科技', '软件', '网络', '智能', '信息', '技术',
66
+ '工程', '建设', '建筑', '机械', '装备', '制造', '加工',
67
+ '文化', '传媒', '出版', '教育', '培训', '咨询', '服务',
68
+ '贸易', '商业', '物流', '运输', '仓储', '物业', '管理',
69
+ '能源', '电力', '热电', '化工', '材料', '冶金', '矿产',
70
+ '装饰', '装修', '景观', '园林', '市政', '建筑装饰',
71
+ '农业', '林业', '渔业', '牧业', '种业', '饲料', '化肥',
72
+ '汽车', '机械', '装备', '工业', '化工', '材料', '冶金',
73
+ '不动产', '房地产', '地产', '置业', '资产管理',
74
+ '金融', '银行', '保险', '证券', '基金', '信托', '期货',
75
+ '酒店', '宾馆', '饭店', '旅馆', '招待所', '度假村',
76
+ '医院', '门诊部', '卫生所', '卫生院', '疾控中心',
77
+ '学校', '中学', '小学', '幼儿园', '大学', '学院',
78
+ '政府', '机关', '机构', '组织', '单位', '部门', '科室',
79
+ '团队', '小组', '工作', '工作地',
80
+ # "X国/中国" 单独被误识
81
+ '沃尔玛', '京东', '阿里', '百度', '腾讯', '网易', '美团', '字节跳动',
82
+ # 行政区+行业后缀误识
83
+ '海淀区', '南山区', '朝阳区', '天河区', '浦东新区',
84
+ '高新区', '开发区', '保税区', '自贸区', '产业园区',
85
+ ])
86
+
87
+ # 常见姓氏(用于单字PER判断)
88
+ COMMON_SURNAMES = '张王李赵刘陈杨黄周吴徐孙马朱胡郭何罗林郑梁谢宋唐韩冯董萧程曹袁邓许傅沈曾彭吕苏卢蒋蔡贾丁魏薛叶阎余潘杜戴夏钟汪田任姜范方石姚谭廖邹熊金陆郝孔白崔康毛邱秦江史顾侯邵孟龙万段雷钱汤尹黎易常武乔贺赖龚文'
89
+
90
+
91
+ def is_blacklisted(text: str) -> bool:
92
+ """检查文本是否在误识别黑名单中"""
93
+ if not text or len(text.strip()) == 0:
94
+ return True
95
+ if text.strip() in FP_BLACKLIST:
96
+ return True
97
+ if re.match(r'^\d+$', text):
98
+ return True
99
+ if len(text) == 1 and text not in COMMON_SURNAMES:
100
+ return True
101
+ if re.match(r'^[\s,,、.。!?;;::\-_()()\[\]【\]]+$', text):
102
+ return True
103
+ return False
@@ -0,0 +1,54 @@
1
+ """
2
+ DataMask 集中配置(环境变量 + 默认值)
3
+ 所有硬编码配置迁移至此
4
+ """
5
+ import os
6
+ import sys
7
+ import hashlib
8
+ from pathlib import Path
9
+
10
+ # ── 路径推导 ────────────────────────────────────────────
11
+ _POC_DIR = Path(__file__).parent.absolute()
12
+ _PROJECT_DIR = _POC_DIR.parent
13
+ _NER_DIR = _POC_DIR / "ner"
14
+
15
+
16
+ def _env_path(var: str, default: str) -> str:
17
+ val = os.environ.get(var, "")
18
+ return val if val else str(Path(default))
19
+
20
+
21
+ # ── FPE 加密密钥 ────────────────────────────────────────
22
+ DATAMASK_KEY = os.environ.get("DATAMASK_KEY", "").encode()
23
+ if not DATAMASK_KEY:
24
+ # POC 默认开发密钥 — 生产环境必须通过环境变量覆盖
25
+ DATAMASK_KEY = hashlib.sha256(b"datamask-poc-dev-key").digest()
26
+
27
+ # ── 模型路径 ────────────────────────────────────────────
28
+ ONNX_MODEL_PATH = _env_path("ONNX_MODEL_PATH", _NER_DIR / "onnx_infer" / "bert_ner_v7.onnx")
29
+ TOKENIZER_DIR = _env_path("TOKENIZER_DIR", _NER_DIR / "onnx_infer")
30
+
31
+ # ── 推理参数 ────────────────────────────────────────────
32
+ CONFIDENCE_THRESHOLD = float(os.environ.get("CONFIDENCE_THRESHOLD", "0.6"))
33
+ MAX_SEQUENCE_LENGTH = int(os.environ.get("MAX_SEQUENCE_LENGTH", "512"))
34
+
35
+ # ── 日志 ────────────────────────────────────────────────
36
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
37
+
38
+ # ── API ─────────────────────────────────────────────────
39
+ API_HOST = os.environ.get("API_HOST", "0.0.0.0")
40
+ API_PORT = int(os.environ.get("API_PORT", "8000"))
41
+
42
+ # ── 实体类型枚举 ────────────────────────────────────────
43
+ # 正则引擎实体类型
44
+ REGEX_ENTITY_TYPES = {
45
+ "PHONE", "PHONE_400", "EMAIL", "URL", "QQ", "WECHAT",
46
+ "OFFICIAL_ACCOUNT", "SERVICE_ACCOUNT", "IDCARD", "BANKCARD",
47
+ "STOCK", "ZIPCODE",
48
+ }
49
+
50
+ # 模型实体类型(需脱敏)
51
+ MODEL_ENTITY_TYPES = {"ORG", "PER", "LOC"}
52
+
53
+ # 保留实体类型(不脱敏,原样保留)
54
+ PRESERVED_ENTITY_TYPES = {"DATE", "MONEY"}
@@ -0,0 +1,164 @@
1
+ """
2
+ DataMask 实体加密层 — Design v1.1 LLM-Friendly 模式核心
3
+
4
+ 设计要点:
5
+ - 同一原文(type 内)跨任务保持 token 一致(HMAC-SHA256 派生)
6
+ - 原文 AES-256-GCM 加密后存库,用于 reverse 还原
7
+ - 每次加密使用随机 nonce(96-bit),保证语义安全
8
+ - 派生密钥与加密密钥分离(HKDF 或双独立密钥),降低密钥泄露影响
9
+
10
+ 密钥架构:
11
+ - ENCRYPTION_KEY (32B): 用于 AES-256-GCM 加解密原文
12
+ - HMAC_KEY (32B): 用于 HMAC-SHA256 派生 token_id
13
+ - 两把密钥独立,POC 默认值由 dev key 派生,生产必须从环境变量覆盖
14
+ """
15
+ import os
16
+ import hmac
17
+ import hashlib
18
+ import base64
19
+ import secrets
20
+ from typing import Optional, Tuple
21
+
22
+
23
+ # ── 默认开发密钥(POC)──────────────────────────────────
24
+ # 生产环境必须通过环境变量 ENCRYPTION_KEY / HMAC_KEY 提供 32 字节密钥
25
+ # base64 编码后填入,避免源码出现裸字节
26
+ _DEV_ENCRYPTION_KEY_B64 = hashlib.sha256(b"datamask-encryption-dev-key").digest()
27
+ _DEV_HMAC_KEY_B64 = hashlib.sha256(b"datamask-hmac-dev-key").digest()
28
+
29
+
30
+ def _load_key(env_name: str, dev_default: bytes) -> bytes:
31
+ """
32
+ 从环境变量加载 base64 编码的 32 字节密钥
33
+ 缺失时回退到开发默认密钥(仅 POC 用途)
34
+ """
35
+ val = os.environ.get(env_name, "").strip()
36
+ if not val:
37
+ return dev_default
38
+ try:
39
+ decoded = base64.b64decode(val)
40
+ if len(decoded) < 32:
41
+ # 太短则 sha256 扩展到 32B
42
+ return hashlib.sha256(decoded).digest()
43
+ return decoded[:32]
44
+ except Exception:
45
+ return dev_default
46
+
47
+
48
+ # 全局默认密钥(POC 友好)
49
+ ENCRYPTION_KEY = _load_key("ENCRYPTION_KEY", _DEV_ENCRYPTION_KEY_B64)
50
+ HMAC_KEY = _load_key("HMAC_KEY", _DEV_HMAC_KEY_B64)
51
+
52
+
53
+ class EntityCrypto:
54
+ """
55
+ 实体加密器 — 负责 token_id 派生与原文加解密
56
+
57
+ 用法:
58
+ crypto = EntityCrypto(encryption_key, hmac_key)
59
+ token_id = crypto.derive_token_id("深圳南山区科技公司", "ORG")
60
+ ciphertext, nonce = crypto.encrypt("深圳南山区科技公司")
61
+ plain = crypto.decrypt(ciphertext, nonce)
62
+ """
63
+
64
+ NONCE_SIZE = 12 # AES-256-GCM 推荐 96-bit nonce
65
+
66
+ def __init__(self, encryption_key: bytes = None, hmac_key: bytes = None):
67
+ """
68
+ :param encryption_key: 32 字节 AES-256 密钥(None 则使用全局默认)
69
+ :param hmac_key: 32 字节 HMAC 密钥(None 则使用全局默认)
70
+ """
71
+ self.encryption_key = encryption_key or ENCRYPTION_KEY
72
+ self.hmac_key = hmac_key or HMAC_KEY
73
+
74
+ if len(self.encryption_key) != 32:
75
+ raise ValueError(f"encryption_key 必须是 32 字节,当前 {len(self.encryption_key)}")
76
+ if len(self.hmac_key) != 32:
77
+ raise ValueError(f"hmac_key 必须是 32 字节,当前 {len(self.hmac_key)}")
78
+
79
+ def derive_token_id(self, original: str, entity_type: str) -> str:
80
+ """
81
+ 派生稳定的 token_id(16 字节 hex = 32 字符)
82
+
83
+ 特性:
84
+ - 同一 (type, original) 永远得到同一 token_id(确定性)
85
+ - 不同 type 下相同 original 派生不同 token_id(隔离)
86
+ - HMAC-SHA256 抗碰撞,无法逆推原文
87
+ - 16 字节 = 128-bit 强度远超 2^64 生日攻击阈值
88
+ """
89
+ if not original:
90
+ raise ValueError("original 不能为空")
91
+ if not entity_type:
92
+ raise ValueError("entity_type 不能为空")
93
+ msg = f"{entity_type.upper()}\x00{original}".encode("utf-8")
94
+ digest = hmac.new(self.hmac_key, msg, hashlib.sha256).digest()
95
+ return digest[:16].hex()
96
+
97
+ def encrypt(self, plaintext: str) -> Tuple[bytes, bytes]:
98
+ """
99
+ AES-256-GCM 加密原文
100
+
101
+ :return: (ciphertext, nonce) — nonce 12 字节随机生成
102
+ :raises ImportError: cryptography 库未安装
103
+ """
104
+ try:
105
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
106
+ except ImportError as e:
107
+ raise ImportError(
108
+ "需要安装 cryptography 库: pip install cryptography"
109
+ ) from e
110
+
111
+ if not isinstance(plaintext, str):
112
+ plaintext = str(plaintext)
113
+ nonce = secrets.token_bytes(self.NONCE_SIZE)
114
+ aesgcm = AESGCM(self.encryption_key)
115
+ ct = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), associated_data=None)
116
+ return ct, nonce
117
+
118
+ def decrypt(self, ciphertext: bytes, nonce: bytes) -> str:
119
+ """
120
+ AES-256-GCM 解密
121
+
122
+ :param ciphertext: 密文(包含 GCM tag)
123
+ :param nonce: 12 字节 nonce
124
+ :return: 原文
125
+ :raises ValueError: 解密失败(tag 校验失败 / 密钥错误)
126
+ """
127
+ try:
128
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
129
+ except ImportError as e:
130
+ raise ImportError(
131
+ "需要安装 cryptography 库: pip install cryptography"
132
+ ) from e
133
+
134
+ if len(nonce) != self.NONCE_SIZE:
135
+ raise ValueError(f"nonce 必须是 {self.NONCE_SIZE} 字节")
136
+ aesgcm = AESGCM(self.encryption_key)
137
+ try:
138
+ pt = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
139
+ except Exception as e:
140
+ raise ValueError(f"解密失败(tag 校验失败): {e}") from e
141
+ return pt.decode("utf-8")
142
+
143
+ def encrypt_to_b64(self, plaintext: str) -> Tuple[str, str]:
144
+ """便捷接口:加密并返回 base64 编码"""
145
+ ct, nonce = self.encrypt(plaintext)
146
+ return base64.b64encode(ct).decode("ascii"), base64.b64encode(nonce).decode("ascii")
147
+
148
+ def decrypt_from_b64(self, ct_b64: str, nonce_b64: str) -> str:
149
+ """便捷接口:从 base64 解密"""
150
+ ct = base64.b64decode(ct_b64)
151
+ nonce = base64.b64decode(nonce_b64)
152
+ return self.decrypt(ct, nonce)
153
+
154
+
155
+ # ── 工厂函数 ────────────────────────────────────────────
156
+ _default_crypto: Optional[EntityCrypto] = None
157
+
158
+
159
+ def get_default_crypto() -> EntityCrypto:
160
+ """获取全局默认 EntityCrypto 实例(延迟初始化)"""
161
+ global _default_crypto
162
+ if _default_crypto is None:
163
+ _default_crypto = EntityCrypto()
164
+ return _default_crypto