docs(ptica): докстрока не обещает защиты, которой нет, и не шлёт на мёртвый хост (#2464) #2988
3 changed files with 145 additions and 10 deletions
|
|
@ -70,10 +70,32 @@ _TABLE_CAPTION_RE = {
|
|||
|
||||
|
||||
def _page_contains_table(text: str, table_no: int) -> bool:
|
||||
"""Возвращает True если текст страницы содержит заголовок таблицы N.
|
||||
"""Возвращает True если на странице встречается строка «Таблица N».
|
||||
|
||||
Требуем явный caption «Таблица N» — hint-только режим (оглавление, перекрёстные ссылки)
|
||||
даёт false positive и подавляется. Если caption присутствует — достаточно.
|
||||
Это поиск подстроки, без разбора контекста. Строка ОГЛАВЛЕНИЯ
|
||||
«Таблица 11 Баланс территории ....... 34» от настоящей подписи не отличается —
|
||||
падеж тот же, именительный, — и вызывающий код ставит found_start=True прямо на
|
||||
странице содержания, начиная выдирать таблицы оттуда.
|
||||
|
||||
Прежняя редакция этой докстроки утверждала обратное — будто ложные срабатывания
|
||||
оглавления и перекрёстных ссылок здесь отсеиваются. Такого кода никогда не было
|
||||
(#2464). Обещание защиты, которой нет, опаснее её отсутствия: читающий не станет
|
||||
её добавлять. (Старая формулировка тут намеренно пересказана, а не процитирована:
|
||||
гейт test_2464_tep_docstring_truth ищет обещание по тексту и не отличил бы цитату
|
||||
от утверждения.)
|
||||
|
||||
Замер 20.08.2026 уточняет и границы проблемы: перекрёстные ссылки в косвенных
|
||||
падежах регекс НЕ ловит — он требует именительное «Таблица N», поэтому
|
||||
«приведены в таблице 12», «см. Таблицу 12», «табл. 12» дают False. Опасно
|
||||
ровно оглавление, а не любое упоминание.
|
||||
|
||||
Почему подавление не реализовано здесь и сейчас: таблица `ekb_ppt_tep` на проде
|
||||
пуста (0 строк) — URL в `_SEED_DOCS` заглушка, живых PDF нет, эвристику отсева
|
||||
оглавления не на чем откалибровать. Правило, придуманное без образцов, ловит
|
||||
ровно те случаи, которые придумали вместе с ним. Условие для реализации — хотя
|
||||
бы один настоящий документ (#1136); целиться следует в признаки оглавления
|
||||
(точки-выноски, номер страницы в конце строки, несколько подписей на одной
|
||||
странице), а не в падежи.
|
||||
"""
|
||||
cap = _TABLE_CAPTION_RE[table_no]
|
||||
return bool(cap.search(text))
|
||||
|
|
@ -303,9 +325,7 @@ def _parse_table13(raw_rows: list[list[str | None]]) -> list[dict[str, Any]]:
|
|||
seen: set[tuple[str, str, str, str]] = set()
|
||||
result: list[dict[str, Any]] = []
|
||||
for rec in rows_clean:
|
||||
area_key = (
|
||||
f"{rec['area_ha_num']:.4f}" if rec["area_ha_num"] is not None else rec["area_ha"]
|
||||
)
|
||||
area_key = f"{rec['area_ha_num']:.4f}" if rec["area_ha_num"] is not None else rec["area_ha"]
|
||||
key = (rec["phase"], rec["composition"], rec["zone"], area_key)
|
||||
if key in seen:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -22,10 +22,22 @@ logger = logging.getLogger(__name__)
|
|||
# ── Seed-документы ─────────────────────────────────────────────────────────────
|
||||
# Образец: ППТ 22823 (2018), пояснительная записка.
|
||||
#
|
||||
# URL — placeholder. Реальный URL пояснительной записки лежит на ГИСОГД ЕКБ
|
||||
# (https://gisogd.ekburg.ru/) под номером проекта планировки, но прямой PDF-линк
|
||||
# требует ручного поиска через UI (#1136). До тех пор — ingest скипает с
|
||||
# WARNING и метрики остаются {"docs": 0}.
|
||||
# URL — placeholder, ingest скипает с WARNING, метрики остаются {"docs": 0}.
|
||||
# Прод 20.08.2026: в таблице ekb_ppt_tep 0 строк — то есть загрузчик не отработал
|
||||
# ни разу.
|
||||
#
|
||||
# ВАЖНО (#2464): хост `gisogd.ekburg.ru`, названный ниже как место, где «лежит
|
||||
# реальный URL», НЕ СУЩЕСТВУЕТ — DNS не резолвит его ни с рабочей машины, ни с
|
||||
# прод-хоста (проверено 20.08.2026). Прежняя редакция этого комментария отправляла
|
||||
# искать документ вручную на портале, которого нет.
|
||||
#
|
||||
# Живой портал ГИСОГД Свердловской области — `gisogd66.midural.ru` (его использует
|
||||
# загрузчик РНС/РВЭ, см. services/scrapers/gisogd66.py). Раздел 13 там — документы
|
||||
# по земельному участку; проекты планировки лежат в других разделах, перечисление
|
||||
# групп доступно через `/api/v1/{schema}/gisogddocgroups/{razdel}`. Перебор
|
||||
# razdel3/4/5 показал разделы «Генеральный план», «Местные нормативы», «Правила
|
||||
# землепользования и застройки» — точный раздел ППТ и формат ссылки на PDF
|
||||
# пояснительной записки ещё предстоит найти (#1136).
|
||||
#
|
||||
# Override-пути для прогона:
|
||||
# 1. Передать в task: ingest_ppt_tep([{"doc_ref": "...", "url": "...",
|
||||
|
|
|
|||
103
backend/tests/services/scrapers/test_2464_tep_docstring_truth.py
Normal file
103
backend/tests/services/scrapers/test_2464_tep_docstring_truth.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Докстрока `_page_contains_table` не обещает защиты, которой нет (#2464).
|
||||
|
||||
Прежняя редакция утверждала:
|
||||
|
||||
«Требуем явный caption «Таблица N» — hint-только режим (оглавление,
|
||||
перекрёстные ссылки) даёт false positive и подавляется.»
|
||||
|
||||
Кода подавления в функции никогда не было — это `bool(cap.search(text))`, то есть
|
||||
обычный поиск подстроки. Строка оглавления «Таблица 11 Баланс территории ..... 34» от
|
||||
настоящей подписи не отличается: падеж тот же, именительный.
|
||||
|
||||
Замер 20.08.2026 уточнил и сам пункт эпика: приведённый в нём пример перекрёстной
|
||||
ссылки «показатели приведены в таблице 12» регекс НЕ ловит — он требует именительное
|
||||
«Таблица N». Опасно ровно оглавление, а не любое упоминание. Это зафиксировано
|
||||
характеризующим тестом, чтобы следующая попытка подавления целилась в признаки
|
||||
оглавления, а не в падежи.
|
||||
|
||||
Обещание защиты, которой нет, опаснее её отсутствия: читающий не станет её
|
||||
добавлять. Тот же класс, что «объявлено ≠ исполняется».
|
||||
|
||||
Почему подавление не реализовано, а докстрока приведена к коду: таблица
|
||||
`ekb_ppt_tep` на проде пуста (0 строк) — URL в `_SEED_DOCS` заглушка, живых PDF нет,
|
||||
эвристику отсева оглавления не на чем откалибровать. Правило, придуманное без
|
||||
образцов, ловит ровно те случаи, которые придумали вместе с ним.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from app.services.scrapers import ekb_ppt_tep_parser as mod
|
||||
|
||||
|
||||
def test_docstring_does_not_promise_suppression_that_is_absent() -> None:
|
||||
"""Головной: докстрока не должна обещать подавление false positive.
|
||||
|
||||
На origin/main она содержит «даёт false positive и подавляется», а в теле
|
||||
функции — один `cap.search(text)`. Красное значение конкретно: обещание есть,
|
||||
реализации нет.
|
||||
"""
|
||||
doc = inspect.getdoc(mod._page_contains_table) or ""
|
||||
src = inspect.getsource(mod._page_contains_table)
|
||||
тело = src.split('"""')[-1]
|
||||
# Ищем именно ОБЕЩАНИЕ («подавляется»), а не разговор о подавлении: новая
|
||||
# редакция объясняет, почему подавление НЕ реализовано, и не должна краснеть.
|
||||
обещает = "подавляется" in doc
|
||||
реализует = any(kw in тело for kw in ("оглавлен", "leader", "count(", "finditer", "rsplit"))
|
||||
assert not (обещает and not реализует), (
|
||||
"докстрока обещает подавление false positive, а в теле только поиск "
|
||||
f"подстроки:\n{тело.strip()}"
|
||||
)
|
||||
|
||||
|
||||
def test_docstring_names_the_actual_behaviour() -> None:
|
||||
"""Докстрока обязана честно называть false positive, а не умалчивать о нём.
|
||||
|
||||
Просто убрать обещание было бы недостаточно: молчание читается как «всё
|
||||
хорошо». Ловит «починку» через вычёркивание неудобной фразы.
|
||||
"""
|
||||
doc = (inspect.getdoc(mod._page_contains_table) or "").lower()
|
||||
assert (
|
||||
"false positive" in doc or "оглавлен" in doc
|
||||
), "докстрока не предупреждает о ложных срабатываниях на оглавлении"
|
||||
|
||||
|
||||
def test_toc_line_really_is_a_false_positive() -> None:
|
||||
"""Характеризующий: строка оглавления действительно проходит как подпись.
|
||||
|
||||
Зелёный с обеих сторон — он фиксирует ФАКТИЧЕСКОЕ поведение, о котором теперь
|
||||
говорит докстрока. Если кто-то реализует подавление, тест покраснеет и
|
||||
заставит обновить и докстроку, и его.
|
||||
"""
|
||||
оглавление = "СОДЕРЖАНИЕ\nТаблица 11 Баланс территории .......... 34\nТаблица 12 ТЭП .... 36"
|
||||
assert mod._page_contains_table(оглавление, 11) is True, "оглавление обязано срабатывать"
|
||||
|
||||
# А вот перекрёстные ссылки в косвенных падежах регекс НЕ ловит: он требует
|
||||
# именительное «Таблица N». Пункт эпика приводил «приведены в таблице 12» как
|
||||
# пример false positive — замер это опроверг, и докстрока теперь говорит точнее.
|
||||
for мимо in (
|
||||
"Основные показатели приведены в таблице 12 настоящей записки.",
|
||||
"см. Таблицу 12 ниже",
|
||||
"Данные в табл. 12",
|
||||
):
|
||||
assert mod._page_contains_table(мимо, 12) is False, f"неожиданно сработало: {мимо!r}"
|
||||
|
||||
assert mod._page_contains_table("Никаких таблиц тут нет", 11) is False
|
||||
|
||||
|
||||
def test_seed_comment_does_not_send_to_a_dead_host() -> None:
|
||||
"""Комментарий сида не должен отправлять искать документ на несуществующем хосте.
|
||||
|
||||
`gisogd.ekburg.ru` не резолвится ни с рабочей машины, ни с прод-хоста
|
||||
(проверено 20.08.2026), а прежний комментарий называл его местом, где «лежит
|
||||
реальный URL». Живой портал — gisogd66.midural.ru.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
src = Path(inspect.getsourcefile(mod)).resolve().parents[3]
|
||||
текст = (src / "app" / "workers" / "tasks" / "ekb_ppt_tep_sync.py").read_text(encoding="utf-8")
|
||||
assert (
|
||||
"НЕ СУЩЕСТВУЕТ" in текст or "не резолв" in текст.lower()
|
||||
), "комментарий сида не предупреждает, что gisogd.ekburg.ru мёртв"
|
||||
assert "gisogd66.midural.ru" in текст, "не назван живой портал ГИСОГД-СО"
|
||||
Loading…
Add table
Reference in a new issue