textmachine/eval/dovodka/narezka.py

133 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""narezka.py — вырезать диапазон глав книги в формат, который принимает движок.
Заведён 02.09 по находке проверки исполнимости: пре-рег требовал «срез исходника (диапазон глав,
файл, его sha256)», а СПОСОБА получить этот файл в проекте не было ни одного. Резалки не
существовало, и свежая сессия обязана была бы её изобрести — вместе с форматом.
ФОРМАТ, СНЯТЫЙ С ЭТАЛОНА (`~/books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt`,
проверено `file` и `od`): кодировка **GB18030**, переводы строки **CRLF**, первая строка файла —
ОБЯЗАТЕЛЬНО заголовок главы. Нарушение любого из трёх молча сдвигает нумерацию глав у движка.
⚠ **МИНА НУМЕРАЦИИ, названная до запуска.** Движок не умеет смещение глав: срез всегда нумеруется
с единицы. Вырезав главы 2642, в базе и отчётах увидишь «главы 117». Соответствие держится
ТОЛЬКО этим файлом и строкой в пре-реге. Поэтому скрипт печатает обе нумерации и пишет их в
манифест рядом с sha256.
⚠ **ТОМА.** В книге нумерация глав СБРАСЫВАЕТСЯ по томам — `第二十六节` встречается несколько раз.
Поэтому режем по ПОРЯДКОВОМУ НОМЕРУ вхождения заголовка от начала файла, а не по тексту заголовка.
Использование:
python3 eval/dovodka/narezka.py --from 26 --to 42 --out <путь>.gb18030.txt
python3 eval/dovodka/narezka.py --selftest
"""
import argparse
import hashlib
import re
import sys
from pathlib import Path
SRC = Path.home() / "books/gu-zhenren/guzhenren-utf8.txt"
HEAD_RE = re.compile(r"^第[^\n]{1,20}节")
def split_chapters(text):
"""[(порядковый номер от 1, заголовок, тело с заголовком)] — по порядку вхождения, не по тексту."""
lines = text.split("\n")
starts = [i for i, ln in enumerate(lines) if HEAD_RE.match(ln)]
out = []
for k, i in enumerate(starts):
j = starts[k + 1] if k + 1 < len(starts) else len(lines)
out.append((k + 1, lines[i], "\n".join(lines[i:j])))
return out
def cut(src_path, first, last, out_path):
text = src_path.read_text(encoding="utf-8")
chapters = split_chapters(text)
if not chapters:
print("ОТКАЗ: в исходнике не найдено ни одного заголовка главы — проверь путь и кодировку")
return 2
if last > len(chapters):
print(f"ОТКАЗ: запрошена глава {last}, а в исходнике их {len(chapters)}")
return 2
picked = chapters[first - 1 : last]
body = "\n".join(ch[2] for ch in picked)
if not HEAD_RE.match(body.split("\n")[0]):
print("ОТКАЗ: первая строка среза не заголовок — движок сдвинет нумерацию")
return 2
data = body.replace("\r\n", "\n").replace("\n", "\r\n").encode("gb18030", errors="strict")
out_path.write_bytes(data)
sha = hashlib.sha256(data).hexdigest()
print(f"срез: главы книги {first}{last} → в файле пронумеруются 1{len(picked)}")
print(f" файл : {out_path}")
print(f" байт : {len(data)}")
print(f" sha256 : {sha}")
print(f" заголовков в срезе: {sum(1 for ch in picked if HEAD_RE.match(ch[1]))}")
print(f" первая строка: {picked[0][1][:60]}")
print(f" последняя глава: {picked[-1][1][:60]}")
print()
print("В пре-рег занести ИМЕННО ЭТУ пару: диапазон книги и sha256 файла.")
print(f" главы книги {first}{last} ≡ главы среза 1{len(picked)}, sha256 {sha}")
return 0
def selftest():
"""Гейты на самом скрипте: без них резалка может молча отдать не тот формат."""
ok = True
def check(name, cond):
nonlocal ok
print(("" if cond else "") + name)
ok = ok and cond
print("СЕЛФТЕСТ narezka.py")
check("исходник на месте", SRC.exists())
if SRC.exists():
chapters = split_chapters(SRC.read_text(encoding="utf-8"))
check(f"глав в исходнике > 2000 (найдено {len(chapters)})", len(chapters) > 2000)
# ⚠ Тест первой редакции проверял повтор ЦЕЛОЙ строки заголовка и был красным: строки
# уникальны, потому что несут НАЗВАНИЕ главы («第一节:纵身亡魔心仍不悔»). Повторяется
# НОМЕР — вот его и проверяем, ради этого и режем по порядку вхождения.
nums = [re.match(r"^第[^:]{1,12}节", c[1]).group(0) for c in chapters
if re.match(r"^第[^:]{1,12}节", c[1])]
check(f"НОМЕР главы в исходнике повторяется (тома): {len(nums)} заголовков, "
f"{len(set(nums))} различных номеров", len(set(nums)) < len(nums))
body = "\n".join(c[2] for c in chapters[25:42])
check("первая строка среза 2642 — заголовок", bool(HEAD_RE.match(body.split("\n")[0])))
try:
enc = body.replace("\n", "\r\n").encode("gb18030")
check("срез кодируется в GB18030 без потерь", True)
check("CRLF на месте", b"\r\n" in enc)
except UnicodeEncodeError as e:
check(f"срез кодируется в GB18030 без потерь — ОШИБКА {e}", False)
ref = Path.home() / "books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt"
if ref.exists():
raw = ref.read_bytes()
check("эталон холодного прогона тоже CRLF", b"\r\n" in raw)
head = raw[:60].decode("gb18030", errors="replace")
check("эталон начинается с заголовка", bool(HEAD_RE.match(head)))
print("\n" + ("РЕЗАЛКА ГОДНА" if ok else "⛔ РЕЗАЛКА НЕ ГОДНА"))
return 0 if ok else 1
def main():
ap = argparse.ArgumentParser(description="вырезать диапазон глав в формат движка")
ap.add_argument("--from", dest="first", type=int, help="первая глава книги (сквозной номер)")
ap.add_argument("--to", dest="last", type=int, help="последняя глава книги включительно")
ap.add_argument("--out", type=Path, help="кудаписать срез")
ap.add_argument("--src", type=Path, default=SRC)
ap.add_argument("--selftest", action="store_true")
a = ap.parse_args()
if a.selftest:
return selftest()
if not (a.first and a.last and a.out):
ap.error("нужны --from, --to и --out (или --selftest)")
return cut(a.src, a.first, a.last, a.out)
if __name__ == "__main__":
sys.exit(main())