Месяц назад я тратил каждое утро на одно и то же: открывал пять вкладок, руками переписывал цены в таблицу, сравнивал с вчерашними. Минут двадцать, каждый день, без выходных. В какой-то момент поймал себя на мысли — это же ровно та работа, которую должна делать машина.
Решил собрать агента. Не просто скрипт парсинга, а штуку, которая сама решает что проверить, замечает аномалии и пишет мне когда что-то изменилось. Вот что из этого вышло — с граблями, тупиками и финальным результатом.
Сначала я пошёл не туда
Первый порыв был предсказуемый: нашёл библиотеку, написал цикл, запустил. Минут через двадцать у меня была таблица с ценами. Отлично, подумал я.
Потом открыл её на следующий день. Один конкурент поменял структуру страницы — селекторы поехали, половина цен пустая. Другой поставил защиту от ботов, и вместо цифр — Cloudflare-заглушка. Третий грузит цены через JavaScript после загрузки, обычный requests их вообще не видит.
Дело в том что простой скрипт — это не агент. Это хрупкий пайплайн, который ломается при первом изменении на сайте конкурента. Мне нужна была система, которая умеет адаптироваться.
Что такое агент в этом контексте
Я понимаю под агентом систему с тремя свойствами: она сама выбирает инструменты под задачу, замечает когда что-то пошло не так и пробует другой подход, и умеет объяснить что нашла. Не просто выдать таблицу цифр, а сказать: «цена на позицию X у конкурента Y упала на 15% за три дня, это похоже на акцию».
Для этого я собрал связку из нескольких компонентов. Основа — языковая модель как "мозг": она получает данные и решает что с ними делать. Вокруг неё — набор инструментов: парсер для статичных страниц, браузер с JavaScript для динамических, база данных для хранения истории и канал оповещений.
Звучит громоздко, на практике — нет. Разберу каждый кусок.
Как устроен сбор данных
Я остановился на двухуровневом подходе. Сначала пробую лёгкий вариант — requests + BeautifulSoup. Если страница отдаёт нормальный HTML с ценой внутри, этого хватает: быстро, дёшево, не нагружает сервер конкурента.
Если не сработало — переключаюсь на Playwright. Он поднимает настоящий браузер, ждёт пока JavaScript отработает, и только потом снимает данные. Медленнее, но берёт практически любую страницу.
Вот примерно как выглядит логика переключения:
async def fetch_price(url: str, selector: str) -> str | None:
# сначала пробуем быстрый способ
try:
response = requests.get(url, headers=HEADERS, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
element = soup.select_one(selector)
if element and element.text.strip():
return element.text.strip()
except Exception:
pass
# если не вышло — поднимаем браузер
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto(url, wait_until="networkidle")
element = await page.query_selector(selector)
if element:
return await element.inner_text()
await browser.close()
return None
От блокировок я закрылся ротацией user-agent и случайными паузами между запросами. Не идеально, но для мониторинга пяти-десяти конкурентов хватает. Промышленный масштаб — это уже разговор про прокси и специализированные сервисы.
Где живут данные и как агент замечает изменения
Все собранные цены идут в SQLite. Структура простая: товар, конкурент, цена, дата снятия. Каждый раз когда агент собирает новые данные, он сравнивает их с предыдущими значениями.
Долго думал, какой порог считать значимым изменением. Сначала поставил 5%. Агент немедленно начал писать мне оповещения каждые два часа — цены на одном из сайтов гуляли в диапазоне ±3–7% постоянно, явно динамическое ценообразование. Пришлось добавить сглаживание: смотреть не на разовый скачок, а на тренд за последние три замера.
Здесь и появляется роль языковой модели. Вместо того чтобы вручную писать правила для каждого случая, я отдаю ей срез данных и прошу интерпретировать:
def analyze_price_changes(history: list[dict]) -> str:
prompt = f"""
Вот история цен конкурента за последние 7 дней:
{json.dumps(history, ensure_ascii=False, indent=2)}
Определи: есть ли значимые изменения? Похоже ли это на акцию,
постоянное снижение или случайные колебания? Ответь кратко,
2-3 предложения, только факты.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
gpt-4o-mini стоит копейки на таких объёмах. И справляется лучше, чем мои самописные правила.
Оповещения и финальная сборка
Оповещения я вывел в Telegram — там уже был бот для других задач. Агент пишет только когда видит реальное изменение, не просто шум. Сообщение выглядит примерно так:
«Конкурент site.ru: цена на "Товар X" снизилась с 2 400 до 1 990 руб. за последние 2 дня. Похоже на акцию — другие позиции не изменились.»
Запуск повесил на cron — раз в четыре часа в рабочее время. Ночью агент не работает, незачем.
Весь проект вышел около 400 строк кода. Один вечер на первую версию, ещё пара вечеров на отладку граблей с JavaScript-сайтами.
Что получилось и что я бы сделал иначе
Агент работает третью неделю. За это время поймал два момента: один конкурент тихо поднял цены на 8% в пятницу вечером — без анонсов, просто молча, — и другой запустил акцию, которую я бы не заметил до понедельника.
С нуля я бы сразу заложил нормальную обработку ошибок. Первые дни агент иногда молчал, потому что сайт конкурента лежал, а скрипт просто падал без записи в лог. Теперь каждый неудачный запрос пишется в базу с причиной — так видна разница между «сайт недоступен» и «селектор поехал».
И ещё одна вещь: хранить CSS-селекторы в конфиге, не хардкодить в коде. Когда конкурент переверстал страницу, я потратил двадцать минут на поиск — где же этот чёртов селектор зарыт. Глупая ошибка, которую легко избежать.
Следующий шаг — научить агента самому находить нужный элемент на странице с помощью модели, без ручного прописывания селекторов. Технически это реально, уже видел несколько библиотек в этом направлении. Но это уже другая история.
