Download scripts/intake_filter.py from logos42/headless: direct link, hf CLI and curl.
- Browser
- Download file 6.47 kB
-
https://huggingface.co/logos42/headless/resolve/main/scripts/intake_filter.py
- Command line
-
hf download hf://logos42/headless/scripts/intake_filter.py
-
curl -L -o intake_filter.py https://huggingface.co/logos42/headless/resolve/main/scripts/intake_filter.py
6.47 kB
| from __future__ import annotations | |
| # llm-wiki-version: 2.0.0 | |
| # runtime: dev-only | |
| """ | |
| intake_filter.py — v2 intake privacy filter. | |
| Rohit G00 v2 范式:raw 摄入前脱敏,wiki 写入时按 audience 分级。 | |
| v2.0 实现: | |
| 1. 读 manifests/redact_rules.yaml(每条 = name + pattern + replacement) | |
| 2. 扫 raw 根目录(默认 ../<project>_raw/,可 --raw-root 覆盖) | |
| 3. 对每个 .md/.txt/.csv/.json 文件应用正则替换 | |
| 4. 在 manifests/redaction_log.md 写日志(每文件命中几条) | |
| 5. 退出口为 0(仅警告),CI 默认不阻塞 | |
| Usage: | |
| python3 scripts/intake_filter.py | |
| python3 scripts/intake_filter.py --dry-run | |
| python3 scripts/intake_filter.py --raw-root ../my_raw | |
| python3 scripts/intake_filter.py --rules manifests/redact_rules.yaml | |
| """ | |
| import argparse | |
| import os | |
| import re | |
| import sys | |
| from datetime import date | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| DEFAULT_RULES = ROOT / "manifests" / "redact_rules.yaml" | |
| DEFAULT_LOG = ROOT / "manifests" / "redaction_log.md" | |
| # 默认 deny extensions / max size (写在脚本里,不进 rules.yaml 避免 YAML 解析复杂) | |
| DEFAULT_DENY_EXT = {".zip", ".rar", ".7z", ".tar", ".gz"} | |
| DEFAULT_MAX_SIZE_MB = 200 | |
| # 10 行内建 PII 正则(无外部依赖) | |
| BUILTIN_PATTERNS = [ | |
| ("phone_cn", re.compile(r"1[3-9]\d{9}"), "[PHONE]"), | |
| ("email", re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), "[EMAIL]"), | |
| ("id_card_cn", re.compile(r"\d{17}[\dXx]"), "[ID]"), | |
| ("credit_card", re.compile(r"\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}"), "[CC]"), | |
| ] | |
| def parse_yaml_rules(path: Path) -> list[tuple[str, re.Pattern, str]]: | |
| """Minimal YAML parser: each rule is a block of 3 lines (name/pattern/replacement).""" | |
| if not path.exists(): | |
| return [] | |
| rules: list[tuple[str, re.Pattern, str]] = [] | |
| block: dict[str, str] = {} | |
| for line in path.read_text(encoding="utf-8").splitlines(): | |
| line_stripped = line.strip() | |
| if not line_stripped or line_stripped.startswith("#"): | |
| continue | |
| if line_stripped.startswith("- name:"): | |
| if block: | |
| _flush(block, rules) | |
| block = {"name": line_stripped.split(":", 1)[1].strip()} | |
| elif line_stripped.startswith("pattern:") and block: | |
| block["pattern"] = line_stripped.split(":", 1)[1].strip().strip('"').strip("'") | |
| elif line_stripped.startswith("replacement:") and block: | |
| block["replacement"] = line_stripped.split(":", 1)[1].strip().strip('"').strip("'") | |
| if block: | |
| _flush(block, rules) | |
| return rules | |
| def _flush(block: dict[str, str], rules: list) -> None: | |
| name = block.get("name", "unnamed") | |
| pat = block.get("pattern", "") | |
| repl = block.get("replacement", "[REDACTED]") | |
| if not pat: | |
| return | |
| try: | |
| rules.append((name, re.compile(pat), repl)) | |
| except re.error: | |
| pass # 跳过非法正则 | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="v2 intake privacy filter") | |
| parser.add_argument("--raw-root", type=Path, | |
| default=lambda: Path(os.environ.get("PROJECT_RAW_ROOT", ROOT.parent / "_raw")), | |
| help="raw root directory to scan (env PROJECT_RAW_ROOT or --raw-root override; default: ../_raw)") | |
| parser.add_argument("--rules", type=Path, default=DEFAULT_RULES, | |
| help="redact_rules.yaml path (optional, falls back to builtins)") | |
| parser.add_argument("--dry-run", action="store_true") | |
| parser.add_argument("--log", type=Path, default=DEFAULT_LOG) | |
| args = parser.parse_args() | |
| if not args.raw_root.exists(): | |
| print(f"intake_filter: raw_root {args.raw_root} does not exist, skipping") | |
| return 0 | |
| # Load rules: yaml first, then builtins | |
| rules = parse_yaml_rules(args.rules) or BUILTIN_PATTERNS | |
| print(f"intake_filter: {len(rules)} patterns loaded ({'yaml' if args.rules.exists() else 'builtin'})") | |
| # Scan | |
| hits: list[dict] = [] | |
| scanned = 0 | |
| skipped = 0 | |
| for path in args.raw_root.rglob("*"): | |
| if not path.is_file(): | |
| continue | |
| scanned += 1 | |
| if path.suffix.lower() in DEFAULT_DENY_EXT: | |
| skipped += 1 | |
| continue | |
| size_mb = path.stat().st_size / (1024 * 1024) | |
| if size_mb > DEFAULT_MAX_SIZE_MB: | |
| skipped += 1 | |
| continue | |
| try: | |
| text = path.read_text(encoding="utf-8", errors="ignore") | |
| except Exception: | |
| continue | |
| file_hits: dict[str, int] = {} | |
| new_text = text | |
| for name, pat, repl in rules: | |
| count = len(pat.findall(new_text)) | |
| if count > 0: | |
| file_hits[name] = count | |
| new_text = pat.sub(repl, new_text) | |
| if file_hits: | |
| if not args.dry_run: | |
| # 仅当命中数 > 0 时回写 | |
| path.write_text(new_text, encoding="utf-8") | |
| hits.append({"file": str(path.relative_to(args.raw_root)), "hits": file_hits}) | |
| # Write log | |
| today = date.today().isoformat() | |
| log_lines = [f"# Intake Redaction Log", "", | |
| f"- date: {today}", | |
| f"- raw_root: {args.raw_root}", | |
| f"- rules: {args.rules} ({'yaml' if args.rules.exists() else 'builtin'})", | |
| f"- dry_run: {args.dry_run}", | |
| f"- scanned: {scanned}, skipped: {skipped}, modified: {len(hits)}", | |
| ""] | |
| if hits: | |
| log_lines.append("## Files modified") | |
| log_lines.append("") | |
| for h in hits: | |
| log_lines.append(f"- `{h['file']}`: " + ", ".join(f"{k}={v}" for k, v in h["hits"].items())) | |
| else: | |
| log_lines.append("## No PII / redact patterns matched.") | |
| if not args.dry_run: | |
| args.log.parent.mkdir(parents=True, exist_ok=True) | |
| args.log.write_text("\n".join(log_lines) + "\n", encoding="utf-8") | |
| # Report | |
| print(f"- scanned: {scanned}") | |
| print(f"- skipped: {skipped} (deny ext or >{DEFAULT_MAX_SIZE_MB}MB)") | |
| print(f"- modified: {len(hits)}") | |
| if hits: | |
| for h in hits[:10]: | |
| print(f" - {h['file']}: " + ", ".join(f"{k}={v}" for k, v in h["hits"].items())) | |
| if len(hits) > 10: | |
| print(f" ... and {len(hits) - 10} more") | |
| if not args.dry_run: | |
| print(f"- log: {args.log}") | |
| else: | |
| print("- dry-run: no files written") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |