#!/usr/bin/env python3 """Полигон, пакет-7: KWIC-контексты вхождений термина по субстрату чанков. Нужен дважды. (1) Разметка кандидатов не по памяти, а по тексту: любое утверждение «это имя персонажа / это жанровый термин» обязано опираться на вхождения, иначе это угадывание. (2) Прототип того, что роль ТЕРМИНОЛОГ получает на вход по D39.42 п.1 («KWIC-контексты вхождений из исходника»), — здесь видно, сколько это стоит по символам, и это цифра для сметы фазы B. $0, stdlib. Детерминирован: вхождения идут в порядке (chapter, chunk_idx, offset). """ from __future__ import annotations import argparse import json import sys from pathlib import Path def load(path: Path) -> list[dict]: return [json.loads(l) for l in path.open(encoding="utf-8") if l.strip()] def kwic(rows: list[dict], needle: str, width: int, limit: int) -> list[tuple[int, int, str]]: out = [] for r in rows: src, pos = r["source"], 0 while True: i = src.find(needle, pos) if i < 0: break a, b = max(0, i - width), min(len(src), i + len(needle) + width) frag = src[a:b].replace("\n", " ⏎ ") out.append((r["chapter"], r["chunk_idx"], frag)) pos = i + len(needle) if limit and len(out) >= limit: return out return out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--chunks", required=True, type=Path) ap.add_argument("--term", action="append", required=True) ap.add_argument("--width", type=int, default=40) ap.add_argument("--limit", type=int, default=6) ap.add_argument("--stats", action="store_true", help="только объём контекстов (для сметы)") a = ap.parse_args() rows = load(a.chunks) for t in a.term: hits = kwic(rows, t, a.width, 0 if a.stats else a.limit) if a.stats: chars = sum(len(f) for _, _, f in hits) print(f"{t}\tвхождений={len(hits)}\tсимволов_контекста={chars}") continue print(f"=== {t} ({len(hits)} показано, лимит {a.limit}) ===") for ch, ci, frag in hits: print(f" [гл.{ch}/{ci}] …{frag}…") return 0 if __name__ == "__main__": raise SystemExit(main())