fix(tradein/yandex): newbuilding resolve/fetch_jk через пул прокси + стоп на пустом пуле (#3197, точки 2-3) #3389

Merged
bot-backend merged 2 commits from fix/3197-yandex-newbuilding-proxy-pool into main 2026-09-06 02:24:45 +00:00
8 changed files with 322 additions and 44 deletions

View file

@ -1944,7 +1944,10 @@ async def scrape_yandex_newbuilding(
"""
# fetch_jk использует внутренний BrowserFetcher, httpx-клиент BaseScraper не нужен.
# config= обязателен (#2322 fix) — иначе BrowserFetcher строится без endpoint=.
scraper = YandexNewbuildingScraper(config=RealScraperConfig())
# proxy_provider (#3197): без него сайдкар уходит на свой env-узел мимо пула.
scraper = YandexNewbuildingScraper(
config=RealScraperConfig(), proxy_provider=_kit_proxy_provider()
)
result = await scraper.fetch_jk(jk_slug=slug, jk_id=id, city=city)
if result is None:
raise HTTPException(404, f"Could not parse Yandex JK: {slug}-{id} in {city}")

View file

@ -367,7 +367,27 @@ async def _job_yandex_newbuilding_sweep(
# исход — назвать прогон неуспешным, а не дотянуть succeeded до ненуля.
processed = int(counters.get("processed") or 0)
succeeded = int(counters.get("succeeded") or 0)
if processed > 0 and succeeded == 0:
if result.no_proxy_stop:
# #3197: прогон оборван на пустом пуле — к площадке не ходили вовсе.
# Это отказ нашей инфраструктуры, а не «ЖК не разрешились»: называть
# такой прогон успешным нельзя, и причина должна быть отличима.
#
# INFO, как у соседей (scheduler.py:175, avito_detail_backfill.py:1050,
# domclick:626): причина уже записана в mark_failed + counters.no_proxy_stop=1,
# а ERROR ставил её в один разряд с падением задачи (logger.exception ниже).
logger.info(
"yandex_newbuilding_sweep run_id=%d: пул прокси пуст — прогон оборван "
"(обработано %d)",
run_id,
processed,
)
ctx.runs.mark_failed(
db,
run_id,
"пул прокси пуст — прогон оборван, к площадке не ходили",
counters,
)
elif processed > 0 and succeeded == 0:
logger.warning(
"yandex_newbuilding_sweep run_id=%d: обработано %d, разрешено 0 — "
"помечаю прогон неуспешным",

View file

@ -43,10 +43,11 @@ import random
import time
from dataclasses import dataclass, field, fields
from scraper_kit.proxy_errors import caused_by_no_proxy
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.services.scraper_adapters import RealScraperConfig
from app.services.scraper_adapters import RealProxyProvider, RealScraperConfig
from app.services.scraper_settings import get_scraper_delay
logger = logging.getLogger(__name__)
@ -78,10 +79,21 @@ class YandexNewbuildingSweepResult:
failed_fetch: int = 0 # fetch_jk вернул None / упал
rows_inserted: int = 0 # строк в market.yandex_jk_enrichment (новых/обновлённых)
# Пул прокси пуст → прогон оборван, к площадке не ходили (#3197).
no_proxy_stop: bool = False
duration_sec: float = field(default=0.0)
def to_dict(self) -> dict[str, int | float]:
return {f.name: getattr(self, f.name) for f in fields(self)}
"""Счётчики прогона; `no_proxy_stop` — 1/0, а не JSON true/false.
Формат один с соседями (`scheduler.py:168` cian, `avito_detail_backfill.py:1043`,
`domclick_detail_backfill.py:610`): SQL-монитор ищет
`counters->>'no_proxy_stop' = '1'` и мимо `true` промахнётся молча.
"""
out: dict[str, int | float] = {f.name: getattr(self, f.name) for f in fields(self)}
out["no_proxy_stop"] = int(self.no_proxy_stop)
return out
# ── SQL ────────────────────────────────────────────────────────────────────────
@ -331,9 +343,25 @@ async def enrich_yandex_newbuilding_sweep(
try:
resolved = await resolve_yandex_jk_slug(
ext_id, city=city, config=RealScraperConfig()
ext_id,
city=city,
config=RealScraperConfig(),
proxy_provider=RealProxyProvider(),
)
except Exception as exc:
# #3197: пустой пул — не отказ площадки: запрос не уходил вовсе, и
# следующий дом упрётся ровно в то же самое.
if caused_by_no_proxy(exc):
result.no_proxy_stop = True
result.failed_resolve += 1
logger.error(
"yandex-nb-sweep: СТОП — пул прокси пуст, к площадке не ходили. "
"house_id=%s processed=%d succeeded=%d",
house_id,
result.processed,
result.succeeded,
)
break
logger.warning(
"resolve_yandex_jk_slug house_id=%s ext_id=%s raised: %s",
house_id,
@ -383,9 +411,22 @@ async def enrich_yandex_newbuilding_sweep(
# ── Fetch через BrowserFetcher ────────────────────────────────────
info = None
try:
scraper = YandexNewbuildingScraper(config=RealScraperConfig())
scraper = YandexNewbuildingScraper(
config=RealScraperConfig(), proxy_provider=RealProxyProvider()
)
info = await scraper.fetch_jk(jk_slug=jk_slug, jk_id=ext_id, city=city)
except Exception as exc:
if caused_by_no_proxy(exc): # #3197 — см. блок resolve выше
result.no_proxy_stop = True
result.failed_fetch += 1
logger.error(
"yandex-nb-sweep: СТОП — пул прокси пуст, к площадке не ходили. "
"house_id=%s processed=%d succeeded=%d",
house_id,
result.processed,
result.succeeded,
)
break
logger.warning(
"fetch_jk failed house_id=%s jk_slug=%s ext_id=%s: %s",
house_id,

View file

@ -27,18 +27,21 @@ import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from app.services.product_handlers import _job_yandex_newbuilding_sweep
from app.tasks.yandex_newbuilding_sweep import YandexNewbuildingSweepResult
def _ctx() -> SimpleNamespace:
return SimpleNamespace(runs=MagicMock())
def _result(processed: int, succeeded: int) -> MagicMock:
r = MagicMock()
r.to_dict = MagicMock(
return_value={"processed": processed, "succeeded": succeeded, "total": 425}
)
return r
def _result(processed: int, succeeded: int) -> YandexNewbuildingSweepResult:
"""Настоящий результат задачи, а не MagicMock.
У MagicMock любой не заданный атрибут (`no_proxy_stop` из #3197) — truthy, то есть
добавление хендлеру новой ветки по полю результата уводило бы сюда ВСЕ три случая,
и тест краснел бы по причине, не имеющей отношения к #2860.
"""
return YandexNewbuildingSweepResult(processed=processed, succeeded=succeeded, total=425)
@pytest.mark.asyncio

View file

@ -0,0 +1,179 @@
"""#3197 (часть 1, Яндекс-новостройки) — sweep ходил в сайдкар мимо прокси-пула.
Обе точки провайдера (`YandexNewbuildingScraper.fetch_jk` и `resolve_yandex_jk_slug`)
конструировали `BrowserFetcher(source="yandex", endpoint=...)` БЕЗ
`proxy_provider`/`use_pool`/`environment` единственных трёх аргументов, которые кладут
"proxy" в тело POST /fetch. Без них сайдкар брал свой env-прокси (`SCRAPER_PROXY_URL`),
на проде это выключенный узел (407 camoufox `InvalidIP` `/fetch` 503, факт #3386),
а прод-отказ «пул пуст не ходить на env/direct» (#2616) на этом пути был мёртв: он
смотрит на `environment`, который до конструктора не доезжал. Соседи уже починены:
cian (#3197, test_3197_cian_history_proxy_pool_wiring.py) и domclick/avito.
Второй тест про то, чем оживший отказ оборачивается в прогоне: пустой пул поднимается
ДО запроса, поэтому следующий ЖК упрётся ровно в то же самое, и прогон обязан оборваться
на первом, а не перебирать весь список. Подделка на уровне `BrowserFetcher.fetch()`,
НАСТОЯЩИЕ `resolve_yandex_jk_slug`/`fetch_jk`: их `except Exception` глотал любое
исключение и возвращал None, поэтому стоп в задаче не сработал бы ни разу (образец
дефекта #3382, `cian/detail.py`).
Сеть/БД/камуфокс замоканы; в сеть тест не ходит.
"""
from __future__ import annotations
import os
from typing import Any, ClassVar
from unittest.mock import AsyncMock, MagicMock, patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import pytest
from scraper_kit.proxy_errors import NoProxyAvailableError
from app.tasks import yandex_newbuilding_sweep as sweep
_BASE_FETCHER = "scraper_kit.providers._base.BrowserFetcher"
class _CapturingFetcher:
"""Собирает kwargs КАЖДОЙ конструкции — обе точки (resolve и fetch_jk) видны разом."""
captured: ClassVar[list[dict[str, Any]]] = []
def __init__(self, **kwargs: Any) -> None:
_CapturingFetcher.captured.append(kwargs)
async def __aenter__(self) -> _CapturingFetcher:
return self
async def __aexit__(self, *_: object) -> None:
return None
async def fetch(self, _url: str, **_kwargs: Any) -> str:
# Короткий HTML: resolve не найдёт slug, fetch_jk отбракует как tiny — обе
# ветки идут дальше по циклу, а нам нужны только kwargs конструктора.
return "<html></html>"
class _EmptyPoolFetcher:
"""Фетчер с пустым пулом: `fetch()` поднимает `NoProxyAvailableError` ДО запроса."""
calls: ClassVar[list[str]] = []
def __init__(self, **_kwargs: Any) -> None:
pass
async def __aenter__(self) -> _EmptyPoolFetcher:
return self
async def __aexit__(self, *_: object) -> None:
return None
async def fetch(self, url: str, **_kwargs: Any) -> str:
_EmptyPoolFetcher.calls.append(url)
raise NoProxyAvailableError("yandex")
def _mock_db(rows: list[dict[str, Any]]) -> MagicMock:
db = MagicMock()
res = MagicMock()
res.scalar_one.return_value = 0 # counts
res.mappings.return_value.all.return_value = rows
res.fetchone.return_value = None # idempotency-проба: ещё не обогащён
db.execute.return_value = res
return db
def _row(house_id: int, *, slug: str | None, ext_id: str | None) -> dict[str, Any]:
return {"house_id": house_id, "yandex_jk_slug": slug, "ext_id": ext_id}
@pytest.mark.parametrize(("use_pool", "environment"), [(True, "production"), (False, "dev")])
async def test_browser_fetcher_gets_proxy_pool_wiring(
monkeypatch: pytest.MonkeyPatch, use_pool: bool, environment: str
) -> None:
"""use_pool/proxy_provider/environment доезжают до BrowserFetcher ИЗ КОНФИГА.
Оба значения флага одним телом: `use_pool` обязан следовать конфигу, а не быть
зашитой константой, `proxy_provider` передаётся в любом случае (при use_pool=False
фетчер его игнорирует, но call-site у dev и прода один).
Строки подобраны так, чтобы прогон прошёл ОБЕ точки: дом без slug идёт в
`resolve_yandex_jk_slug`, дом со slug в `fetch_jk`.
"""
from app.core.config import settings
_CapturingFetcher.captured = []
monkeypatch.setattr(settings, "use_proxy_pool_browser", use_pool)
monkeypatch.setattr(settings, "environment", environment)
rows = [_row(1, slug=None, ext_id="111"), _row(2, slug="tatlin", ext_id="222")]
with patch(_BASE_FETCHER, _CapturingFetcher):
await sweep.enrich_yandex_newbuilding_sweep(_mock_db(rows), limit=2, request_delay_sec=0.0)
captured = _CapturingFetcher.captured
assert len(captured) == 2, "обе точки (resolve и fetch_jk) обязаны строить фетчер"
for kwargs in captured:
assert kwargs["source"] == "yandex"
# .get(), а не [] — красное должно читаться как «значение не то», а не KeyError.
assert kwargs.get("proxy_provider") is not None, "без провайдера пул не подключится"
assert kwargs.get("use_pool") is use_pool, "флаг пула должен доезжать из конфига"
# #2616 шаг 1: без environment отказ «пул пуст» на этом пути мёртв.
assert kwargs.get("environment") == environment
@pytest.mark.parametrize(
("rows", "url_part"),
[
([_row(i, slug=None, ext_id=str(i)) for i in (1, 2, 3)], "-1/"),
([_row(i, slug=f"jk{i}", ext_id=str(i)) for i in (1, 2, 3)], "jk1-1/"),
],
ids=["resolve", "fetch_jk"],
)
async def test_empty_pool_stops_the_sweep_on_first_house(
rows: list[dict[str, Any]], url_part: str
) -> None:
"""«Пул пуст» на первом доме обрывает прогон, а не перебирает все три.
Замокано: БД и сам `BrowserFetcher` подделка кончается на `fetch()`. НЕ замоканы
`resolve_yandex_jk_slug`/`fetch_jk`: прогон идёт через настоящие функции провайдера,
иначе тест был бы зелёным по построению (живые функции исключение ГЛОТАЛИ, и стоп
не срабатывал ровно дефект #3382).
"""
_EmptyPoolFetcher.calls = []
with patch(_BASE_FETCHER, _EmptyPoolFetcher):
result = await sweep.enrich_yandex_newbuilding_sweep(
_mock_db(rows), limit=3, request_delay_sec=0.0
)
# Сначала измеримое поведение (сколько раз пошли), потом флаг: красное на откате
# должно означать «прошли 3 дома вместо 1», а не «поля нет».
assert len(_EmptyPoolFetcher.calls) == 1, "к сайдкару ходили ровно один раз"
assert _EmptyPoolFetcher.calls[0].endswith(url_part)
assert result.processed == 1, "прогон обязан оборваться, а не пройти все 3 дома"
assert getattr(result, "no_proxy_stop", False) is True
async def test_scheduler_marks_run_failed_on_empty_pool() -> None:
"""Прогон с пустым пулом финализируется как failed, а не 'done' с честным нулём."""
from app.services import product_handlers as ph
ctx = MagicMock()
result = sweep.YandexNewbuildingSweepResult(processed=1, failed_fetch=1, no_proxy_stop=True)
with patch(
"app.tasks.yandex_newbuilding_sweep.enrich_yandex_newbuilding_sweep",
AsyncMock(return_value=result),
):
await ph._job_yandex_newbuilding_sweep(MagicMock(), 3197, {"limit": 3}, ctx)
ctx.runs.mark_done.assert_not_called()
ctx.runs.mark_failed.assert_called_once()
_db, run_id, reason, counters = ctx.runs.mark_failed.call_args.args
assert run_id == 3197
assert "пул" in reason
# Ровно 1, а не JSON true: SQL-монитор соседей ищет counters->>'no_proxy_stop' = '1'
# (scheduler.py:168 cian, avito_detail_backfill.py:1043, domclick_detail_backfill.py:610).
# bool — подкласс int, поэтому `== 1` один сам по себе зелёный и на True: тип проверяем
# отдельно, иначе красное на откате не наступит.
assert counters["no_proxy_stop"] == 1 and not isinstance(counters["no_proxy_stop"], bool)

View file

@ -97,11 +97,15 @@ async def test_cian_fetch_newbuilding_endpoint_none_without_config(monkeypatch):
@pytest.mark.asyncio
async def test_yandex_fetch_jk_passes_endpoint_from_config(monkeypatch):
"""config передан в конструктор скрапера → endpoint пробрасывается в BrowserFetcher."""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy)
"""config передан в конструктор скрапера → endpoint пробрасывается в BrowserFetcher.
config = SimpleNamespace(browser_http_endpoint=_TEST_ENDPOINT)
С #3197 путь идёт через `build_browser_fetcher` (подключение к пулу прокси) — как у
cian выше, подменяется BrowserFetcher В ФАБРИКЕ, сама фабрика настоящая.
"""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
config = SimpleNamespace(browser_http_endpoint=_TEST_ENDPOINT, use_proxy_pool_browser=False)
scraper = YandexNewbuildingScraper(config=config)
await scraper.fetch_jk(jk_slug="tatlin", jk_id="1592987")
@ -131,11 +135,14 @@ async def test_yandex_fetch_jk_endpoint_none_without_config(monkeypatch):
@pytest.mark.asyncio
async def test_resolve_yandex_jk_slug_passes_endpoint_from_config(monkeypatch):
"""config= (новый kwarg-only параметр) должен пробрасывать endpoint в BrowserFetcher."""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy)
"""config= (новый kwarg-only параметр) должен пробрасывать endpoint в BrowserFetcher.
config = SimpleNamespace(browser_http_endpoint=_TEST_ENDPOINT)
#3197: конструкция переехала в `build_browser_fetcher` — спай ставим на фабрику.
"""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
config = SimpleNamespace(browser_http_endpoint=_TEST_ENDPOINT, use_proxy_pool_browser=False)
await resolve_yandex_jk_slug("1592987", config=config)
spy.assert_called_once()

View file

@ -206,16 +206,18 @@ def build_browser_fetcher(
`ScraperConfig` невозможно на уровне сигнатуры (не полагаемся на
code-review чтобы поймать пропущенный параметр).
`proxy_provider=None` (дефолт) валидно: часть providers (avito legacy
curl-only путь, domclick, cian/yandex newbuilding) пока не подключены к
browser-proxy-пулу (#2160 P4 wiring gap, отдельный issue) — `use_pool` в
этом случае эффективно игнорируется `BrowserFetcher` (`proxy_provider is
None` env-fallback, см. `browser_fetcher.py::_pool_proxy`).
`proxy_provider=None` (дефолт) валидно, но у providers больше не встречается:
после #3382 (cian newbuilding) и #3389 (yandex newbuilding) провайдер передают
ВСЕ call-site'ы providers (avito/cian/domclick/yandex, serp и newbuilding).
Без пула остались только прямые конструкции `BrowserFetcher(...)` мимо этой
фабрики служебные ручки `app/api/v1/admin.py` (511 cian-login, 702
domclick-detail-debug); прямые конструкции в `orchestration/pipeline.py` и в
backfill-задачах пул получают. `use_pool` при `proxy_provider is None` эффективно
игнорируется `BrowserFetcher` (env-fallback, см. `browser_fetcher.py::_pool_proxy`).
`fetch_timeout_s=None` (дефолт) используется дефолт `BrowserFetcher`
(120s). Yandex-провайдер передаёt здесь 30s явно (см. вызовы в
`yandex/serp.py`/`yandex/newbuilding.py`) единственный provider с
отличным от дефолта таймаутом.
(120s). Явный таймаут передаёт ровно один call-site `yandex/serp.py` (30s);
`yandex/newbuilding.py` идёт на дефолтных 120s.
`environment=getattr(config, "environment", "dev")` (#2616 шаг 1) — прокидывается в
`BrowserFetcher._pool_proxy`: пул пуст/сломан + прод отказ вместо мёртвого

View file

@ -29,6 +29,8 @@ from selectolax.parser import HTMLParser, Node
from scraper_kit.base import BaseScraper
from scraper_kit.browser_fetcher import BrowserFetcher
from scraper_kit.providers._base import build_browser_fetcher
from scraper_kit.proxy_errors import caused_by_no_proxy
from scraper_kit.yandex_helpers import (
RE_JK_ID,
parse_house_class,
@ -39,7 +41,7 @@ if TYPE_CHECKING:
# TYPE_CHECKING-импорт (как в avito/imv.py, cian/newbuilding.py) — избегаем
# hard import cycle и лишней зависимости в офлайн-тестах парсинга, которым
# BrowserFetcher/config не нужны.
from scraper_kit.contracts import ScraperConfig
from scraper_kit.contracts import ProxyProvider, ScraperConfig
logger = logging.getLogger(__name__)
@ -158,6 +160,7 @@ class YandexNewbuildingScraper(BaseScraper):
*,
config: ScraperConfig | None = None,
delay_provider: Callable[[str], float] | None = None,
proxy_provider: ProxyProvider | None = None,
) -> None:
super().__init__()
# Strangler-инжекция (#2133): провайдер задержки приходит снаружи вместо
@ -169,6 +172,11 @@ class YandexNewbuildingScraper(BaseScraper):
# kwarg, что давало TypeError на любом вызове fetch_jk и не давало caller'у
# исправить это снаружи (сигнатура вообще не принимала config).
self._config = config
# #3197: без proxy_provider сайдкар берёт свой env-узел (SCRAPER_PROXY_URL) —
# на проде это выключенный узел 9 (407 → camoufox InvalidIP → /fetch 503),
# то есть путь шёл мимо пула целиком, а прод-отказ «пул пуст» (#2616) был мёртв
# (он смотрит на environment, который сюда не доезжал).
self._proxy_provider = proxy_provider
async def fetch_around(self, lat: float, lon: float, radius_m: int = 1000) -> list: # type: ignore[override]
raise NotImplementedError(
@ -184,11 +192,23 @@ class YandexNewbuildingScraper(BaseScraper):
через tradein-browser (camoufox) контейнер единственный рабочий путь (#974).
"""
url = f"{self.base_url}/{city}/kupit/novostrojka/{jk_slug}-{jk_id}/"
endpoint = self._config.browser_http_endpoint if self._config is not None else None
# #3197: через фабрику — endpoint/use_pool/environment из config mandatory
# (образец: yandex/serp.py::__aenter__). config=None (dev/офлайн-тесты) —
# прежний путь без пула, как в cian/newbuilding.py.
if self._config is not None:
fetcher_cm = build_browser_fetcher(
self._config, "yandex", proxy_provider=self._proxy_provider
)
else:
fetcher_cm = BrowserFetcher(source="yandex", endpoint=None) # type: ignore[arg-type]
try:
async with BrowserFetcher(source="yandex", endpoint=endpoint) as fetcher:
async with fetcher_cm as fetcher:
html = await fetcher.fetch(url)
except Exception:
except Exception as exc:
# «Пул пуст» — не отказ страницы: запрос не уходил вовсе, и вызывающий обязан
# оборвать прогон, а не перебирать оставшиеся ЖК (#3197, образец #3382).
if caused_by_no_proxy(exc):
raise
logger.exception("yandex nb browser fetch failed: %s", url)
return None
if not html or len(html) < 500:
@ -340,6 +360,7 @@ async def resolve_yandex_jk_slug(
city: str = "ekaterinburg",
*,
config: ScraperConfig | None = None,
proxy_provider: ProxyProvider | None = None,
) -> str | None:
"""Найти Yandex Realty slug для ЖК по его ext_id (jk_id).
@ -368,7 +389,10 @@ async def resolve_yandex_jk_slug(
Args:
jk_id: Yandex Realty ext_id ЖК.
city: город (по умолчанию ekaterinburg).
config: ScraperConfig источник browser_http_endpoint для BrowserFetcher.
config: ScraperConfig источник browser_http_endpoint/use_pool/environment
для BrowserFetcher (через build_browser_fetcher).
proxy_provider: пул браузерных прокси (#3197). None → узел берётся из env
сайдкара, как было; на проде этот env-узел выключен (407).
Returns:
slug (str без id-суффикса), или None. Причина None всегда попадает в лог
@ -376,15 +400,18 @@ async def resolve_yandex_jk_slug(
такого ЖК нет» требуют разных действий, и сливать их в одно сообщение
значит заставлять следующего читателя гадать.
"""
page_url = (
f"https://realty.yandex.ru/{city}/kupit/novostrojka/"
f"{_JK_SLUG_PLACEHOLDER}-{jk_id}/"
)
endpoint = config.browser_http_endpoint if config is not None else None
page_url = f"https://realty.yandex.ru/{city}/kupit/novostrojka/{_JK_SLUG_PLACEHOLDER}-{jk_id}/"
# #3197: см. fetch_jk — та же проводка пула через фабрику.
if config is not None:
fetcher_cm = build_browser_fetcher(config, "yandex", proxy_provider=proxy_provider)
else:
fetcher_cm = BrowserFetcher(source="yandex", endpoint=None) # type: ignore[arg-type]
try:
async with BrowserFetcher(source="yandex", endpoint=endpoint) as fetcher:
async with fetcher_cm as fetcher:
html = await fetcher.fetch(page_url)
except Exception as exc:
if caused_by_no_proxy(exc):
raise
logger.warning("resolve_yandex_jk_slug jk_id=%s: браузер не ответил: %s", jk_id, exc)
return None
@ -395,11 +422,7 @@ async def resolve_yandex_jk_slug(
# Плейсхолдер отбрасываем: страница ссылается сама на себя запрошенным
# адресом, и без этого фильтра мы бы «разрешили» slug в тот, что сами же
# и придумали — то есть записали бы выдумку как факт.
slugs = [
slug
for slug in _slug_link_re(jk_id).findall(html)
if slug != _JK_SLUG_PLACEHOLDER
]
slugs = [slug for slug in _slug_link_re(jk_id).findall(html) if slug != _JK_SLUG_PLACEHOLDER]
if not slugs:
logger.warning(
"resolve_yandex_jk_slug jk_id=%s: на странице нет ни одной ссылки с этим id "