Avito
CLI avito читает Avito через браузер пользователя. Десять команд, все read-only.
Команда печатает один JSON-объект в stdout. Отказ печатается в stderr и в exit code.
Вызывай только CLI. Не открывай страницы сам. Не собирай URL вручную: URL объявления
и searchUrl бери из ответа команды.
Команды
| Команда | Вход | Ответ |
|---|---|---|
search <query> |
текст запроса | первую страницу выдачи и searchUrl. Гео задаётся здесь: --location-id, --metro, --district, --coords, --radius |
get-page <searchUrl> --page <n> |
searchUrl |
другую страницу той же выдачи |
get-filters <searchUrl> |
searchUrl |
все фильтры этой выдачи, их значения и что уже применено |
apply-filters <searchUrl> --set <k=v;k=v> |
searchUrl |
суженную выдачу и новый searchUrl |
get-categories <searchUrl> |
searchUrl |
дерево категорий и куда можно перейти |
move-category <searchUrl> --to <name> |
searchUrl |
ту же выдачу в другой категории |
get-item <url> |
URL объявления | полный текст, attributes, прайс-лист, фотографии файлами |
get-seller-reviews <itemUrl> |
URL объявления | ленту отзывов продавца |
get-location <query> |
название города или региона | locationId для search, ID метро и районов |
get-coords <address> |
адрес | координаты для --coords |
searchUrl — единственный носитель состояния. Бери его из ответа предыдущей команды
и передавай в следующую.
Флоу
Подразумевай, что пользователь ждёт не список выдачи, а несколько оптимальных товаров, которые ты выбрал и проверил сам. Выдача — материал для отбора, а не ответ.
Шаги 1–3 обязательны и выполняются по порядку.
1. Браузер. Выполни avito browser.
- Endpoint доступен — иди дальше.
- Кандидаты перечислены, ни один не запомнен — покажи список пользователю, спроси,
каким браузером читать Avito, выполни
avito browser use --profile <dir>. - Список пуст — попроси пользователя включить отладку на
chrome://inspect/#remote-debuggingв том браузере, которым он пользуется. Повториavito browser.
Не запускай браузер сам. Не указывай на новый профиль: Avito отказывает профилю без истории. Первая команда сессии просит у пользователя одобрение соединения — один раз на сессию.
2. --help. Перед первым вызовом каждой команды в сессии выполни
avito <command> --help. Он печатает точный тип ответа и все аргументы. Не угадывай
имена полей и флагов.
3. Каталог и файлы. Создай свой временный каталог: mkdir -p <tmp>. Ниже
<tmp> — он.
Следующие ответы всегда пиши в отдельные файлы, а решения принимай по jq из них:
| Команды | Почему файл обязателен |
|---|---|
search, get-page, apply-filters, move-category |
до 50 карточек; тот же ответ читается несколькими срезами |
get-filters |
ключи, значения и зависимости фильтров нужны на следующих шагах |
get-categories |
дерево нужно сравнить с текущей категорией перед переходом |
Один вызов — один файл. Не повторяй вызов ради другого формата: снова прочитай файл.
Не применяй head к JSON. При отказе файл пустой, а сообщение уходит в stderr;
проверяй exit code. get-item, get-seller-reviews, get-location и get-coords
можно читать из stdout.
4. Настройка выдачи. Пройди этот цикл до отбора карточек.
4.1. Гео и текст запроса. Сначала задай гео, если оно нужно:
| Условие | Начало цепочки |
|---|---|
| город не назван | search <query> |
| назван город | get-location <город> → search <query> --location-id <id> |
| у метро или в районе | get-location <город> --geo metro → search <query> --metro <ids> |
| рядом с адресом | get-coords <адрес> → search <query> --coords <lat,lon> --radius <км> |
Для каждого ограничения пользователя реши, где продавец вероятнее его указал:
- конкретное название, модель или свойство, которое обычно пишут словами в заголовке
или тексте карточки, оставь в
query; - диапазон или несколько допустимых значений одного свойства не перечисляй серией
запросов: оставь в
queryобщий предмет покупки, но не шире нужной категории, а варианты задай фильтром; - если не уверен, сделай один разумный широкий запрос и сначала проверь фильтры и
категории. Не запускай соседние варианты
search, пока не прошёл пункты 4.2–4.4.
Первый ответ сохрани и сразу проверь минимальный конверт:
avito search "ddr4 32gb" --location-id 637640 > <tmp>/search.json
jq '{query, category, searchUrl, itemsCount, medianPrice}' <tmp>/search.json
4.2. Фильтры обязательны. После search и после каждого move-category всегда
вызывай get-filters на новом searchUrl, даже если ожидаешь отбирать только по
словам. Сохрани ответ и найди фильтры по названиям и подписям значений:
avito get-filters "<searchUrl>" > <tmp>/filters-1.json
jq --arg words 'Тип накопителя|Форм-фактор|Объ[её]м' '
{searchUrl, filters: [.filters[]
| select((.name + " " + ([.options[]] | join(" "))) | test($words; "i"))
| {key, name, unit, valueSyntax, currentValue,
changesFiltersOnSelect, options}]}' <tmp>/filters-1.json
В проекции обязательны key, name, valueSyntax, currentValue,
changesFiltersOnSelect и options: без них нельзя ни составить --set, ни понять,
что словарь ещё может перестроиться. unit нужен для диапазонов. Ключи и значения
копируй дословно; подписи из options нельзя передавать вместо их ключей.
- Нужные ключи и значения есть → запомни полный набор отборочных значений и иди к пункту 4.5.
- Нужного ключа или значения нет → проверь зависимость формы в пункте 4.3.
4.3. Может ли нужный фильтр появиться? Покажи все фильтры, которые перестраивают форму:
jq '{filters: [.filters[] | select(.changesFiltersOnSelect)
| {key, name, unit, valueSyntax, currentValue,
changesFiltersOnSelect, options}]}' <tmp>/filters-1.json
Если один из них по смыслу является родителем недостающего свойства, примени его
точное значение, сохрани ответ, затем заново сохрани get-filters от нового
searchUrl и повтори проекцию из 4.2. Например, выбор «Типа накопителя» может
показать «Форм-фактор», причём с разными options для SSD и HDD. Не перебирай
несвязанные значения вслепую.
avito apply-filters "<searchUrl>" \
--set 'params[родитель]=<value>' > <tmp>/after-parent.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/after-parent.json
avito get-filters "<newSearchUrl>" > <tmp>/filters-2.json
Передавай в следующий apply-filters последний searchUrl: фильтры с другими
ключами, уже применённые к этому URL, сохранятся. Повторный key=... заменяет только
всё значение того же ключа, а не дополняет его. Поэтому несколько допустимых
значений одного фильтра передавай сразу через запятую (key=a,b), а для снятия
фильтра используй key=. Если ни один changesFiltersOnSelect: true не может по
смыслу открыть недостающее свойство, иди к пункту 4.4.
4.4. Категория. Если фильтров недостаточно или они не соответствуют предмету, проверь, куда Avito поместил запрос и есть ли подходящая более узкая категория:
avito get-categories "<searchUrl>" > <tmp>/categories.json
jq '{query, searchUrl, categories: [.categories[] |
{role, name, depth, parent, current, hasChildren, navigable,
preservesQuery}]}' <tmp>/categories.json
- Есть правильная более узкая или соседняя категория с
navigable: trueиpreservesQueryнеfalse→ перейди по её точномуname, сохрани результат и проверь конверт. Затем вернись к 4.2: фильтры прежней категории недействительны. - Текущая категория правильная и полезнее перейти некуда → отсутствие фильтра
окончательно. Оставь это ограничение для проверки карточек и
get-item, затем иди к 4.5.
avito move-category "<searchUrl>" --to '<name>' > <tmp>/moved.json
jq '{query, category, searchUrl, itemsCount, medianPrice}' <tmp>/moved.json
4.5. Зафиксируй выдачу. Примени все найденные ограничения одним вызовом без
сортировки и сохрани его ответ как <defaultFile>. Если применять нечего,
<defaultFile> — уже сохранённый последний ответ search или move-category.
Сохрани его searchUrl: от него строятся сортировки в пункте 5. Затем ещё раз
сохрани get-filters от этого окончательного URL как filters-final.json: это
актуальный снимок формы и применённых значений. Категория и фильтры должны быть
определены до страниц: apply-filters и move-category принимают только страницу 1
и отказывают URL с p=<n>.
avito apply-filters "<searchUrl>" \
--set '<key=value;key=value>' > <tmp>/default.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/default.json
avito get-filters "<defaultSearchUrl>" > <tmp>/filters-final.json
5. Отбор. Кандидатов выбираешь ты, а не пользователь из списка. Сначала найди
нужную подпись сортировки в последнем файле get-filters и возьми соответствующий
ключ значения из options; саму подпись в --set не передавай:
jq '.filters[] | select(.key == "sort") |
{key, name, valueSyntax, currentValue, changesFiltersOnSelect, options}' \
<tmp>/filters-final.json
- Пользователь не задал порядок. Сначала прочитай первые 25 карточек выдачи по
умолчанию. Затем примени к её
searchUrlтолько точное значение сортировки «по дате»: остальные фильтры уже несёт URL. Сохрани второй файл и прочитай его первые - Пользователь просил дешевле. Примени точное значение сортировки по возрастанию цены и в том же вызове нижнюю границу цены: разумная минимальная цена товара × 0.8.
- Пользователь задал другой порядок. Возьми соответствующее значение из
sort.options, примени к текущемуsearchUrlи начни с первых 25.
avito apply-filters "<defaultSearchUrl>" \
--set 'sort=<значение по дате>' > <tmp>/by-date.json
jq '{query, searchUrl, itemsCount, medianPrice}' <tmp>/by-date.json
jq '.items[:25] | map({title, price, minPrice, hasPriceList, published,
location, url, descriptionPreview})' <defaultFile>
jq '.items[:25] | map({title, price, minPrice, hasPriceList, published,
location, url, descriptionPreview})' <tmp>/by-date.json
Выбери интересные или непонятные карточки, убери дубли по url между сортировками и
открой каждую через get-item. Если подходящих вариантов мало, только тогда прочитай
items[25:50] из каждого уже сохранённого файла тем же набором полей. Лишь если и
эти четыре среза не дали достаточно кандидатов, используй get-page на нужном
финальном searchUrl, сохрани ответ в новый файл, проверь
{query, page, searchUrl, itemsCount, medianPrice} и продолжи тот же отбор.
Особенности CLI
- Карточка — не объявление.
descriptionPreviewобрезан самим Avito,imageCount— число фотографий, а не фотографии. Полный текст и таблица характеристикattributesесть только вget-item. queryиsearchUrlговорят, что ты получил. Avito канонизирует запрос в маршрут категории.query: nullзначит, что текст растворился в категории и совпадение по словам не гарантировано.categoryв ответеsearch— категория, в которую Avito отнёс запрос; две выдачи из разных категорий несравнимы.price— то число, которое Avito напечатал на карточке, вместе со скидкой или бонусом, если он их дал.minPriceвместоpriceзначит «от …» — это нижняя граница, а не цена.hasPriceList: trueзначит таблицу цен; она приходит какpriceListвget-item. Обаnull— цена договорная.price: 0— бесплатно.published— точный момент, ISO 8601, UTC.publishedTextвget-item— строка Avito без года и секунд.- Фотографии. Ни одна команда не возвращает фотографию в JSON.
get-item <url> --images-dir <абсолютный существующий каталог>создаёт<dir>/<itemId>/и пишет туда01.jpg,02.jpg… в порядке галереи, а пути кладёт вimages. Сначала прочитай текст, потом скачивай фотографии одного-двух кандидатов. Если хотя бы один файл не скачался, падает весь вызов; повтори команду без--images-dir. - Фильтры. Ключи и значения бери из
get-filtersдословно;valueSyntaxговорит, что писать послеkey=.;разделяет фильтры,,— значения одного фильтра,..— диапазон, пустое значение очищает фильтр. Новый ключ добавляется к фильтрам во входномsearchUrl; повтор того же ключа заменяет всё его значение. Послеmove-categoryперечитайget-filters— ключи прежней категории недействительны. changesFiltersOnSelect: true— этот фильтр перестраивает форму: после его применения набор фильтров другой. Пример: на жёстких дисках «Форм-фактора» нет, пока не выбран «Тип накопителя», и словарь у него разный для SSD и HDD. Если применил такой фильтр и нужного ключа не хватало — вызовиget-filtersна новомsearchUrlещё раз, там он и появится. Список может и сократиться, так что прежние ключи тоже перечитывай.- Между запросами держится пауза. Случайная, от 1 до 2,5 секунд, и она общая для всех команд: интервал считается от последнего запроса всей машины, а не текущего вызова. Поэтому команда может начаться с ожидания, а цепочка из нескольких команд идёт небыстро. Это не зависание — не прерывай команду и не запускай команды параллельно, чтобы ускориться: параллельные вызовы встают в ту же очередь.
--remove-reservedубирает зарезервированные объявления и действует на один вызов. Передавай его каждой команде цепочки. Страница приходит короче, и что именно убрано, не сообщается.
Особенности Avito
Регион задаёт search --location-id. Состав выдачи меняют два фильтра, оба
применяются через apply-filters: localPriority («Сначала ближайшие», значение 1)
и d (Авито Доставка).
localPriority |
d |
Что в выдаче |
|---|---|---|
| не задан | не задан | объявления региона запроса и объявления других регионов с Авито Доставкой |
1 |
не задан | только объявления региона запроса |
| не задан | задан | только объявления с Авито Доставкой, из всех регионов; регион запроса не ограничивает |
1 |
задан | только объявления региона запроса, у которых есть Авито Доставка |
Отсюда следует: по умолчанию выдача не ограничена одним городом. locationName в
конверте называет регион запроса, а не регион каждого объявления. Если пользователю
нужен самовывоз в своём городе, применяй localPriority=1.
Правила покупки
- Ходовой товар, висящий давно, — ред флаг. Считай возраст по
published. Ликвидный товар по рыночной цене уходит за дни. Возраст в недели значит, что объявление смотрели и не взяли: дефект, завышенная цена или продавец не отвечает. Скажи об этом пользователю. - Покупка нового товара — читай отзывы. Прежде чем рекомендовать объявление,
выполни
get-seller-reviews. Смотриscore,stageи текст, а не только среднее. Смотри много ли отзывов на похожие товары или они накручены продажой мелочи. - Частный продавец продает новую вещь как БУ. Обычно люди не продают только что выпущенные товары без причин - ищи в полном тексте причину продажи
- Цена сильно ниже прочих — причина открыть
get-item, а не вывод. Ищи в полном тексте «нюанс», «на запчасти», и т.д. - Сравнивай сравнимое. Не сравнивай цены между категориями и не сравнивай
объявления с
hasPriceList: trueпо числу на карточке.
Отказы
Отказ печатается в stderr как CODE: message, иногда со строкой hint:. Машиночитаемая
часть — exit code.
| Exit | Code | Что произошло | Что делать |
|---|---|---|---|
| 2 | ARGUMENT |
аргумент не принят, запроса в сеть не было | исправь аргумент; сообщение называет ограничение |
| 66 | EMPTY_RESULT |
запрос выполнен, возвращать нечего | это ответ; сообщение называет вид пустоты |
| 1 | COMMAND_EXEC |
ответ пришёл, но ему нельзя верить: HTTP-отказ, изменившаяся форма ответа, невыполненное постусловие | остановись и сообщи. Не повторяй |
| 75 | TIMEOUT |
ответа не дождались | сообщи |
| 77 | ACCESS |
Avito ответил не данными: лимит запросов, страница проверки, страница без состояния | передай пользователю |
77 не чинится повтором. Не взаимодействуй с CAPTCHA. Не ищи обходной маршрут к тем же данным. Попроси пользователя открыть www.avito.ru в том же браузере и посмотреть, что тот требует.
could not reach the browser: … значит, что CLI не дошёл до сайта. Выполни
avito session status и передай результат пользователю: закрытый браузер, выключенная
отладка или неодобренное соединение — это его действие, а не повод повторять команду.