# -*- 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('{}
'
'{}
上传 PDF,自动识别并脱敏敏感信息(手机号 / 身份证 / 银行卡等)