Нужно понять, в каких странах B2B-конкурент размещает рекламу в этом квартале, как долго крутятся креативы, какой у них примерно объём показов и на какие сегменты нацелен таргетинг. Всё это обычно есть в публичной библиотеке рекламы — такие каталоги многие платформы публикуют для соблюдения требований к прозрачности рекламы. Но после открытия DevTools выясняется, что аккуратного JSON-эндпоинта нет: только целая страница серверного HTML. Вы пишете парсер, он работает и отдаёт данные. Через три недели платформа выкатывает редизайн — и парсер без единой ошибки перестаёт извлекать вообще все поля. На выходе тихо появляются пустые значения, на основании которых легко принять неверное решение.
Ниже — о том, как поддерживать такой парсер после редизайнов и где модель реально полезна в его обслуживании. Сначала о границах данных: здесь используются только публичные библиотеки рекламы и creative center самих платформ, доступные при обычном входе под собственным аккаунтом. Никаких подписей, обходов ограничений или непубличных эндпоинтов. Это ограничение прямо заложено в архитектуру парсера.
Почему B2B-рекламу часто приходится извлекать из HTML
Даже библиотеки рекламы отдают данные очень по-разному. Одни потребительские сервисы — например, библиотека рекламы Meta — предлагают структурированный поиск и JSON. Другие требуют активную сессию уже для самого поиска. А библиотеки рекламы большинства B2B-платформ существуют исключительно в виде серверно отрендеренных HTML-страниц, без JSON-эндпоинта. Причина проста: такая библиотека — артефакт compliance, а не продуктовый API.
Её создавали не для разработчиков, а для выполнения требований к прозрачности рекламы. У неё нет версии, changelog и обещаний обратной совместимости. Страница рассчитана на человека: сервер рендерит HTML, и это единственный доступный вам «API».
Хрупкость здесь неизбежна. Вы зависите от деталей чужого интерфейса, владельцы меняют их когда захотят и не обязаны предупреждать. Изменение поля в JSON API хотя бы считается изменением контракта; для команды платформы переделка HTML — обычная фронтенд-итерация. Полностью избежать этого нельзя. Остаётся строить парсер так, чтобы он корректно деградировал и быстро чинился после редизайна, а не молча возвращал пустые поля.
Потоковый парсер снижает не только расход памяти, но и когнитивную нагрузку
Первый импульс — взять lxml или BeautifulSoup, построить DOM всей страницы и пройтись по нему через .find(). Подход рабочий, но для этой задачи он неудачен. DOM — это промежуточное представление, которое браузер создаёт при отрисовке страницы; MDN определяет DOM как дерево узлов, к которому скрипты обращаются через структуру документа. А вам нужно всего лишь достать несколько полей. Полное дерево не требуется — и привязываться к его форме не стоит.
В итоге я остановился на наследнике стандартного Python-класса HTMLParser: чуть больше восьмисот строк, если точно — 881. Это полностью потоковый парсер. Несколько колбэков starttag / data / endtag управляют конечным автоматом: он накапливает данные по мере сканирования, при границе карточки выдаёт запись, очищает состояние и идёт дальше. Полное DOM-дерево никогда не строится.
Выигрыш у потокового подхода вполне практический. Во-первых, память: HTML страницы с деталями легко занимает от десятков до сотен KB, а DOM хранит в памяти структуру всей страницы. Потоковому парсеру достаточно знать лишь текущую точку сканирования и состояние текущей карточки. Но ещё важнее снижение когнитивной нагрузки. Как только вы пишете .find('div').find('div')[2], логика становится зависимой от иерархической позиции в DOM. А именно иерархию редизайн меняет охотнее всего: добавили контейнер, вынесли обёртку — и все позиции сдвинулись. Конечный автомат заставляет спрашивать другое: «То, что я сейчас сканирую, по смыслу является началом карточки, числом показов или тегом таргетинга?» Позиция меняется, семантика — нет.
Три правила, которые помогают пережить редизайн
Всё сводится к трём принципам — каждый из них обычно становится очевиден после очередного редизайна.
Первое: привязывайтесь к смыслу, а не к позиции. Конечный автомат должен переходить по семантическим сигналам: тексту метки поля — читаемым словам вроде «total impressions» или «run dates», маркерам с ролью, семантической границе блока. Но не по логике «третий узел сверху». Проверка очень простая: сохранится ли разбор, если элемент переместят или обернут ещё одним контейнером? Если да — якорь выбран правильно. Позиционные якоря разваливаются при первом же редизайне, семантические обычно выдерживают чисто визуальные изменения.
Второе: отсутствующее поле не должно вызывать исключение. Перед накоплением данных по каждой карточке создавайте объект по шаблону, где все поля имеют пустые значения по умолчанию: пустая строка для текста, None для чисел, пустой массив для списков. Заполняйте то, что нашли, а остальное оставляйте пустым. Сбой при извлечении одного поля не должен уничтожать ни карточку, ни тем более результаты по всей странице. Если у объявления нет текста CTA, всё равно нужны его показы и целевые страны. Терять всю доступную разведывательную ценность страницы из-за второстепенного поля — худшее архитектурное решение.
Третье: отмечайте полноту результата, чтобы «пусто» не выглядело так же, как «сломано». Это самый частый и самый дорогой промах. Ответ «распарсено 0 объявлений» может означать две совершенно разные вещи: у рекламодателя действительно не было рекламы в этом квартале либо структура страницы изменилась и парсер больше ни за что не зацепился. Эти состояния должны различаться в возвращаемом результате. Для этого добавляйте подтверждающие признаки: число найденных карточек, общий объём, заявленный самой страницей, и состояние пагинации. Тогда комбинацию вроде «карточек 0, но метаданные страницы говорят, что должна быть выдача, а маркера следующей страницы нет» можно распознать как структурное изменение и выдать понятную ошибку вместо безразличного пустого списка.
На этом же уровне реализуется упомянутая ранее граница доступа: парсер проверяет, не перебросили ли его на страницу входа. Как только заголовок указывает на login/signup-страницу, он завершает работу с ошибкой, а не пытается её разобрать. Он обрабатывает только публичные страницы, которые вы обычно видите под собственным аккаунтом, останавливается перед login wall и никогда не пытается его преодолеть.
Главная ценность — в срезах и фильтрах, а не в карточке одного объявления
На этом этапе может показаться, что задача — идеально вытащить все поля каждой рекламы. На самом деле нет. Поля одного объявления сами по себе мало что значат; ценность появляется, когда объявления можно разложить по измерениям. Фильтры поиска в библиотеке рекламы уже содержат готовый набор таких измерений. Если представить их в виде программируемых параметров запроса, получится не «одно объявление», а срез запуска конкурента:
Страна: где компания рекламируется, а где нет. Если B2B-компания внезапно запускает рекламу в новой стране, это нередко указывает на экспансию раньше, чем её собственный сайт.
Период показа — даты начала и окончания: как долго работал конкретный креатив. Долгий срок размещения — сильный сигнал: никто не продолжает платить за креатив, который не конвертирует. Его продолжительность — фактически результат A/B-теста, подтверждённый реальными деньгами и проведённый другой стороной.
Диапазон показов — минимум/максимум: грубый индикатор расходов. Абсолютное значение неточно, но его хватает, чтобы расставить приоритеты между размещениями.
Аспекты таргетинга: какие настройки включены или исключены. Это наиболее прямой источник данных об аудитории — о тех, кого конкурент считает потенциальными покупателями своего продукта.
Извлечение полей — лишь средство, а эти измерения — цель. Поэтому при проектировании парсера полезно идти от обратного: какие минимальные поля нужно стабильно извлекать, чтобы поддержать запросы и сортировку по этим срезам? Остальные красивые, но второстепенные поля можно не собирать без ущерба для разведки.
После редизайна поручите модели сравнить старый и новый HTML
Парсер такого типа неизбежно сломается после редизайна. И именно при его ремонте модель действительно уместна — но не внутри самого парсинга. Парсинг детерминирован: это жёстко заданный конечный автомат, куда не следует добавлять вызовы модели. Не отдавайте модели детерминированную работу — здесь действует тот же принцип, что и в reverse engineering. Модель нужна для сопровождения.
Рабочий процесс такой: откройте библиотеку рекламы под своим аккаунтом, сохраните HTML до редизайна и HTML после него, передайте оба файла модели вместе со списком полей, которые извлекает текущий парсер. Попросите определить по diff, у каких полей изменились семантические якоря, где теперь должен быть новый якорь и какие несколько строк нужно минимально поправить. Это типичная задача для reasoning-tier: модели нужно найти, сохранилась ли соответствующая семантика в новой структуре, и предложить применимый план, а не ограничиться фразой «структура была изменена». Разным этапам нужны разные способности. Одна модель на всё либо обходится дороже, либо снижает точность:
Этап | Что требуется от модели | Выбор | model id |
|---|---|---|---|
Прочитать всю SSR HTML-страницу и сопоставить старую и новую структуру | Длинный контекст: модель должна целиком принять страницу с деталями размером от десятков до сотен KB | Kimi K3 |
|
После редизайна разобрать diff старого и нового HTML, определить дрейф якоря и предложить минимальную правку | Сильное рассуждение: объясняет изменение через структуру, а не просто повторяет факт | Claude Opus 5 |
|
Массово нормализовать и размечать карточки сотен рекламодателей в разведывательные данные | Низкая стоимость, сотни или тысячи вызовов с высокой параллельностью | Claude Sonnet 5 |
|
Установить причину расхождения, когда сравнение с фикстурой не проходит | Средний уровень рассуждений: объяснение через «ожидаемое поле против фактического извлечения» | GPT-5.6 Sol |
|
Второй уровень — ключевой и единственный этап, где смена модели заметно влияет на результат. Не верьте на слово, что здесь нужен reasoning-tier: это легко проверить коротким протоколом.
Сохраните HTML одной страницы до редизайна и после него: откройте библиотеку рекламы под своим аккаунтом и сохраните страницу.
Передайте оба HTML-файла вместе со списком полей текущего парсера в
claude-opus-5иgpt-5.6-sol.Оцените один критерий: указывает ли предложенное исправление на конкретную смену семантического якоря — например, «раньше якорем была метка 'total impressions', в новой версии изменилась роль контейнера этой метки, переключите якорь на X» — или ограничивается расплывчатым «структура была изменена, рекомендуем адаптировать решение».
Первый ответ можно применить сразу, второй ничего не даёт. В этом и состоит критерий выбора: от него напрямую зависит, сколько итераций вслепую вы пройдёте в день редизайна.
Одного такого сравнения достаточно, чтобы увидеть разницу яснее любого бенчмарка.
Главная проблема — цена переключения между моделями
Четыре уровня распределены между тремя вендорами, тремя SDK, тремя схемами авторизации и тремя форматами ошибок. Большинство смотрит на объём работы по подключению трёх клиентов для разных этапов, решает, что это не стоит усилий ради «редкого ремонта парсера и массовой очистки данных», и остаётся на одной модели. В итоге для починки после редизайна используется уровень, который умеет лишь сказать «рекомендуем адаптировать», — время тратится, а причина неясна.
AIReiter убирает этот слой сложности: один ключ, один OpenAI-совместимый интерфейс, все четыре уровня за ним. Для переключения достаточно изменить поле model в теле запроса.
# Исправление после редизайна: reasoning-tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<old-new HTML diff + current field list, ask where the anchor drifted>"}]
}'
# Массовая нормализация разведданных: меняем model, остальное оставляем как есть
# "model": "claude-sonnet-5"
# Длинный контекст для чтения всей страницы: "model": "kimi-k3"
# Установление причины расхождений: "model": "gpt-5.6-sol"
Если вы уже используете OpenAI SDK, укажите для base_url значение https://aireiter.com/api/v1 и больше ничего не меняйте. Для Anthropic SDK используйте POST /api/v1/messages с тем же ключом.
По цене: модели Claude доступны со скидкой 30% от прайс-листа, модели GPT — за половину цены, а Kimi K3 вызывается тем же ключом. В этом сценарии скидка приходится на основные расходы. Главная статья — не ремонт после редизайна: это редкий, но ценный вызов. Основные затраты создаёт массовая нормализация разведывательных данных: вы отслеживаете двадцать рекламодателей-конкурентов, у каждого десятки или сотни карточек, и передаёте их модели, чтобы выделить обещание, аудиторию и период показа. Это самый плотный по вызовам этап, выполняемый на Claude Sonnet со скидкой 30%. Следом идёт длинный контекст для чтения целой страницы и сопоставления структуры. Две самые дорогие части процесса как раз покрываются скидкой.
Попробовать без регистрации: вручную передайте одной и другой модели пару HTML до и после изменений, сравните, какая действительно укажет место дрейфа якоря, и уже затем решайте, стоит ли подключать это в процесс.
После ремонта парсера и массовой нормализации исходных карточек следующий шаг — передать эту разведку в creative pipeline для принятия решений и генерации ассетов. Этому посвящён полный цикл от ключевого слова до готового объявления; текущая статья описывает его самый ранний этап — надёжное извлечение публичных данных.
Последнее слово о корректности полей остаётся за фикстурой
Пока исправление от модели не прошло проверку, это лишь рекомендация. Модель может сказать: «якорь нужно заменить на X», и предложение будет выглядеть логично, но нет гарантии, что X подходит для всех карточек. B2B-объявления бывают с изображением и текстом, только текстовые, карусельные, с посадочной страницей и без неё. Два примера, которые видела модель, могут не покрывать всё разнообразие.
Защита здесь та же, что и от галлюцинаций модели в reverse engineering: сравнение по фиксированному вектору. Сохраните набор известных входов — несколько HTML реальных страниц — и их заведомо корректные выходы, то есть результаты извлечения полей, однажды проверенные вручную. Оформите их как фикстуру и закоммитьте в репозиторий. При каждом изменении парсера, сделанном вручную или по рекомендации модели, запускайте этот набор заново и сравнивайте поля по одному. Только так после внешнего редизайна можно быстро понять: «я неправильно применил рекомендацию или страница снова изменилась?» Если всё проходит — правка верна. Если падают отдельные случаи, проблемные поля в них сразу показывают, на каком слое находится ошибка. Модель генерирует предложения по ремонту, фикстура определяет, верны ли они; смешивать эти роли нельзя. Полный метод такого дифференциального сравнения описан в материале о дифференциальном тестировании; к парсеру библиотеки рекламы применяется тот же барьер. Без него вы принимаете уверенность модели за корректность. Тогда легко «внести правку, не увидеть ошибок, выкатить её — и через три дня обнаружить, что данные по одной стране всё это время были пустыми».
Итог
B2B-разведку по рекламе часто приходится извлекать из HTML, потому что библиотека рекламы — это compliance-артефакт, а не продуктовый API: у неё нет контракта, а редизайн может случиться в любой момент. Устойчивость к изменениям держится на трёх принципах: использовать семантические, а не позиционные якоря; корректно переживать отсутствие полей вместо исключений; отмечать полноту результата, чтобы «пусто» и «сломано» не смешивались. Настоящая ценность не в полях отдельного объявления, а в измерениях, по которым можно разложить запуск конкурента: стране, периоду показа, диапазону показов и аспектам таргетинга. У модели здесь конкретная роль: не парсинг, который должен оставаться детерминированным конечным автоматом, а сопровождение. После редизайна reasoning-tier помогает сопоставить старую и новую структуру и предложить ремонт, а недорогой уровень с высокой параллельностью подходит для массового превращения карточек в разведывательные данные. Но окончательное решение о корректности полей всегда принимает фикстура. Подключите эти уровни через единый интерфейс — и единственным трением останется смена поля model, которую решает выбор модели.