Доразметка студий: nnm «Перевод: … (Студия)», kinozal из заголовка раздачи
* 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
f91f6cb48e
commit
83bab13893
6 files changed
+7488
-3056
No files matched your search
@@ -0,0 +1,103 @@
|
||||
"""Доразметка студий в 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()
|
||||
Reference in new issue
Block a user