From 71f27fee7d80666915d3bd3524ad4acfb3dca62a Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 20 Aug 2026 17:49:03 +0500 Subject: [PATCH] =?UTF-8?q?fix(ptica):=20=D1=82=D0=B8=D0=BF=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=E2=80=94=20=D0=BF=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=BC=D1=83=20=D1=83=D0=BF=D0=BE=D0=BC=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E,=20=D0=B0=20=D0=BD=D0=B5=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BA=D1=83=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BE=D0=BA=20(#2464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_detect_kind` проверял «резервир» первым и возвращал «резервирование» безусловно. Постановление об изъятии, где резервирование упомянуто вскользь — типовая формулировка «ранее зарезервированных земель», ссылка на утративший силу акт, — классифицировалось как резервирование. Ошибка не единичная: `kind` в `extract_reservations` один на весь документ, поэтому неверный тип уходит в КАЖДУЮ строку land_reservation по этому акту. Побеждает то слово, что встретилось раньше. Тема документа стоит в заголовке, поэтому позиция — сигнал сильнее порядка проверок, и он симметричен: заголовок «О резервировании» так же выигрывает у «изъятия» в теле. Простая смена порядка проверок этой симметрии не даёт — на неё поставлен отдельный контроль. Текущих ошибок на проде нет, и это измерено: в land_reservation 27 строк, все из источника izyatie_ekb_ocr, все «изъятие», ни в одной выдержке слова «резервир» не встречается. Правка закрывает возможность, а не чинит существующую порчу. Двусторонне: против origin/main два теста красные с конкретным неверным значением (`assert 'резервирование' == 'изъятие'`). Контроли — симметрия заголовка, одиночные маркеры, откат к default_kind — зелёные с обеих сторон. Формулировки в тестах взяты с прода дословно (basis_act). Co-Authored-By: Claude Opus 5 --- .../scrapers/page_reservation_parser.py | 20 ++++- .../test_2464_detect_kind_position.py | 83 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 backend/tests/services/scrapers/test_2464_detect_kind_position.py diff --git a/backend/app/services/scrapers/page_reservation_parser.py b/backend/app/services/scrapers/page_reservation_parser.py index 9797acc5..96ab63e7 100644 --- a/backend/app/services/scrapers/page_reservation_parser.py +++ b/backend/app/services/scrapers/page_reservation_parser.py @@ -127,10 +127,24 @@ def _parse_act_date(text: str) -> date | None: def _detect_kind(text: str, default_kind: str) -> str: - """Определяет тип операции: 'резервирование' | 'изъятие' | default_kind.""" - if _RE_REZERV.search(text): + """Определяет тип операции: 'резервирование' | 'изъятие' | default_kind. + + Побеждает то слово, что встретилось РАНЬШЕ, а не то, что стоит выше в + коде (#2464). Прежний безусловный приоритет «резервир» переклассифицировал + ВЕСЬ документ — все участки разом, — если постановление об изъятии хоть + раз ссылалось на резервирование (типовая формулировка «ранее + зарезервированных земель», ссылка на утративший силу акт). Тема документа + стоит в заголовке, поэтому позиция первого упоминания — сигнал сильнее + порядка проверок, и он симметричен: заголовок «О резервировании» так же + выигрывает у «изъятия» в теле. + """ + m_rez = _RE_REZERV.search(text) + m_izy = _RE_IZYAT.search(text) + if m_rez and m_izy: + return "резервирование" if m_rez.start() < m_izy.start() else "изъятие" + if m_rez: return "резервирование" - if _RE_IZYAT.search(text): + if m_izy: return "изъятие" return default_kind diff --git a/backend/tests/services/scrapers/test_2464_detect_kind_position.py b/backend/tests/services/scrapers/test_2464_detect_kind_position.py new file mode 100644 index 00000000..647ea28f --- /dev/null +++ b/backend/tests/services/scrapers/test_2464_detect_kind_position.py @@ -0,0 +1,83 @@ +"""Тип постановления определяется по первому упоминанию, а не по порядку проверок (#2464). + +`_detect_kind` проверял «резервир» ПЕРВЫМ и возвращал «резервирование» безусловно. +Постановление об изъятии, где резервирование упомянуто вскользь — типовая +формулировка «ранее зарезервированных земель», ссылка на утративший силу акт, — +переклассифицировало **весь документ**: `kind` в `extract_reservations` один на +все участки, поэтому ошибка размножается на каждую строку `land_reservation`. + +Побеждает то слово, что встретилось раньше. Тема документа стоит в заголовке, +поэтому позиция — сигнал сильнее порядка проверок, и он симметричен: заголовок +«О резервировании» так же выигрывает у «изъятия» в теле. + +Формулировки взяты с прода: `basis_act` в `land_reservation` (источник +`izyatie_ekb_ocr`, 27 строк, все «изъятие») начинается со слов «Сообщение о +планируемом изъятии земельных участков…». +""" + +from __future__ import annotations + +from app.services.scrapers.page_reservation_parser import ( + _detect_kind, + extract_reservations, +) + +# Заголовок — с прода, дословно. +_ЗАГОЛОВОК_ИЗЪЯТИЕ = ( + "Сообщение о планируемом изъятии земельных участков и объектов недвижимого " + "имущества для муниципальных нужд в целях строительства улицы Энергостроителей" +) + + +def test_izyatie_in_title_wins_over_later_rezervirovanie() -> None: + """Головной: упоминание резервирования в теле не переписывает тему заголовка. + + На origin/main возвращается «резервирование» — неверное значение, а не отказ. + """ + text = ( + f"{_ЗАГОЛОВОК_ИЗЪЯТИЕ}. Изъятию подлежат участки, ранее зарезервированные " + "постановлением о резервировании земель от 12.03.2019." + ) + assert ( + _detect_kind(text, "изъятие") == "изъятие" + ), "документ об изъятии классифицирован по случайному упоминанию в теле" + + +def test_all_parcels_of_the_document_are_affected() -> None: + """Ошибка типа размножается на КАЖДЫЙ участок — kind в документе один на всех.""" + text = ( + f"{_ЗАГОЛОВОК_ИЗЪЯТИЕ}. Изъять земельные участки с кадастровыми номерами " + "66:41:0303004:22, 66:41:0303004:23, 66:41:0206032:8499, ранее " + "зарезервированные постановлением о резервировании земель." + ) + records = extract_reservations(text, default_kind="изъятие") + assert len(records) == 3, f"разобрано участков: {len(records)}" + kinds = {r.reservation_kind for r in records} + assert kinds == {"изъятие"}, f"типы участков: {kinds} — ошибка ушла бы во все строки" + + +def test_rezervirovanie_in_title_still_wins() -> None: + """Контроль симметрии: заголовок о резервировании выигрывает у изъятия в теле. + + Ловит «починку» через простую смену порядка проверок — она сломала бы этот случай. + """ + text = ( + "Постановление о резервировании земель для муниципальных нужд. " + "Зарезервированные участки не подлежат изъятию до истечения срока." + ) + assert _detect_kind(text, "изъятие") == "резервирование" + + +def test_single_keyword_documents_unchanged() -> None: + """Контроль: документы с одним словом-маркером распознаются как раньше.""" + assert _detect_kind("Об изъятии земельного участка", "резервирование") == "изъятие" + assert _detect_kind("О резервировании земель", "изъятие") == "резервирование" + + +def test_no_keyword_falls_back_to_default() -> None: + """Контроль: без маркеров остаётся default_kind, а не выдуманный тип.""" + assert _detect_kind("Об утверждении схемы расположения участка", "изъятие") == "изъятие" + assert ( + _detect_kind("Об утверждении схемы расположения участка", "резервирование") + == "резервирование" + ) -- 2.45.3