# -*- coding: utf-8 -*- """文档数据脱敏系统 - Web 平台(FastAPI, 端口 8736) 平台组负责:app.py / static / templates。 管线逻辑来自 tmkit(splitter/extractor/detector/redactor);若 tmkit 尚未就绪, 自动降级为占位逻辑(TODO 标注),保证页面可用。 """ import re import shutil import threading import time import uuid from pathlib import Path from typing import Optional from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.responses import FileResponse, HTMLResponse WORKDIR = Path(__file__).resolve().parent UPLOAD_ROOT = Path("/tmp/tm-uploads") UPLOAD_ROOT.mkdir(parents=True, exist_ok=True) # ---------------- tmkit 装载(可降级) ---------------- try: import tmkit # noqa TMKIT_READY = True TMKIT_ERR = None except Exception as e: # ImportError 或子模块缺失 tmkit = None TMKIT_READY = False TMKIT_ERR = str(e) def process_pdf(input_path: Path, out_path: Path, task: dict): """核心处理:tmkit 就绪走真管线,否则占位逻辑。返回敏感项列表。""" items = [] if TMKIT_READY: import fitz # PyMuPDF,tmkit 的底层依赖 pdf_bytes = input_path.read_bytes() # 阶段 1:分流(native / scanned) task["stage"] = "页面分流" task["progress"] = 10 page_types = tmkit.classify_pages(pdf_bytes) or [] # 阶段 2+3:逐页抽取 + PII 识别 task["stage"] = "文字抽取与敏感项识别" task["progress"] = 40 detector = tmkit.default_detector() doc = fitz.open(stream=pdf_bytes, filetype="pdf") hits, items = [], [] for i, page in enumerate(doc): page_no = i + 1 words = tmkit.extract_page_words(page) found = detector.scan_page_words(words, page_no) for h in found or []: items.append({"type": h.get("type", "未知"), "text": h.get("text", ""), "page": h.get("page", page_no)}) if h.get("bbox") is not None: hits.append({"page": h.get("page", page_no), "bbox": h["bbox"]}) # 阶段 4:脱敏回写 task["stage"] = "脱敏回写" task["progress"] = 85 out_bytes = tmkit.redact_pdf(pdf_bytes, hits) out_path.write_bytes(out_bytes) else: # ---------- TODO 占位逻辑(tmkit 未就绪) ---------- task["stage"] = "占位处理(tmkit 未就绪)" task["progress"] = 50 time.sleep(1.2) # 模拟耗时,方便前端看到进度 raw = input_path.read_bytes() # 用正则从原始字节里粗扫手机号样式,仅作演示 text = raw.decode("latin-1", errors="ignore") for m in re.finditer(r"1[3-9]\d{9}", text): items.append({"type": "手机号(占位)", "text": m.group(), "page": 1}) task["progress"] = 85 shutil.copy(input_path, out_path) # ---------- 占位结束 ---------- if not out_path.exists(): shutil.copy(input_path, out_path) return items # ---------------- 任务表 ---------------- TASKS: dict = {} # task_id -> task dict LOCK = threading.Lock() def _run_task(task_id: str, input_path: Path, out_path: Path): task = TASKS[task_id] try: items = process_pdf(input_path, out_path, task) with LOCK: task.update(status="done", stage="完成", progress=100, items=items, finished_at=time.time()) except Exception as e: with LOCK: task.update(status="error", stage="失败", error=str(e)) app = FastAPI(title="文档数据脱敏系统") @app.get("/api/tasks") def list_tasks(task_id: Optional[str] = None): with LOCK: if task_id: if task_id not in TASKS: raise HTTPException(404, "task not found") return TASKS[task_id] return {"tmkit_ready": TMKIT_READY, "tmkit_error": TMKIT_ERR, "tasks": list(TASKS.values())} @app.post("/api/process") async def process(file: UploadFile = File(...)): if not (file.filename or "").lower().endswith(".pdf"): raise HTTPException(400, "仅支持 PDF 文件") task_id = uuid.uuid4().hex[:12] tdir = UPLOAD_ROOT / task_id tdir.mkdir(parents=True, exist_ok=True) input_path = tdir / "input.pdf" out_path = tdir / "redacted.pdf" with open(input_path, "wb") as f: shutil.copyfileobj(file.file, f) TASKS[task_id] = { "task_id": task_id, "filename": file.filename, "status": "processing", "stage": "排队中", "progress": 5, "items": [], "created_at": time.time(), "dir": str(tdir), } threading.Thread(target=_run_task, args=(task_id, input_path, out_path), daemon=True).start() return {"task_id": task_id} @app.get("/download/{task_id}") def download(task_id: str, type: str = "redacted"): task = TASKS.get(task_id) if not task: raise HTTPException(404, "task not found") if task.get("status") != "done": raise HTTPException(409, "任务未完成") tdir = Path(task["dir"]) src = tdir / ("redacted.pdf" if type == "redacted" else "input.pdf") if not src.exists(): raise HTTPException(404, "文件不存在") if type == "redacted": name = f"{Path(task['filename']).stem}_脱敏.pdf" else: name = task["filename"] return FileResponse(src, filename=name) @app.get("/progress", response_class=HTMLResponse) def progress_page(): blocks = [] for key, label in (("backend", "管线组"), ("web", "平台组(Web)"), ("deploy", "部署组")): p = WORKDIR / f"PROGRESS-{key}.md" content = p.read_text(encoding="utf-8") if p.exists() else "> 进行中(暂无进展文件)" blocks.append('
{}' '
{}
'.format(label, content.replace("&", "&").replace("<", "<"))) return """ 开发进展看板 · 文档数据脱敏系统
文档数据脱敏系统 · 开发进展看板
""" + "".join(blocks) + """
""" @app.get("/", response_class=HTMLResponse) def index(): return INDEX_HTML INDEX_HTML = """ 文档数据脱敏系统

📄 文档数据脱敏系统

上传 PDF,自动识别并脱敏敏感信息(手机号 / 身份证 / 银行卡等)

📁
将 PDF 拖拽到此处,或点击选择文件
""" if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8736)