From e9c85c41324e151dea3beb384761d56c356c01cc Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 7 Nov 2026 16:30:10 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B5=D1=82=D0=BA=D0=B8=20rutracker:=20?= =?UTF-8?q?=C2=AB=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4:=C2=BB=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=B6=D0=B5=D0=BA=20=D0=BD=D0=B8=D0=B6=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Парсер меток (rt_parse) требовал текст после «Перевод:» на той же строке, поэтому раздачи с пустым «Перевод:» и списком «Профессиональный (…) | Студия» по строке на дорожку (t=6919056, 6927571) не получали меток, релиз не сохранялся, а сегментатор учился на их дорожках как на негативах. Теперь rt_bare_list берёт подряд идущие строки со словом типа (через пустые строки; строки «тип: определение» — глоссарий — не берутся). То же правило в html_extract.extract_segments. На data/raw: +252 раздачи получают метки, в 12 добавляются недостающие дорожки. PARSE_VERSION 3 -> 4 (переразбор при «Переиндексировать data/raw»). Тест rt_parse на 6919056; 276 ok. Co-Authored-By: Claude Sonnet 5.5 --- src/html_extract.py | 2 +- tests/run_tests.py | 9 +++++++ tools/collect.py | 2 +- tools/harvest/harvest_core.py | 48 +++++++++++++++++++++++++++-------- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/html_extract.py b/src/html_extract.py index 1eb588b..257da2a 100644 --- a/src/html_extract.py +++ b/src/html_extract.py @@ -133,7 +133,7 @@ def extract_segments(text: str) -> list[str]: k = i while k < len(lines) and not lines[k].strip(): k += 1 - if k >= len(lines) or not RE_TYPE_START.match(lines[k]): + if k >= len(lines) or not RE_TYPE_START.match(lines[k]) or ":" in lines[k]: # «тип: определение» — глоссарий break body += " ; " + lines[k].strip() i = k + 1 diff --git a/tests/run_tests.py b/tests/run_tests.py index fa32e41..19fff3b 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -823,6 +823,15 @@ check("метки rt_parse: кодек и битрейт из строки «А all(r["codec"] == "AC3" and r["kbps"] == "192" for r in _hr), str(_hr)) check("метки rt_parse: 5 студий + 1 доп. 18+", len(_rows) == 6, str(len(_rows))) +_bare = ("Перевод:\nПрофессиональный (дублированный) | Cinema Tone Production\n\nПрофессиональный (псевдодублированный) | HDRezka Studio\n" + "Профессиональный (многоголосый закадровый) | HDRezka Studio\nПрофессиональный (многоголосый закадровый) | TVShows\n" + "\nОригинальная аудиодорожка: Английская\nСубтитры: Русские, английские\n") +_br, _ = _hc.rt_parse({"id": "6919056", "title": "Астрал 6 [2026, WEB-DL 2160p]", "post": _bare}) +check("метки rt_parse: «Перевод:» без значения + список строк (6919056)", + [(r["translation_type"], r["studio"]) for r in _br] == [ + ("проф. дубляж", "Cinema Tone Production"), ("люб. дубляж", "HDrezka"), # псевдодубляж = «люб. дубляж» + ("проф. многоголосый", "HDrezka"), ("проф. многоголосый", "TVShows")], str([(r["translation_type"], r["studio"]) for r in _br])) + _sf = __import__("src.release", fromlist=["x"]).series_info("Сериал / Сезон 1 / Серии 1-8 из 8 [2026]", [ {"studio": "LostFilm", "episodes": "1-4", "episode_ranges": [[1, 4]]}, {"studio": "HDrezka", "episodes": None, "episode_ranges": []}]) diff --git a/tools/collect.py b/tools/collect.py index 04a96e2..e0b196f 100644 --- a/tools/collect.py +++ b/tools/collect.py @@ -1028,7 +1028,7 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No # Версия разбора: поднимите при правке парсеров (harvest_core, html_to_text) — тогда # кэш «уже разобрано» сбросится сам и данные пересоберутся один раз. -PARSE_VERSION = 3 +PARSE_VERSION = 4 def load_json(path: Path) -> dict: diff --git a/tools/harvest/harvest_core.py b/tools/harvest/harvest_core.py index 55d8900..6164649 100644 --- a/tools/harvest/harvest_core.py +++ b/tools/harvest/harvest_core.py @@ -1288,6 +1288,10 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]: # ---- rutracker ----------------------------------------------------- RT_RE_FIELD = re.compile( r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*(?P.+)$", re.I) +RT_RE_BARE = re.compile( + r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*$", re.I) +RT_RE_TYPE_START = re.compile( + r"^\s*(?:Профессиональн|Любительск|Авторск|Дублирован|Многоголос|Двухголос|Одноголос)", re.I) RT_RE_SUBS = re.compile(r"^\s*Субтитры\s*:\s*(?P.+)$", re.I) RT_RE_BRACKET = re.compile(r"\[(?P[^\]]*)\]") RT_RE_SEP = re.compile(r"\s*(?:\+|;|/)\s*(?=Проф|Люб|Авт|Дубл|Многоголос|Двухголос|Одноголос)", re.I) @@ -1366,20 +1370,44 @@ def audio18_rows(rows: list[dict], post: str) -> list[dict]: return rows + extra +def rt_bare_list(lines: list[str], i: int, taken: set[int]) -> list[str]: + """«Перевод:» без значения: дорожки списком ниже, по строке на дорожку. + + Берутся подряд идущие строки со словом типа озвучки; между ними могут стоять + пустые (в HTML после
идёт ещё и перевод строки). Номера взятых строк + кладутся в taken, чтобы их не разбирали второй раз. + """ + out, j = [], i + 1 + while True: + k = j + while k < len(lines) and not lines[k].strip(): + k += 1 + if k >= len(lines) or not RT_RE_TYPE_START.match(lines[k]) or ":" in lines[k]: # «тип: определение» — глоссарий + return out + out.append(lines[k]) + taken.add(k) + j = k + 1 + + def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]: post = rec.get("post") or "" info = rt_parse_title(rec.get("page_title") or rec.get("title") or "") - entries, subs = [], "" - for line in post.splitlines(): + entries, subs, taken = [], "", set() + lines = post.splitlines() + for i, line in enumerate(lines): + if i in taken: + continue m = RT_RE_FIELD.match(line) - if m: - for part in rt_split_entries(m.group("body")): - e = rt_parse_entry(part) - if e["type"] in ("оригинал", "субтитры"): - continue - if not e["studio"] and e["type"] == "неизвестно": - continue - entries.append(e) + bodies = [m.group("body")] if m else (rt_bare_list(lines, i, taken) if RT_RE_BARE.match(line) else []) + if bodies: + for body in bodies: + for part in rt_split_entries(body): + e = rt_parse_entry(part) + if e["type"] in ("оригинал", "субтитры"): + continue + if not e["studio"] and e["type"] == "неизвестно": + continue + entries.append(e) continue m = RT_RE_SUBS.match(line) if m and not subs: