用 Python logging.Filter 实现手机号、邮箱与身份证号日志脱敏
【摘要】 日志中常混入手机号、邮箱和身份证号。本文用 Python logging.Filter 编写正则替换过滤器并挂到 Handler 上,给出可配置字段、最小可运行示例、运行方法与性能注意事项,帮助在落盘前完成脱敏。
用 Python logging.Filter 在日志落盘前脱敏手机号、邮箱和身份证号
问题场景
应用日志经常直接把用户输入、接口参数或异常上下文写入文件、转发到远端。手机号、邮箱、身份证号一旦落盘,后续排查、归档、共享都会扩大暴露面。比较稳妥的做法不是在每个 logger.info 前手写替换,而是在日志输出链路上加一层过滤器。Python 的 logging.Filter 可以挂在 Handler 上,在 Handler 真正写出日志之前修改 LogRecord。
需要先明确:过滤器适合处理“消息正文”。异常堆栈、extra 字段、Formatter 自定义字段需要额外处理。
实现思路
- 为三类敏感信息预编译正则,避免每条日志重复编译。
- 编写
SensitiveDataFilter(logging.Filter),在filter(record)中调用record.getMessage()得到最终消息文本,替换后写回record.msg,并清空record.args,防止 Formatter 再次拼接原始参数。 - 只把过滤器挂到需要落盘的 Handler 上,例如
FileHandler;如果多个 Handler 都要输出,应分别挂载,或统一在 Formatter 层处理。 - 规则以
(名称, 编译后的正则, 替换串)列表传入,便于增删字段。
最小可运行示例
import logging
import re
# 规则:名称、预编译正则、替换模板
DEFAULT_RULES = [
(
'mobile',
re.compile(r'(?<!\d)(1[3-9]\d)(\d{4})(\d{4})(?!\d)'),
r'\1****\3',
),
(
'email',
re.compile(
r'(?<![A-Za-z0-9._%+-])([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*'
r'@([A-Za-z0-9.-]+\.[A-Za-z]{2,})(?![A-Za-z0-9.-])'
),
r'\1***@\2',
),
(
'id_card',
re.compile(r'(?<!\d)([1-9]\d{5})(\d{4})(\d{2})(\d{2})(\d{3}[\dXx])(?!\d)'),
r'\1********\5',
),
]
class SensitiveDataFilter(logging.Filter):
def __init__(self, rules=None, name=''):
super().__init__(name)
self.rules = rules if rules is not None else DEFAULT_RULES
def _sanitize(self, text):
if not isinstance(text, str):
return text
for _, pattern, repl in self.rules:
text = pattern.sub(repl, text)
return text
def filter(self, record):
try:
message = record.getMessage()
except Exception:
# 格式化失败时退回原始 msg,避免过滤器改变日志行为
message = str(record.msg)
record.msg = self._sanitize(message)
record.args = ()
return True
def build_logger(log_path='app.log'):
logger = logging.getLogger('safe_app')
logger.setLevel(logging.INFO)
logger.propagate = False
if logger.handlers:
return logger
handler = logging.FileHandler(log_path, encoding='utf-8')
handler.setFormatter(
logging.Formatter('%(asctime)s %(levelname)s %(message)s')
)
handler.addFilter(SensitiveDataFilter())
logger.addHandler(handler)
return logger
if __name__ == '__main__':
log = build_logger('app.log')
log.info('用户手机号 13000000000,邮箱 alice@example.com')
log.info('身份证号 110101199001011234 已提交')
log.info('参数:%s,手机号:%s', {'email': 'bob@example.com'}, '13000000000')
运行方法
把代码保存为 demo.py,执行:
python demo.py
然后查看当前目录下的 app.log,应看到类似内容:
... INFO 用户手机号 130****0000,邮箱 a***@example.com
... INFO 身份证号 110101********1234 已提交
... INFO 参数:{'email': 'b***@example.com'},手机号:130****0000
示例中的手机号、邮箱和身份证号均为占位值,不对应真实用户。
可配置字段
SensitiveDataFilter 接受 rules 参数。新增规则时可以继续传入预编译正则,例如为银行卡号增加一条掩码规则:
rules = DEFAULT_RULES + [
('bank_card', re.compile(r'(?<!\d)(\d{4})\d{8}(\d{4})(?!\d)'), r'\1********\2'),
]
handler.addFilter(SensitiveDataFilter(rules=rules))
如果某类字段不需要脱敏,从规则列表中移除对应项即可;如果不同 Handler 需要不同规则,可以分别创建过滤器实例。
边界与限制
- 只处理消息正文。
exc_info的异常文本通常由 Formatter 追加,不会被这个过滤器捕获;如果异常消息中含敏感数据,需要自定义 Formatter 或在异常输出前处理。 extra字段和record.__dict__中的自定义字段不会自动脱敏。Formatter 若引用这些字段,应在 Formatter 层处理。- 正则覆盖有限:不匹配带空格或短横线的号码、15 位旧身份证、国际手机号、非 ASCII 邮箱等。应按业务补充规则,同时避免过度匹配。
- 过滤器修改的是共享的
LogRecord。挂到某个 Handler 后,后续 Handler 看到的是脱敏后的消息;如果其他 Handler 依赖原始参数,需要复制记录或改用 Formatter。 - 性能方面,正则已经预编译;只挂到必要 Handler;避免在高频 DEBUG 日志上全量运行多条复杂正则。日志量大时应对实际消息长度和规则数量做压测,不要只看单条耗时。
- 脱敏不是加密。掩码后仍可能通过组合信息推断,合规要求高时还需结合访问控制、日志留存周期和加密存储。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)