把报错日志发给 AI 前,先用 Python 做一次本地脱敏

报错日志太长时,我也会想到让 AI 帮忙找问题。

但复制之前最好停一下。日志里除了异常堆栈,还可能混着用户邮箱、手机号、Authorization、API Key 和带 Token 的请求地址。

手动删除并不稳,日志一长就容易漏。更省事的办法,是先在本地跑一次脱敏脚本,只把处理后的副本交给 AI。

这个脚本处理什么

下面的脚本默认识别:

  • Authorization: Bearer ...
  • api_keytokenpasswordsecret 等字段;
  • URL 查询参数中的 Token;
  • 邮箱地址;
  • 中国大陆手机号。

它不会修改原始日志,也不会删除 Traceback、文件名、行号和异常类型。

完整代码

保存为 redact_log.py

bash 复制代码
from __future__ import annotations

import argparse
import re
from collections.abc import Callable
from pathlib import Path


Replacement = str | Callable[[re.Match[str]], str]


def mask_email(match: re.Match[str]) -> str:
    local, domain = match.group(1), match.group(2)
    visible = local[0] if local else "*"
    return f"{visible}***@{domain}"


RULES: list[tuple[str, re.Pattern[str], Replacement]] = [
    (
        "authorization",
        re.compile(
            r"(?i)(\bAuthorization\s*[:=]\s*Bearer\s+)"
            r"[A-Za-z0-9._~+/=-]{8,}"
        ),
        r"\1***",
    ),
    (
        "url_secret",
        re.compile(
            r"(?i)([?&](?:api[_-]?key|access[_-]?token|"
            r"token|secret)=)[^&\s]+"
        ),
        r"\1***",
    ),
    (
        "named_secret",
        re.compile(
            r'''(?ix)
            (?<![?&])
            (?P<prefix>
                ["']?(?:api[_-]?key|access[_-]?token|
                token|password|passwd|secret)["']?
                \s*[:=]\s*
            )
            (?P<quote>["']?)
            (?P<value>[^\s,&"'}]+)
            (?P=quote)
            '''
        ),
        lambda m: (
            f"{m.group('prefix')}"
            f"{m.group('quote')}***{m.group('quote')}"
        ),
    ),
    (
        "email",
        re.compile(
            r"(?<![\w.+-])([\w.+-]+)@"
            r"([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)"
        ),
        mask_email,
    ),
    (
        "cn_mobile",
        re.compile(
            r"(?<!\d)(1[3-9]\d)(\d{4})(\d{4})(?!\d)"
        ),
        r"\1****\3",
    ),
]


def redact_text(text: str) -> tuple[str, dict[str, int]]:
    counts: dict[str, int] = {}
    result = text

    for name, pattern, replacement in RULES:
        result, count = pattern.subn(replacement, result)

        if count:
            counts[name] = count

    return result, counts


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Redact common secrets and personal data "
            "from a UTF-8 log file."
        )
    )
    parser.add_argument(
        "input",
        type=Path,
        help="Source log file",
    )
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        required=True,
        help="Redacted log file",
    )
    parser.add_argument(
        "--overwrite",
        action="store_true",
        help="Allow replacing an existing output file",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    source = args.input.expanduser().resolve()
    output = args.output.expanduser().resolve()

    if not source.is_file():
        raise SystemExit(
            f"Input file does not exist: {source}"
        )

    if source == output:
        raise SystemExit(
            "Input and output must be different files"
        )

    if output.exists() and not args.overwrite:
        raise SystemExit(
            f"Output already exists: {output} "
            "(use --overwrite to replace it)"
        )

    if not output.parent.is_dir():
        raise SystemExit(
            f"Output directory does not exist: {output.parent}"
        )

    try:
        original = source.read_text(encoding="utf-8")
    except UnicodeDecodeError as exc:
        raise SystemExit(
            "Input is not valid UTF-8; "
            "convert its encoding first"
        ) from exc

    redacted, counts = redact_text(original)
    output.write_text(redacted, encoding="utf-8")

    total = sum(counts.values())
    details = ", ".join(
        f"{name}={count}"
        for name, count in counts.items()
    ) or "none"

    print(f"Redacted {total} item(s): {details}")
    print(f"Wrote: {output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

脚本需要 Python 3.10 或更高版本,不依赖第三方包。

运行方法

bash 复制代码
python redact_log.py app.log -o app.redacted.log

如果目标文件已经存在,需要明确允许覆盖:

bash 复制代码
python redact_log.py app.log \
  -o app.redacted.log \
  --overwrite

假设原始日志是:

bash 复制代码
Authorization: Bearer abc.def-123456789
email=alice.dev@example.com
phone=13812345678
api_key='sk-demo-123456'
url=https://example.com/run?token=query-secret&mode=debug
ValueError: request failed

处理后会变成:

bash 复制代码
Authorization: Bearer ***
email=a***@example.com
phone=138****5678
api_key='***'
url=https://example.com/run?token=***&mode=debug
ValueError: request failed

可以看到,Token 被隐藏了,但 URL 后面的 mode=debug 仍然保留。异常类型、文件路径和行号也不会受到影响。

使用前还要注意

正则脱敏不是万能的。

项目可能使用自定义字段,例如 private_codesession_id 或业务内部密钥。遇到这种情况,需要继续往 RULES 中增加规则。

另外,脚本生成的是脱敏副本,不代表一定没有遗漏。真正发送给外部 AI 工具前,最好再人工搜索一次:

bash 复制代码
password
token
secret
key
Authorization

如果日志涉及公司源码、用户数据或其他机密,即使经过正则脱敏,也不应该直接提交到未经确认的外部服务。

Python 官方文档建议复杂场景使用预编译正则对象;subn() 还能同时返回替换后的文本和替换次数,正好适合统计本次处理了多少项。Python [re](https://docs.python.org/3/library/re.html "re") 官方文档

如果想在程序输出日志时直接脱敏,可以继续把相同规则接入 logging.Filter。过滤器可以在 Handler 输出日志前检查或修改记录。Python [logging](https://docs.python.org/3/library/logging.html#filter-objects "logging") 官方文档

如果平时长期使用 ChatGPT、Claude、Cursor 或 Kiro,也可以了解 gpt68.com。它是第三方 AI 会员充值平台,使用前应看清套餐说明、账号要求和售后规则。工具能不能写代码是一方面,提交给工具的内容是否安全,同样值得注意。

相关推荐
moMo2 小时前
考卷上有几道题?Temperature 和 Top-K 调参指南
ai编程
云原生melo荣2 小时前
Multi-Agent 系统(一):问题域与架构选型——为什么这次"固定流程"编排不动
agent·ai编程
码农胖大海3 小时前
AI 响应慢自查清单
agent·ai编程
梓䈑3 小时前
【用 Vibe Coding 实现的 C++17 在线判题系统】前端开发 + Web 自动化测试
前端·c++·ai编程
机建狂魔4 小时前
Codex 接入第三方模型 API 实战:以 Mimo 为例
java·服务器·数据库·ai·ai编程·codex
一条鱼丶6 小时前
35 个文件、14.58% 数据是空的,我让 TRAE Work 十分钟收拾干净了
ai编程
Pokerhead7 小时前
一个 Codex,能装下所有 AI 模型?
大数据·人工智能·ai·大模型·ai编程·codex
西安小哥7 小时前
AI 知识库与智能检索:高阶面试实战指南
ai编程