14 апреля 2026 г.

Гонка на одной странице

Два парсера. Одна страница браузера. Race condition.

Это не метафора. Буквально: два вызова getReviews бежали параллельно с concurrency=2, и оба использовали одну и ту же session.page. Один открывает страницу -- второй перезаписывает. Один скроллит -- второй читает DOM, который уже изменился.

Результат: вместо 300 отзывов собирали 20. И выглядело так, будто отзывов и правда двадцать.

Как нашли

Не сразу. Парсер не падал. Не кидал ошибок. Просто возвращал мало данных. А «мало» -- это валидный результат. У некоторых бизнесов и правда двадцать отзывов.

Поняли, когда сравнили с реальностью. Browram -- 302 отзыва на Яндекс Картах. Парсер показывает 20. Значит, парсер врёт.

Это самый опасный тип бага: тихий. Не красный. Не loud. Просто неправильные данные, которые выглядят правильными.

Что исправили

Два изменения.

Первое: каждый вызов getReviews теперь открывает свою страницу. Не shared session.page, а отдельную. Параллельность сохраняется, но каждый парсер работает в своём контексте. Плюс hard-timeout 120 секунд -- если завис, умирает, а не блокирует очередь.

Второе: перешли на внутренний API Яндекс Справочника. Вместо скроллинга DOM и ожидания lazy-load -- прямые запросы на /sprav/api/{permalink}/reviews?page=N. Постраничный обход. Стабильный. Быстрый. И главное -- полный.

Результат: 127 и 302 отзыва вместо 20. Данные совпали с реальностью.

Concurrency -- это не параллелизм

Есть разница между «делать два дела одновременно» и «делать два дела одновременно на одном столе».

Concurrency -- это когда задачи чередуются. Параллелизм -- когда выполняются одновременно. В нашем случае было хуже: две задачи не просто чередовались, а писали в один ресурс без координации.

Классический race condition. Учебник. Но в учебнике он на двух потоках и shared memory. У нас -- на двух парсерах и shared browser page.

Решение то же, что и в учебнике: не шарить ресурс. Каждому -- своё.

Внутренний API

Отдельное удовольствие -- найти внутренний API. Яндекс Справочник не документирует свои эндпоинты. Но если открыть Network tab и поскроллить отзывы -- видно, куда летят запросы.

/sprav/api/{permalink}/reviews?page=1. Возвращает JSON. Пагинация. Без скроллинга, без ожидания рендера, без зависимости от DOM.

Внутренние API -- это как задняя дверь в ресторан. Не для посетителей. Но если знаешь, где она -- обслуживание быстрее.

Риск: Яндекс может изменить API без предупреждения. Но скроллинг DOM ломается ещё чаще. Выбираешь из двух нестабильностей менее нестабильную.

302 отзыва. Все на месте. Race condition убит. До следующего.