RUEN
Обсудить проект

Инженерный кейс

Цены объявлений в долларах

Небольшой личный инструмент: расширение дорисовывает рядом с рублёвой ценой сумму в долларах по курсу Нацбанка, а бот следит за новыми объявлениями по сохранённым шаблонам поиска.

зависимостей у расширения
0
способа найти цену
2
прокси на запрос
3

Две независимые вещи вокруг одной автомобильной площадки. Расширение дорисовывает рядом с рублёвой ценой сумму в долларах по официальному курсу — после того как площадка убрала долларовые цены сама. Бот следит за новыми объявлениями по сохранённым шаблонам поиска и присылает карточку с фото и характеристиками.

Цену на странице ищут двумя независимыми способами

Первый обходит текстовые узлы, находит рублёвый знак и поднимается вверх до элемента, чей текст целиком «число и валюта» — но не выше четырёх предков. Второй берёт элементы с классами вроде price или cost, где текст — голое число.

Очевидное решение, один селектор по классу карточки, живёт до ближайшего релиза площадки: классы там генерируются сборкой. А расширение обновляется через проверку в магазине — починка доезжает до людей днями. Два детектора не дают гарантии, но ломаются по разным причинам: первый переживает полную смену классов, второй — случай, когда валюты в тексте вообще нет.

Ограничение «не выше четырёх предков» тоже не украшение: без него подъём доходит до контейнера всей карточки, и цена прилипает к случайному месту.

Устаревший курс лучше ошибки

Если обновить курс не удалось, отдаётся старое значение, и только при полном отсутствии кэша возвращается ошибка.

«Не смог обновить — верни ошибку» выглядит честнее, но делает расширение неработающим при любой заминке сети: бейджи пропадают со всех цен разом. При этом курс Нацбанка меняется раз в сутки по рабочим дням, и вчерашнее значение отличается на доли процента. Чтобы это не превратилось в тихий обман, дата курса показывается в подсказке у каждого бейджа и в окне расширения, вместе со временем последнего обновления.

Страница умеет достать курс сама, и это не дублирование

Обычно курс тянет фоновый процесс и кладёт в общее хранилище. Но если ответа нет и кэша нет, тот же запрос делает сама страница.

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

Объявления берутся из данных страницы, а не через браузер в фоне

Бот делает обычный запрос и достаёт из разметки готовый объект состояния, из которого площадка сама рисует список.

Очевидных вариантов два, и оба хуже. Разбирать HTML селекторами — ломаться на каждом релизе и потом превращать «от 1 250 р.» обратно в число. Поднять headless-браузер — получить сотни мегабайт памяти на запуск, при том что у сервиса стоит ограничение в 600 МБ на весь процесс, а живёт он на дешёвом сервере. Готовый объект состояния уже типизирован: числа числами, характеристики полями. Цена размена названа честно — привязка к внутренней структуре, которую никто не обещал не менять.

Первая проверка шаблона молчит

Сразу после создания шаблона бот забирает одну страницу, помечает всё увиденным и выходит, ничего не прислав. Уведомления начинаются со следующего раза.

Иначе шаблон «до тридцати тысяч» умирает в момент создания: человек получает десяток карточек с фотографиями немедленно и ещё сообщение «и ещё три тысячи». Инструмент нужен для другого — показать то, чего не было минуту назад.

Листание прекращается по двум условиям, и второе — про лимит отправки

Следующая страница берётся, только если в хвосте нет ни одного знакомого объявления и свежих набралось меньше двойного лимита отправки.

Первое условие очевидно. Одного его мало: если шаблон широкий или бот стоял сутки, знакомых не окажется ни на одной странице, и листание пойдёт до упора — четыре запроса к площадке, которая за это отвечает своим ограничением, ради данных, которые всё равно не отправятся. Запас двойной, а не ровный, потому что часть добытого отсеется как платное поднятие.

Платные поднятия отсеиваются, иначе шаблон превращается в спам старого

Объявления с пометкой платного поднятия не отправляются никогда.

Дедупликации по идентификатору кажется достаточно, но поднятие выталкивает объявление в начало списка заново. Без фильтра бот видит «в начале появилось то, чего там не было» и присылает месячное объявление как новинку — и так при каждой новой оплате.

Неотправленная карточка не помечается виденной

Отправка идёт с тремя попытками и разными запасными путями: при ограничении со стороны мессенджера — пауза ровно на столько, сколько он попросил; при ошибке фотографии — то же самое текстом; при сломанной разметке — без разметки. Если все попытки упали, обход прерывается, и объявление виденным не помечается.

Порядок здесь обратный привычному: сначала подтверждённая отправка, потом отметка. Поэтому неотправленное само вернётся в следующий раз. И запасные пути разные, потому что причины разные: ограничение лечится ожиданием, битая ссылка на фото — только отказом от фото, сломанная разметка — только отказом от разметки.

Пул прокси с растущим остыванием — и пароль не попадает в журнал

На запрос выдаётся до трёх адресов по кругу, упавший уходит в остывание на пять, десять или пятнадцать минут по числу ошибок подряд, успех сбрасывает счётчик. Если в бане все — всё равно пробуются. У каждой записи есть короткая метка вида «схема, узел, порт» — без пароля, чтобы её можно было писать в журнал.

Каждое решение здесь закрывает свою ловушку. Фиксированная пауза заставляет стабильно мёртвый адрес съедать попытку каждые пять минут вечно. Без сброса счётчика один сетевой сбой за месяц навсегда записывает рабочий адрес в подозрительные. Пустой список кандидатов — это гарантированный отказ, а забаненный пять минут назад мог уже ожить: попытка дешевле отказа.

Manifest V3 · JavaScript · Node.js 18 · Telegraf · SQLite · undici · systemd