Files
Aislo/resources/tester/_pdf_gap_report.py
T
eomsangdonandClaude Opus 5 5de4eeb505 docs(verification): 건설 PDF ↔ md 구멍을 장별 목록으로 — 결손 749 · 허구 547
브레인 B18 ①. 총량만으로는 누가 무엇을 고칠지 몰라 **쪽 · 표 제목 · 줄 글자**를 담아 장별로 나눔.

- `resources/tester/_pdf_gap_report.py` — 값 셈은 커버리지 시험 잣대를 그대로 씀(두 벌 금지)
- 목록 `docs/raw/verification/2026-09-18_건설PDF대조/` (목차 + 장별 43 파일 · 699 줄)
  · 결손 = PDF 에 있는데 md 에 없음 · 허구 = md 에만(대개 붙은 줄 — 같은 자리에서 짝을 이룸)
  · 큰 것: 제13장 플랜트 271 · 제8장 건설기계 122 · 제2장 토목 120 · 제4장 기계설비 51
- 가짜 양성 둘 다듬음
  · 쪽 머리 줄(「제13장 … 723」)·붙은 머리(`173제8장 건설기계`)는 목록에서 가림
    ⚠ **셈에서는 안 뺌** — md 도 머리를 본문에 붙여 적어 한쪽만 빼면 허구가 늘어남(749/547 → 600/929)
  · 표 제목 칸이 자료 줄을 집던 것 — **글자가 둘 이상 있어야 제목**으로 봄
  · 쪽 머리를 먼저 훑어 「장 모름」 474 → 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-18 16:48:56 +09:00

173 lines
7.9 KiB
Python

