* harvest_core.nnm_part_studio: студия из части строки «Перевод:» nnm («Дублированный (Невафильм)», «… - Jaskier»); раньше такие строки шли с «(не указана)», и модель училась не извлекать студию даже из «Дублированный (Пифагор)». tech-строки со студиями больше не выбрасываются, если на странице нет строк «Аудио N» с именами. * harvest_core.kz_title_rows: kinozal, студии из заголовка «ПМ (LostFilm, HDrezka Studio)», которых нет на странице; вход — сегмент как у сервиса, строка «Аудио Rus: … | Студия» — в audio_raw. * tools/relabel_studios.py — то же по готовому labels.csv без пересборки страниц; применено к data/labels.csv: со студией 22 061 -> 29 331 строк (nnm +3 051, kinozal +4 219 строк в 3 007 релизах); prepare_data: 39 418 строк, спан студии 24 004 -> 31 214. * e2e: эталон как aggregate — «тип без студии» убирается, если у типа есть студия. Co-Authored-By: Claude Opus 5.5 <[email protected]>
104 lines
4.6 KiB
Python
104 lines
4.6 KiB
Python
"""Доразметка студий в data/labels.csv без пересборки страниц.
|
||
|
||
То же, что теперь делает сборщик (tools/harvest/harvest_core.py) при переиндексации,
|
||
но по готовому labels.csv и data/raw — минуты вместо полного разбора HTML:
|
||
|
||
* nnm, строки «Перевод:» (studio_source=tech) с «(не указана)»: студия, названная
|
||
в самой части строки («Дублированный (Невафильм)», «… - Jaskier») — nnm_part_studio;
|
||
* kinozal: студии из заголовка раздачи «ПМ (LostFilm, HDrezka Studio)», которых нет
|
||
среди строк релиза (studio_source=title) — kz_title_rows по data/raw/kinozal/<id>.json.
|
||
|
||
Ручные релизы (data/manual_ids.txt) не трогаются. Перед записью — копия
|
||
data/labels.bak_relabel_<время>.csv. Повторный запуск ничего не добавляет.
|
||
|
||
.venv\\Scripts\\python tools\\relabel_studios.py # применить
|
||
.venv\\Scripts\\python tools\\relabel_studios.py --dry-run # только статистика
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import json
|
||
import shutil
|
||
import sys
|
||
import time
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(ROOT / "tools" / "harvest"))
|
||
|
||
import harvest_core as H # noqa: E402
|
||
|
||
LABELS = ROOT / "data" / "labels.csv"
|
||
RAW_KZ = ROOT / "data" / "raw" / "kinozal"
|
||
MANUAL = ROOT / "data" / "manual_ids.txt"
|
||
NO_STUDIO = "(не указана)"
|
||
|
||
|
||
def relabel(rows: list[dict], raw_kz: Path = RAW_KZ, manual: set[str] | None = None) -> tuple[list[dict], Counter]:
|
||
"""rows -> (новые rows, статистика). Исходные dict меняются на месте."""
|
||
manual = manual or set()
|
||
stats: Counter = Counter()
|
||
for r in rows:
|
||
if r["source"] == "nnm" and r.get("studio_source") == "tech" and r["studio"] == NO_STUDIO \
|
||
and r["release_id"] not in manual:
|
||
studio = H.nnm_part_studio(r.get("role_raw") or "")
|
||
if studio:
|
||
r["studio"] = studio
|
||
stats["nnm: студия из «Перевод:»"] += 1
|
||
by_rel: dict[str, list[dict]] = defaultdict(list)
|
||
for r in rows:
|
||
if r["source"] == "kinozal":
|
||
by_rel[r["release_id"]].append(r)
|
||
added: list[dict] = []
|
||
for rid, rel_rows in by_rel.items():
|
||
if rid in manual:
|
||
continue
|
||
path = raw_kz / f"{rid.removeprefix('kz-')}.json"
|
||
if not path.exists():
|
||
stats["kinozal: нет raw JSON"] += 1
|
||
continue
|
||
rec = json.loads(path.read_text(encoding="utf-8"))
|
||
rec["id"] = rid
|
||
text = "\n".join(x for x in (rec.get("release_tab") or "", rec.get("tech_tab") or "") if x)
|
||
new = H.kz_title_rows(rec, text, rel_rows)
|
||
if new:
|
||
stats["kinozal: релизов со студиями из заголовка"] += 1
|
||
stats["kinozal: студий из заголовка"] += len(new)
|
||
stats["kinozal: из них со строкой «Аудио»"] += sum(bool(x["audio_raw"]) for x in new)
|
||
added += new
|
||
return rows + added, stats
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--dry-run", action="store_true", help="только посчитать, файл не менять")
|
||
args = ap.parse_args()
|
||
with LABELS.open(encoding="utf-8-sig", newline="") as f:
|
||
reader = csv.DictReader(f)
|
||
fields = reader.fieldnames
|
||
rows = list(reader)
|
||
manual = set(MANUAL.read_text(encoding="utf-8").split()) if MANUAL.exists() else set()
|
||
before = sum(r["studio"] != NO_STUDIO for r in rows)
|
||
rows, stats = relabel(rows, manual=manual)
|
||
after = sum(r["studio"] != NO_STUDIO for r in rows)
|
||
for k, v in stats.items():
|
||
print(f"{k}: {v}")
|
||
print(f"строк: {len(rows)}; со студией: {before} -> {after}")
|
||
if args.dry_run:
|
||
return
|
||
backup = LABELS.with_name(f"labels.bak_relabel_{time.strftime('%Y%m%d_%H%M%S')}.csv")
|
||
shutil.copy(LABELS, backup)
|
||
with LABELS.open("w", encoding="utf-8-sig", newline="") as f:
|
||
w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
|
||
w.writeheader()
|
||
w.writerows(rows)
|
||
print(f"записано: {LABELS.relative_to(ROOT)}; копия: {backup.name}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|