브레인 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
173 lines
7.9 KiB
Python
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)}")
|