# -*- coding: utf-8 -*-
"""건설 PDF ↔ md 구멍을 **장별 목록 파일**로 뽑는다 — 2026-09-18 브레인 B18.
왜 — 구멍 총량(`test_const_pdf_coverage.py`)만으로는 누가 무엇을 고칠지 모른다.
**쪽 · 표 제목 · 줄 글자**를 담아 장별로 나눠 두면 md 를 고치는 창(랩탑서브·안티그래비티)이
제 몫만 열어 고칠 수 있다.
담는 것
- **결손** — PDF 줄에 있는 값이 md 에 없음. 줄 글자를 그대로 적어 어디를 옮겨 적을지 보임.
- **허구** — md 줄에만 있는 값. 대개 **붙은 줄**(`0230-00020.23,0008900.90`)이라 같은 자리에서
결손과 짝을 이룬다 — 그 줄을 풀면 둘이 함께 사라진다.
⚠ 값 셈은 `test_const_pdf_coverage.py` 의 잣대를 그대로 쓴다(두 벌 금지).
돌리기: `./venv/Scripts/python.exe resources/tester/_pdf_gap_report.py`
"""
from __future__ import annotations
import collections
import importlib.util
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "docs/raw/verification/2026-09-18_건설PDF대조"
_spec = importlib.util.spec_from_file_location(
"cov", Path(__file__).with_name("test_const_pdf_coverage.py")
)
cov = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(cov)
#: 쪽 머리의 장 이름 — 「제8장 건설기계」·「공통부문」.
_CHAPTER = re.compile(r"제\s*(\d+)\s*장\s*([가-힣]{2,10})")
#: 표 제목 자리 — 「(0230) 대형 브레이커」·「8-3-1 …」·「2. 전동환산(q 표)」.
#: ⚠ **글자가 있어야 제목**이다 — 수만 늘어선 자료 줄을 제목으로 잡으면 목록이 헷갈린다.
_CAPTION = re.compile(r"^\(?\d[\d\-.]*\)?[.\s]*[가-힣A-Za-z]")
_HANGUL = re.compile(r"[가-힣]")
def _missing_lines(limit_pages: int | None = None) -> dict[str, list[dict]]:
"""장별 결손 줄 — `{장: [{쪽, 제목, 줄, 빠진값}]}`."""
import pymupdf
left = collections.Counter(cov.md_numbers()) # 아직 안 쓴 md 값
found: dict[str, list[dict]] = collections.defaultdict(list)
chapter = "장 모름" # 쪽 머리가 없는 쪽은 앞 쪽의 장을 잇는다
with pymupdf.open(cov.PDF) as doc:
for number, page in enumerate(doc, start=1):
if limit_pages and number > limit_pages:
break
rows = cov._page_rows(page)
# ⚠ **쪽 머리를 먼저 훑는다** — 쪽 머리가 줄 차례의 첫 줄이 아니라서(좌표 정렬)
# 차례대로 보면 앞 줄들이 「장 모름」으로 떨어진다.
for line in rows:
if m := _CHAPTER.search(line):
chapter = f"제{m.group(1)}{m.group(2)}".strip()
break
caption = ""
for line in rows:
if cov._LEADER.search(line):
continue
tokens = [
cov._norm(x) for x in cov._NUM.findall(line) if cov._valuish(cov._norm(x))
]
if not tokens or cov._ONLY_NUMBER.fullmatch(line.strip()):
# 값이 없는 줄은 **표 제목 후보**로 기억한다
text = line.strip()
if _CAPTION.match(text) and len(_HANGUL.findall(text)) >= 2:
caption = text[:60]
continue
missing = []
for token in tokens:
if left[token] > 0:
left[token] -= 1
else:
missing.append(token)
if missing and not cov._PAGE_HEAD.search(line): # 쪽 머리 줄은 목록에서 가림
found[chapter].append(
{"쪽": number, "제목": caption, "줄": line.strip()[:120], "빠진값": missing}
)
return found
def _invented_lines() -> dict[str, list[dict]]:
"""장별 허구 줄 — md 에만 있는 값이 든 줄(대개 붙은 줄)."""
left = collections.Counter(cov.pdf_numbers()[0])
found: dict[str, list[dict]] = collections.defaultdict(list)
chapter = "장 모름"
for number, line in enumerate(cov.MD.read_text(encoding="utf-8").splitlines(), start=1):
if m := _CHAPTER.search(line):
chapter = f"제{m.group(1)}{m.group(2)}".strip()
if cov._LEADER.search(line):
continue
tokens = [cov._norm(x) for x in cov._NUM.findall(line) if cov._valuish(cov._norm(x))]
if not tokens or cov._ONLY_NUMBER.fullmatch(line.strip()):
continue
extra = []
for token in tokens:
if left[token] > 0:
left[token] -= 1
else:
extra.append(token)
# 쪽 머리가 본문에 붙은 줄(`173제8장 건설기계`)은 고칠 거리가 아니라 가린다
if extra and not (cov._PAGE_HEAD.search(line) and len(line.strip()) < 30):
found[chapter].append({"md줄": number, "줄": line.strip()[:160], "없는값": extra})
return found
def write() -> dict[str, int]:
missing, invented = _missing_lines(), _invented_lines()
OUT.mkdir(parents=True, exist_ok=True)
for stale in OUT.glob("*.md"): # 다시 뽑을 때 옛 목록이 남지 않게 — 이 폴더는 이 스크립트 것
stale.unlink()
counts: dict[str, int] = {}
for chapter in sorted(set(missing) | set(invented)):
safe = re.sub(r"[^\w가-힣]+", "_", chapter).strip("_") or "장모름"
rows_missing, rows_invented = missing.get(chapter, []), invented.get(chapter, [])
counts[chapter] = len(rows_missing) + len(rows_invented)
lines = [
f"# {chapter} — PDF ↔ md 구멍 (2026-09-18 뽑음)",
"",
f"결손 줄 {len(rows_missing)} · 허구 줄 {len(rows_invented)}",
"",
"## 결손 — PDF 에 있는데 md 에 없음",
"",
"| 쪽 | 표 제목 | PDF 줄 | 빠진 값 |",
"| --- | --- | --- | --- |",
]
for row in rows_missing:
line = row["줄"].replace("|", "·")
lines.append(
f"| {row['쪽']} | {row['제목'].replace('|', '·')} | {line} | {' '.join(row['빠진값'])} |"
)
lines += [
"",
"## 허구 — md 에만 있음 (대개 붙은 줄 · 위 결손과 같은 자리)",
"",
"| md 줄 | 줄 글자 | PDF 에 없는 값 |",
"| --- | --- | --- |",
]
for row in rows_invented:
lines.append(
f"| {row['md줄']} | {row['줄'].replace('|', '·')} | {' '.join(row['없는값'])} |"
)
(OUT / f"{safe}.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
index = [
"# 건설 PDF ↔ md 구멍 — 장별 목록 (2026-09-18 뽑음)",
"",
"쓰는 법 — 제 장 파일을 열어 **결손** 줄을 PDF 그 쪽에서 확인해 md 에 옮겨 적고,",
"**허구** 줄(대개 붙은 줄)은 PDF 대로 풀어 적는다. 고친 뒤 다시 뽑으면 줄어든다.",
"",
"```",
"다시 뽑기 ./venv/Scripts/python.exe resources/tester/_pdf_gap_report.py",
"총량 시험 ./venv/Scripts/python.exe -m pytest resources/tester/test_const_pdf_coverage.py",
"```",
"",
"| 장 | 줄 수 | 파일 |",
"| --- | --- | --- |",
]
for chapter, count in sorted(counts.items(), key=lambda kv: -kv[1]):
name = re.sub(r"[^\w가-힣]+", "_", chapter).strip("_") or "장모름"
index.append(f"| {chapter} | {count} | [{name}.md]({name}.md) |")
(OUT / "000_목차.md").write_text("\n".join(index) + "\n", encoding="utf-8")
return counts
if __name__ == "__main__":
got = write()
for chapter, count in sorted(got.items(), key=lambda kv: -kv[1]):
print(f"{count:5d} {chapter}")
print(f"합 {sum(got.values())} 줄 · {OUT.relative_to(ROOT)}")