190 lines
6.7 KiB
Python
190 lines
6.7 KiB
Python
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import zipfile
|
||
from collections import defaultdict
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
from xml.etree import ElementTree as ET
|
||
|
||
|
||
WORD_NS = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
|
||
ARTICLE_RE = re.compile(r"^第[一二三四五六七八九十百千万零〇两0-9]+条(?:之[一二三四五六七八九十百千万零〇两0-9]+)?")
|
||
DIVISION_RE = re.compile(r"^第[一二三四五六七八九十百千万零〇两0-9]+分编")
|
||
CHAPTER_RE = re.compile(r"^第[一二三四五六七八九十百千万零〇两0-9]+章")
|
||
SECTION_RE = re.compile(r"^第[一二三四五六七八九十百千万零〇两0-9]+节")
|
||
|
||
SOURCE_ORDER = [
|
||
"LAW-CIVIL-CODE-2020",
|
||
"LAW-E-SIGNATURE-2019",
|
||
"LAW-PIPL-2021",
|
||
"LAW-LABOR-CONTRACT-2012",
|
||
"LAW-DATA-SECURITY-2021",
|
||
"LAW-COMPANY-2023",
|
||
]
|
||
|
||
|
||
def read_json(path: Path) -> dict[str, Any]:
|
||
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
||
|
||
def parse_docx_paragraphs(path: Path) -> list[str]:
|
||
with zipfile.ZipFile(path) as archive:
|
||
root = ET.fromstring(archive.read("word/document.xml"))
|
||
paragraphs: list[str] = []
|
||
for node in root.findall(".//w:p", WORD_NS):
|
||
text = "".join((text_node.text or "") for text_node in node.findall(".//w:t", WORD_NS)).strip()
|
||
if text:
|
||
paragraphs.append(text)
|
||
return paragraphs
|
||
|
||
|
||
def build_catalog(paragraphs: list[str]) -> list[dict[str, str]]:
|
||
division = ""
|
||
chapter = ""
|
||
section = ""
|
||
catalog: list[dict[str, str]] = []
|
||
for text in paragraphs:
|
||
if DIVISION_RE.match(text):
|
||
division = text
|
||
chapter = ""
|
||
section = ""
|
||
continue
|
||
if CHAPTER_RE.match(text):
|
||
chapter = text
|
||
section = ""
|
||
continue
|
||
if SECTION_RE.match(text):
|
||
section = text
|
||
continue
|
||
if ARTICLE_RE.match(text):
|
||
article_no = ARTICLE_RE.match(text).group(0)
|
||
catalog.append(
|
||
{
|
||
"article_no": article_no,
|
||
"division": division,
|
||
"chapter_title": chapter,
|
||
"section_title": section,
|
||
}
|
||
)
|
||
return catalog
|
||
|
||
|
||
def latest_docx_by_source(artifact_root: Path) -> dict[str, Path]:
|
||
mapping: dict[str, Path] = {}
|
||
for path in artifact_root.glob("**/official-downloads/*/official.docx"):
|
||
source_id = path.parent.name
|
||
current = mapping.get(source_id)
|
||
if current is None or path.stat().st_mtime > current.stat().st_mtime:
|
||
mapping[source_id] = path
|
||
return mapping
|
||
|
||
|
||
def load_imported_articles(phase14_root: Path) -> tuple[dict[str, set[str]], list[Path]]:
|
||
manifest_paths = sorted(phase14_root.glob("legal-article*.json"))
|
||
incremental_root = phase14_root / "incremental-batches"
|
||
if incremental_root.exists():
|
||
manifest_paths.extend(sorted(incremental_root.glob("*.json")))
|
||
|
||
imported: dict[str, set[str]] = defaultdict(set)
|
||
for path in manifest_paths:
|
||
payload = read_json(path)
|
||
for source in payload.get("sources") or []:
|
||
for article in source.get("articles") or []:
|
||
imported[str(source["source_id"])].add(str(article["article_no"]))
|
||
return imported, manifest_paths
|
||
|
||
|
||
def write_json(path: Path, payload: Any) -> None:
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
|
||
def write_md(path: Path, summary: dict[str, Any], per_source: list[dict[str, Any]]) -> None:
|
||
lines = [
|
||
"# Phase14 剩余未导条文目录",
|
||
"",
|
||
f"- 生成时间:`{summary['generated_at']}`",
|
||
f"- 基线 manifest:`{summary['baseline_manifest_count']}` 个",
|
||
f"- 官方 docx 来源:`{summary['docx_source_count']}` 个",
|
||
"",
|
||
"## 汇总",
|
||
"",
|
||
"| source_id | total | imported | remaining | latest_docx |",
|
||
"|---|---:|---:|---:|---|",
|
||
]
|
||
for item in per_source:
|
||
lines.append(
|
||
f"| {item['source_id']} | {item['catalog_count']} | {item['imported_count']} | {item['remaining_count']} | {item['docx_path']} |"
|
||
)
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## 剩余样例",
|
||
"",
|
||
]
|
||
)
|
||
for item in per_source:
|
||
lines.append(f"### {item['source_id']}")
|
||
lines.append("")
|
||
if not item["remaining"]:
|
||
lines.append("- 无剩余条文。")
|
||
lines.append("")
|
||
continue
|
||
lines.append("| article_no | division | chapter | section |")
|
||
lines.append("|---|---|---|---|")
|
||
for row in item["remaining"][:30]:
|
||
lines.append(
|
||
f"| {row['article_no']} | {row['division']} | {row['chapter_title']} | {row['section_title']} |"
|
||
)
|
||
lines.append("")
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text("\n".join(lines), encoding="utf-8")
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Report remaining Phase14 legal articles not yet imported.")
|
||
parser.add_argument("--phase14-root", type=Path, required=True)
|
||
parser.add_argument("--artifact-root", type=Path, required=True)
|
||
parser.add_argument("--output-dir", type=Path, required=True)
|
||
args = parser.parse_args()
|
||
|
||
imported, manifest_paths = load_imported_articles(args.phase14_root)
|
||
docx_by_source = latest_docx_by_source(args.artifact_root)
|
||
|
||
per_source: list[dict[str, Any]] = []
|
||
for source_id in SOURCE_ORDER:
|
||
docx_path = docx_by_source.get(source_id)
|
||
if docx_path is None:
|
||
continue
|
||
catalog = build_catalog(parse_docx_paragraphs(docx_path))
|
||
imported_set = imported.get(source_id, set())
|
||
remaining = [row for row in catalog if row["article_no"] not in imported_set]
|
||
per_source.append(
|
||
{
|
||
"source_id": source_id,
|
||
"docx_path": str(docx_path),
|
||
"catalog_count": len(catalog),
|
||
"imported_count": len(imported_set),
|
||
"remaining_count": len(remaining),
|
||
"remaining": remaining,
|
||
}
|
||
)
|
||
|
||
summary = {
|
||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||
"baseline_manifest_count": len(manifest_paths),
|
||
"docx_source_count": len(docx_by_source),
|
||
}
|
||
write_json(args.output_dir / "phase14-remaining-articles.json", {"summary": summary, "sources": per_source})
|
||
write_md(args.output_dir / "phase14-remaining-articles.md", summary, per_source)
|
||
print(json.dumps({"summary": summary, "sources": per_source}, ensure_ascii=False, indent=2))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|