govdoc-md-cleaner v0.1.0: 政务文档 Markdown 清洗工具(规则引擎 + CLI + 测试)

- 8 条 YAML 声明规则:页码/页眉页脚/目录点线/图片/HTML表格/散落标签/行尾空白/空行
- 防误伤设计:protect 正则 + 内容形态豁免 + OCR burst 检测
- md-clean single/batch CLI,JSON 清洗报告
- 18 个单元测试

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:04:10 +08:00
commit c19e9fcebf
10 changed files with 814 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
"""命令行入口。
用法:
md-clean single <input.md> [-o output.md] [--diff] [--rules rules.yaml]
md-clean batch <dir> [-o outdir] [--pattern "*.md"] [--report report.json]
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import List, Optional
from cleaner.cleaner import MarkdownCleaner
from cleaner.rules import load_rules
def _build(path: Optional[Path]) -> MarkdownCleaner:
return MarkdownCleaner(rules=load_rules(path))
def cmd_single(args: argparse.Namespace) -> int:
cleaner = _build(args.rules)
src = Path(args.input)
raw = src.read_text(encoding="utf-8")
result = cleaner.clean_text(raw)
if args.output:
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(result.text, encoding="utf-8")
print(f"已写入 {args.output}")
if args.diff:
print(MarkdownCleaner.diff(raw, result.text))
print(json.dumps(result.stats, ensure_ascii=False, indent=2))
return 0
def cmd_batch(args: argparse.Namespace) -> int:
cleaner = _build(args.rules)
src_dir = Path(args.directory)
files = sorted(p for p in src_dir.rglob(args.pattern) if p.is_file())
if not files:
print(f"{src_dir} 下未找到匹配 {args.pattern} 的文件", file=sys.stderr)
return 1
out_dir = Path(args.output) if args.output else src_dir.parent / (src_dir.name + "_cleaned")
report = []
for f in files:
rel = f.relative_to(src_dir)
dst = out_dir / rel
result = cleaner.clean_file(f, dst)
report.append({"file": str(rel), "stats": result.stats})
hits = result.total_hits
print(f"[ok] {rel} 命中 {hits}")
if args.report:
Path(args.report).parent.mkdir(parents=True, exist_ok=True)
Path(args.report).write_text(
json.dumps({"files": report}, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"报告已写入 {args.report}")
print(f"共清洗 {len(files)} 个文件 → {out_dir}")
return 0
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(prog="md-clean", description="政务文档 Markdown 清洗工具")
sub = parser.add_subparsers(dest="command", required=True)
p1 = sub.add_parser("single", help="清洗单个文件")
p1.add_argument("input", help="输入 .md 文件")
p1.add_argument("-o", "--output", help="输出路径(缺省打印统计不写文件)")
p1.add_argument("--diff", action="store_true", help="打印 unified diff")
p1.add_argument("--rules", type=Path, help="规则 YAML 路径")
p2 = sub.add_parser("batch", help="批量清洗目录(递归)")
p2.add_argument("directory", help="输入目录")
p2.add_argument("-o", "--output", help="输出目录(缺省 <dir>_cleaned")
p2.add_argument("--pattern", default="*.md", help="文件 glob(默认 *.md")
p2.add_argument("--report", help="清洗报告 JSON 输出路径")
p2.add_argument("--rules", type=Path, help="规则 YAML 路径")
args = parser.parse_args(argv)
if args.command == "single":
return cmd_single(args)
return cmd_batch(args)
if __name__ == "__main__":
sys.exit(main())