#!/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**, первая строка файла — ОБЯЗАТЕЛЬНО заголовок главы. Нарушение любого из трёх молча сдвигает нумерацию глав у движка. ⚠ **МИНА НУМЕРАЦИИ, названная до запуска.** Движок не умеет смещение глав: срез всегда нумеруется с единицы. Вырезав главы 26–42, в базе и отчётах увидишь «главы 1–17». Соответствие держится ТОЛЬКО этим файлом и строкой в пре-реге. Поэтому скрипт печатает обе нумерации и пишет их в манифест рядом с 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("первая строка среза 26–42 — заголовок", 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())