412 lines
19 KiB
Python
412 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""품셈 md 도구 — 기계검사(check): 수 대조 · 글자 대조 (`pum_md_tool` 에서 나눔 · 밖에서는 `pum_md_tool` 로 부름)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import collections
|
||
import difflib
|
||
import hashlib
|
||
import re
|
||
import unicodedata
|
||
from pathlib import Path
|
||
|
||
from pum_md_base import ( # noqa: E402,F401 — 바탕(책 설정·줄 읽기·머리 찾기)은 pum_md_base.py
|
||
_NUM,
|
||
BOOKS,
|
||
COST,
|
||
HEAD_KEYS,
|
||
ROOT,
|
||
STATES,
|
||
_page_rows,
|
||
find_heading,
|
||
heading_pattern,
|
||
join_spaced,
|
||
key,
|
||
locate,
|
||
read_rows,
|
||
visual_text,
|
||
)
|
||
|
||
from pum_md_plan import _is_footer, _side_number, read_head # noqa: E402
|
||
|
||
# ── check ──────────────────────────────────────────────────────────────
|
||
#: md 문법 — 글자 대조·수 대조에서 뺌(원문 글자가 아님)
|
||
_COMMENT = re.compile(r"<!--.*?-->", re.S)
|
||
_IMAGE = re.compile(r"!\[[^\]]*\]\([^)]*\)")
|
||
_TABLE_RULE = re.compile(r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$")
|
||
#: 막음표 — 원문 글자가 md 문법으로 읽히지 않게 앞에 `\` 를 붙인 것(`\*` `\-` `\#` `\>` `\|` …)
|
||
_ESCAPED = re.compile(r"\\([\\`*_{}\[\]()#+\-.!|>~^])")
|
||
_HOLD = "\x00"
|
||
#: 글자층 밖 — PDF 글자층의 사용자 영역 글리프(U+E000–F8FF)는 뜻 없는 코드(식의 「(m³)」·분수선).
|
||
#: ①② 에서 빼고 개수·쪽을 기계검사 칸에 드러냄(③ 이 그림으로 봄). md 에서 그 자리를 받아쓴 글자는
|
||
#: `<!-- 글자층 밖 -->…<!-- /글자층 밖 -->` 로 감싸면 ①② 에서 빼고 「받아씀 N자」로 드러냄.
|
||
_PUA = re.compile("[\ue000-\uf8ff]")
|
||
_OFF_OPEN, _OFF_CLOSE = "\x01", "\x02"
|
||
_OFF_MARK = re.compile(r"<!--\s*(/?)글자층 밖\s*-->")
|
||
|
||
|
||
def _same_han(ch: str) -> str:
|
||
"""호환 한자(U+F900–FAFF) → 통합 한자(率 U+F9DB → 率 U+7387). ⚠ NFKC 통째로는 안 씀 — ㎥·①·㎡ 가 풀림."""
|
||
return unicodedata.normalize("NFKC", ch) if "\uf900" <= ch <= "\ufaff" else ch
|
||
|
||
|
||
def book_of(path: Path) -> dict:
|
||
path = path.resolve()
|
||
for book in BOOKS.values():
|
||
if path.is_relative_to(book["out"].resolve()):
|
||
return book
|
||
raise ValueError(f"본문/ 아래 파일이 아님: {path}")
|
||
|
||
|
||
def split_head(text: str) -> tuple[str, str]:
|
||
"""(머리, 몸) — 머리는 첫 `---` 부터 다음 `---` 까지."""
|
||
lines = text.split("\n")
|
||
end = lines.index("---", 1)
|
||
return "\n".join(lines[: end + 1]), "\n".join(lines[end + 1 :])
|
||
|
||
|
||
def md_lines(body: str) -> list[tuple[int, str]]:
|
||
"""몸의 (줄 번호, 원문 글자) — 주석 · 그림 · 표 구분 줄 · md 문법 글자를 뺌. 줄 번호는 몸 기준 1부터.
|
||
|
||
쪽을 넘은 표가 되풀이 찍은 머리 줄은 md 표에 아예 안 적음(규칙 4-5) — `pdf_lines` 가 스스로 뺌.
|
||
"""
|
||
body = _OFF_MARK.sub(lambda m: _OFF_CLOSE if m.group(1) else _OFF_OPEN, body)
|
||
body = _COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), body)
|
||
out = []
|
||
for number, line in enumerate(body.split("\n"), start=1):
|
||
if _TABLE_RULE.match(line):
|
||
continue
|
||
line = _IMAGE.sub("", line).replace("<br>", " ")
|
||
held = _ESCAPED.findall(line) # 막음표 뒤 원문 글자는 아래 문법 떼기에서 지킴
|
||
line = _ESCAPED.sub(_HOLD, line)
|
||
line = re.sub(r"^\s*#+\s", "", line) # 제목
|
||
line = re.sub(r"^\s*(>\s*)+", "", line) # 인용
|
||
line = re.sub(r"[|^_]", " ", line) # 표 칸 · 첨자 표시(`10^-7` · `C_m`)
|
||
for ch in held:
|
||
line = line.replace(_HOLD, ch, 1)
|
||
out.append((number, line))
|
||
return out
|
||
|
||
|
||
#: 항 경계 — 「9-17-4 제목」·「(7505) 제목」 같은 줄이 나오면 그 앞까지 본 줄은 잊음(딴 항의 흔한 머리말과
|
||
#: 안 헷갈리게). 분류번호(4자리-4자리)는 안 걸리게 앞 수를 1~2자리로 잡음.
|
||
_ITEM_HEAD = re.compile(r"^\d{1,2}-\d{1,2}(?:-\d{1,2})?\s\S|^\(\d+\)\s\S")
|
||
#: 되풀이 머리로 안 보는 줄 — 목·항 머리, 번호 매긴 줄, 「[주]」·「[참고]」·「<…>」. 표 머리는 이런 모양이 아님.
|
||
#: 여기서 되풀이 빼기를 멈춤 — 같은 글이 앞에 또 있어도 새 표·새 목의 첫 줄이므로 빼면 안 됨.
|
||
_NOT_REPEAT = re.compile(
|
||
r"^\d{1,2}-\d{1,2}(?:-\d{1,2})?\s" # 13-6-3 수문 제작
|
||
r"|^\(\d+\)\s" # (1) 제목 · (7505) 분류
|
||
r"|^\d{1,2}\.\s" # 1. 제목
|
||
r"|^[가-힣]\.\s" # 가. 제목
|
||
r"|^[①-⑳㉮-㉻]" # ① ㉮
|
||
r"|^\[[가-힣]{1,6}\]" # [주] [참고] [설계예]
|
||
r"|^[<※◦○●□■]" # <…> ※ ◦ ○ ● □ ■
|
||
)
|
||
|
||
|
||
def _chars(row: str) -> collections.Counter:
|
||
"""그 줄의 글자 셈 — 글자층 밖 글리프·공백을 뺀 뒤 호환 한자를 통합 한자로(②글자 대조와 같은 잣대)."""
|
||
kept, _ = _split_off([(0, row)])
|
||
return collections.Counter(_tight(kept)[0])
|
||
|
||
|
||
def _repeat_budget(rows: list[tuple[int, str]], body: str) -> collections.Counter:
|
||
"""되풀이 머리로 뺄 수 있는 글자 셈 — PDF 가 md 보다 더 가진 만큼.
|
||
|
||
md 는 되풀이된 머리를 안 적으므로(규칙 4-5) 그 차이가 곧 뺄 몫 — 이 몫을 넘겨 빼지 않음.
|
||
딴 표의 흔한 머리말이 우연히 앞 글과 같아도 몫이 없으면 안 빠짐.
|
||
"""
|
||
kept_pdf, _ = _split_off(rows)
|
||
kept_md, _ = _split_off(md_lines(body))
|
||
return collections.Counter(_tight(kept_pdf)[0]) - collections.Counter(_tight(kept_md)[0])
|
||
|
||
|
||
def pdf_lines(
|
||
doc, head: dict[str, str], footer: re.Pattern, body: str = ""
|
||
) -> list[tuple[int, str]]:
|
||
"""그 절 PDF 글자층의 (PDF 쪽, 줄) — 시작표지 줄부터 끝표지 줄 앞까지 · 머리 PDF쪽 밖은 안 봄 · 쪽 꼬리 뺌.
|
||
|
||
쪽을 넘거나(다음 쪽 맨 위) 한 쪽 안에서 「→」로 끊긴 표가 되풀이 찍은 머리 줄(규칙 4-5)은 스스로 뺌.
|
||
셋을 다 갖춘 줄만 뺌 — 하나라도 어긋나면 그 자리에서 빼기를 멈춤:
|
||
|
||
① 이번 항에서 이미 나온 글자와 공백 빼고 이어 붙음(줄 나눔은 안 가림 — 쪽마다 줄 묶임이 달라도 잡음).
|
||
새 항이 시작하면(「9-17-4 …」·「(7505) …」) 이미 본 글자를 잊음.
|
||
② 목·항 머리 모양이 아님 — 「1.」·「가.」·「①」·「[주]」·「<…>」 로 시작하는 줄은 새 표·새 목의 첫 줄.
|
||
③ md 가 못 가진 글자 몫 안임(`body` 를 줬을 때) — PDF 가 md 보다 더 가진 만큼만 뺌.
|
||
"""
|
||
first, _, last = head["PDF쪽"].partition("~")
|
||
first, last = int(first), int(last or first)
|
||
start_page, start_row = locate(doc, head["시작표지"], first, slack=0)
|
||
end_page, end_row = last + 1, 0
|
||
if head["끝표지"] and head["끝표지"] != "끝":
|
||
try:
|
||
end_page, end_row = locate(doc, head["끝표지"], last, slack=1)
|
||
except ValueError:
|
||
pass
|
||
raw: list[tuple[int, str, bool]] = [] # (쪽, 줄, 이어진 자리의 첫 줄인가)
|
||
for number in range(start_page, min(end_page, last) + 1):
|
||
rows = [row.strip() for row in read_rows(doc[number - 1])]
|
||
if rows and _is_footer(doc[number - 1], rows[-1], footer):
|
||
rows[-1] = "" # 쪽 꼬리
|
||
if rows and _side_number(doc[number - 1], rows[0]):
|
||
rows[0] = "" # 눕힌 쪽 옆 띠 쪽 번호
|
||
fresh = number > start_page # 다음 쪽 맨 위
|
||
for index, row in enumerate(rows):
|
||
if (number, index) < (start_page, start_row) or (number, index) >= (end_page, end_row):
|
||
continue
|
||
if not row:
|
||
continue
|
||
if row == "→": # 한 쪽 안에서 표가 끊긴 자리
|
||
fresh = True
|
||
continue
|
||
raw.append((number, row, fresh))
|
||
fresh = False
|
||
budget = _repeat_budget([(n, r) for n, r, _ in raw], body) if body else None
|
||
gone, later = [False] * len(raw), []
|
||
since_head, at = "", 0
|
||
while at < len(raw):
|
||
number, row, fresh = raw[at]
|
||
if fresh and (run := _repeat_run(raw, at, since_head)):
|
||
chars = sum((_chars(raw[k][1]) for k in run), collections.Counter())
|
||
if budget is None or not (chars - budget): # 통째로 몫에 드는 것부터 뺌
|
||
if budget is not None:
|
||
budget -= chars
|
||
for k in run:
|
||
gone[k] = True
|
||
at = run[-1] + 1
|
||
continue
|
||
later.append(run) # 몫이 모자란 것은 뒤로 미룸
|
||
if _ITEM_HEAD.match(row):
|
||
since_head = ""
|
||
since_head += re.sub(r"\s+", "", row)
|
||
at += 1
|
||
for run in later: # 남은 몫으로 앞 토막만 더 뺌 — 표 머리 뒤에 몸 줄이 딸려 잡힌 자리
|
||
for k in run:
|
||
chars = _chars(raw[k][1])
|
||
if chars - budget:
|
||
break
|
||
budget -= chars
|
||
gone[k] = True
|
||
return [(number, row) for (number, row, _), out in zip(raw, gone) if not out]
|
||
|
||
|
||
def _repeat_run(raw: list[tuple[int, str, bool]], at: int, since_head: str) -> list[int]:
|
||
"""이어진 자리에서 되풀이 머리로 볼 줄 차례 — 이미 나온 글자에 이어 붙는 줄만, 첫 다른 줄 앞까지."""
|
||
run, trial = [], ""
|
||
for k in range(at, len(raw)):
|
||
_, row, fresh = raw[k]
|
||
if (k > at and fresh) or _NOT_REPEAT.match(row):
|
||
break
|
||
grown = trial + re.sub(r"\s+", "", row)
|
||
if not grown or grown not in since_head:
|
||
break
|
||
trial = grown
|
||
run.append(k)
|
||
return run
|
||
|
||
|
||
def _split_off(lines: list[tuple[int, str]]) -> tuple[list[tuple[int, str]], list[tuple[int, str]]]:
|
||
"""(셀 줄, 글자층 밖 줄) — PDF 는 사용자 영역 글리프 · md 는 `글자층 밖` 으로 감싼 글자를 떼어 냄."""
|
||
kept, off, inside = [], [], False
|
||
for at, line in lines:
|
||
keep, gone = [], []
|
||
for ch in line:
|
||
if ch in (_OFF_OPEN, _OFF_CLOSE):
|
||
inside = ch == _OFF_OPEN
|
||
elif inside or _PUA.match(ch):
|
||
# 뗀 자리는 가름 — 분수선 양쪽 수가 붙지 않게(「174」·「2」 ≠ 「1742」)
|
||
keep.append(" ")
|
||
gone.append(ch)
|
||
else:
|
||
keep.append(ch)
|
||
kept.append((at, "".join(keep)))
|
||
if "".join(gone).strip():
|
||
off.append((at, "".join(gone)))
|
||
return kept, off
|
||
|
||
|
||
def _tight(lines: list[tuple[int, str]]) -> tuple[str, list[int]]:
|
||
"""공백 뗀 글자 줄 + 글자마다 어디서 왔나(쪽 또는 md 줄) · 호환 한자는 통합 한자로."""
|
||
text, where = [], []
|
||
for at, line in lines:
|
||
for ch in re.sub(r"\s", "", line):
|
||
text.append(_same_han(ch))
|
||
where.append(at)
|
||
return "".join(text), where
|
||
|
||
|
||
def _numbers(lines: list[tuple[int, str]]) -> tuple[collections.Counter, dict[str, list[int]]]:
|
||
counts, where = collections.Counter(), collections.defaultdict(list)
|
||
for at, line in lines:
|
||
for token in _NUM.findall(line):
|
||
counts[token] += 1
|
||
where[token].append(at)
|
||
return counts, where
|
||
|
||
|
||
def compare(pdf: list[tuple[int, str]], md: list[tuple[int, str]]) -> dict:
|
||
"""① 수(갯수까지) ② 글자(공백 뗌 · 갯수까지) — 양방향. 글자층 밖(사용자 영역 글리프 · 감싼 받아쓰기)은 빼고 따로 셈."""
|
||
pdf, pdf_off = _split_off(pdf)
|
||
md, md_off = _split_off(md)
|
||
pdf_n, pdf_at = _numbers(pdf)
|
||
md_n, md_at = _numbers(md)
|
||
pdf_t, pdf_where = _tight(pdf)
|
||
md_t, md_where = _tight(md)
|
||
missing_c = collections.Counter(pdf_t) - collections.Counter(md_t)
|
||
extra_c = collections.Counter(md_t) - collections.Counter(pdf_t)
|
||
spots = []
|
||
if missing_c or extra_c: # 자리 — 차례대로 맞대 어긋난 토막(갯수 차가 난 글자가 든 것만)
|
||
matcher = difflib.SequenceMatcher(None, pdf_t, md_t, autojunk=False)
|
||
for op, i1, i2, j1, j2 in matcher.get_opcodes():
|
||
gone, added = pdf_t[i1:i2], md_t[j1:j2]
|
||
if op == "equal" or not (set(gone) & set(missing_c) or set(added) & set(extra_c)):
|
||
continue
|
||
spots.append(
|
||
{
|
||
"pdf": pdf_where[min(i1, len(pdf_where) - 1)] if pdf_where else None,
|
||
"md": md_where[min(j1, len(md_where) - 1)] if md_where else None,
|
||
"context": pdf_t[max(0, i1 - 8) : i1],
|
||
"pdf_only": gone,
|
||
"md_only": added,
|
||
}
|
||
)
|
||
return {
|
||
"num_missing": {k: (v, pdf_at[k][:v]) for k, v in (pdf_n - md_n).items()},
|
||
"num_extra": {k: (v, md_at[k][:v]) for k, v in (md_n - pdf_n).items()},
|
||
"char_missing": missing_c,
|
||
"char_extra": extra_c,
|
||
"spots": spots,
|
||
"off_pdf": (
|
||
sum(len(re.sub(r"\s", "", t)) for _, t in pdf_off),
|
||
sorted({a for a, _ in pdf_off}),
|
||
),
|
||
"off_md": sum(len(re.sub(r"\s", "", t)) for _, t in md_off),
|
||
}
|
||
|
||
|
||
#: 자간 벌린 수 — md 주석 `<!-- 자간 벌린 수: 「1 . 3」→「1.3」 · 「3 0 4」→「304」 -->` (규칙 3-13).
|
||
#: 짐작으로 붙이지 않고 **적힌 글자열만** PDF 쪽에서 붙여 셈 · 오른쪽은 왼쪽에서 공백만 뺀 것이어야 함.
|
||
_SPACED = re.compile(r"<!--\s*자간 벌린 수:(.*?)-->", re.S)
|
||
_SPACED_PAIR = re.compile(r"「([^」]+)」\s*→\s*「([^」]+)」")
|
||
#: 글자층 붙은 수 — 자간 벌린 수의 거꾸로. 글자층이 여러 칸 값을 공백 없이 한 덩이로 담은 것
|
||
#: md 주석 `<!-- 글자층 붙은 수: 「0.3350.335」→「0.335 0.335」 -->` · 왼쪽은 오른쪽에서 공백만 뺀 것이어야 함.
|
||
_GLUED = re.compile(r"<!--\s*글자층 붙은 수:(.*?)-->", re.S)
|
||
#: 기계검사 예외 — md 주석 `<!-- 기계검사 예외: 되풀이 머리 -->` (규칙 6장 · 머리 바로 아래 한 줄).
|
||
#: 도구가 스스로 못 가리는 되풀이 머리를 쓴 절 — 사람이 쪽 그림으로 대조하고 손으로 붙임.
|
||
#: 붙이면 ①② 가 어긋나도 「예외(되풀이 머리)」 로 적고 확정 파일이면 지문도 적음.
|
||
_EXEMPT = re.compile(r"<!--\s*기계검사 예외:\s*되풀이 머리\s*-->")
|
||
|
||
|
||
def _rewrite(pdf: list[tuple[int, str]], body: str, mark: re.Pattern, ok):
|
||
"""(PDF 줄, 고친 곳 수, 못 쓴 글자열) — md 주석에 적힌 글자열만 PDF 줄에서 바꿈(PDF 에 있고 ok 일 때만)."""
|
||
done, bad = 0, []
|
||
for block in mark.findall(body):
|
||
for old, new in _SPACED_PAIR.findall(block):
|
||
hits = sum(line.count(old) for _, line in pdf)
|
||
if not hits or not ok(old, new):
|
||
bad.append(old)
|
||
continue
|
||
pdf = [(at, line.replace(old, new)) for at, line in pdf]
|
||
done += hits
|
||
return pdf, done, bad
|
||
|
||
|
||
def join_spaced_numbers(pdf: list[tuple[int, str]], body: str):
|
||
"""(PDF 줄, 붙인 곳 수, 못 쓴 글자열) — md 주석에 적힌 자간 벌린 수만 PDF 줄에서 붙임."""
|
||
return _rewrite(pdf, body, _SPACED, lambda spaced, tight: re.sub(r"\s", "", spaced) == tight)
|
||
|
||
|
||
def split_glued_numbers(pdf: list[tuple[int, str]], body: str):
|
||
"""(PDF 줄, 가른 곳 수, 못 쓴 글자열) — md 주석에 적힌 글자층 붙은 수만 PDF 줄에서 가름."""
|
||
return _rewrite(
|
||
pdf, body, _GLUED, lambda glued, split: glued != split and re.sub(r"\s", "", split) == glued
|
||
)
|
||
|
||
|
||
def verdict(result: dict) -> str:
|
||
counts = (
|
||
sum(v for v, _ in result["num_missing"].values()),
|
||
sum(v for v, _ in result["num_extra"].values()),
|
||
sum(result["char_missing"].values()),
|
||
sum(result["char_extra"].values()),
|
||
)
|
||
joined, bad = result.get("spaced", (0, []))
|
||
split, bad_glued = result.get("glued", (0, []))
|
||
clean = not any(counts) and not bad and not bad_glued
|
||
word = "통과" if clean else ("예외(되풀이 머리)" if result.get("exempt") else "불통")
|
||
line = (
|
||
f"{word} · ①수 결손 {counts[0]} 허구 {counts[1]} · ②글자 빠짐 {counts[2]} 더함 {counts[3]}"
|
||
)
|
||
glyphs, pages = result.get("off_pdf", (0, []))
|
||
if glyphs or result.get("off_md"): # ③ 이 그림으로 그 식을 꼭 보게
|
||
where = f"({', '.join(map(str, pages))}쪽)" if pages else ""
|
||
line += f" · 글자층 밖 글리프 {glyphs}개{where} · 받아씀 {result.get('off_md', 0)}자"
|
||
if joined: # ③ 이 그림으로 그 수를 꼭 보게
|
||
line += f" · 자간 벌린 수 {joined}곳"
|
||
if bad:
|
||
line += " · 자간 벌린 수 못 씀 " + " ".join(f"「{s}」" for s in bad)
|
||
if split: # ③ 이 그림으로 칸마다 값을 꼭 보게
|
||
line += f" · 글자층 붙은 수 {split}곳"
|
||
if bad_glued:
|
||
line += " · 글자층 붙은 수 못 씀 " + " ".join(f"「{s}」" for s in bad_glued)
|
||
return line
|
||
|
||
|
||
def passed(line: str) -> str:
|
||
"""기계검사 한 줄이 받아들일 만한가 — 「통과」 또는 「예외(되풀이 머리)」."""
|
||
return line.startswith(("통과", "예외"))
|
||
|
||
|
||
def fingerprint(text: str) -> str:
|
||
"""확정지문 — 줄끝을 LF 로 고쳐 셈(git 이 CRLF 로 풀어도 같게) · 도구가 적는 두 칸(기계검사·확정지문)은 뺌."""
|
||
lines = text.replace("\r\n", "\n").split("\n")
|
||
kept = [ln for ln in lines if not ln.startswith(("기계검사:", "확정지문:"))]
|
||
return hashlib.sha256("\n".join(kept).encode("utf-8")).hexdigest()
|
||
|
||
|
||
def set_head(text: str, field: str, value: str) -> str:
|
||
head, body = split_head(text)
|
||
head = re.sub(rf"(?m)^{field}:.*$", f"{field}: {value}".rstrip(), head, count=1)
|
||
return head + "\n" + body
|
||
|
||
|
||
def check_file(path: Path) -> tuple[dict, str]:
|
||
"""(대조 결과, 한 줄 판정) — 파일은 안 고침."""
|
||
import pymupdf
|
||
|
||
_, body = split_head(path.read_text(encoding="utf-8").replace("\r\n", "\n"))
|
||
book = book_of(path)
|
||
with pymupdf.open(book["pdf"]) as doc:
|
||
pdf = pdf_lines(doc, read_head(path), book["footer"], body)
|
||
pdf, joined, bad = join_spaced_numbers(pdf, body)
|
||
pdf, split, bad_glued = split_glued_numbers(pdf, body)
|
||
result = compare(pdf, md_lines(body))
|
||
result["spaced"] = (joined, bad)
|
||
result["glued"] = (split, bad_glued)
|
||
result["exempt"] = bool(_EXEMPT.search(body))
|
||
return result, verdict(result)
|
||
|
||
|
||
def check(path: Path) -> int:
|
||
"""검사하고 머리 「기계검사」 칸에 적음 · 상태 `확정` 이고 통과인데 확정지문이 비었으면 지문도 적음.
|
||
|
||
⚠ 있는 확정지문은 안 고침 — 확정 뒤 바뀐 파일을 지문을 다시 적어 덮으면 시험이 못 잡음.
|
||
"""
|
||
result, line = check_file(path)
|
||
print(f"{path.name}: {line}")
|
||
for token, (count, at) in sorted(result["num_missing"].items()):
|
||
print(f" ① 결손 {token} ×{count} (PDF {', '.join(map(str, at))}쪽)")
|
||
for token, (count, at) in sorted(result["num_extra"].items()):
|
||
print(f" ① 허구 {token} ×{count} (md 몸 {', '.join(map(str, at))}줄)")
|
||
for spot in result["spots"][:40]:
|
||
print(
|
||
f" ② PDF {spot['pdf']}쪽 · md 몸 {spot['md']}줄 · …{spot['context']}"
|
||
f" [PDF만 「{spot['pdf_only']}」 · md만 「{spot['md_only']}」]"
|
||
)
|
||
text = set_head(path.read_text(encoding="utf-8").replace("\r\n", "\n"), "기계검사", line)
|
||
head = read_head(path)
|
||
if head.get("상태") == "확정" and passed(line) and not head.get("확정지문"):
|
||
text = set_head(text, "확정지문", fingerprint(text))
|
||
path.write_text(text, encoding="utf-8", newline="\n")
|
||
return 0 if passed(line) else 1
|