avito_parser/
|
├── core/ # Бизнес-логика: данные, кэш, парсинг полей
| ├── __init__.py
| ├── ad.py
| ├── cache.py
| ├── cache_config.py
| └── field_parsers.py
|
├── parser/ # Парсинг сайта Авито через браузер
| ├── __init__.py
| ├── parser.py
| ├── parser_config.py
| ├── slug_builder.py
| └── translit_pack.py
|
├── exporter/ # Экспорт данных в Excel
| ├── __init__.py
| ├── exporter.py
| └── exporter_config.py
|
├── tests/ # Тесты ключевых модулей
| ├── __init__.py
| ├── helpers.py
| ├── all_tests.py
| ├── fields_tests.py
| ├── slug_tests.py
| ├── cache_tests.py
| └── exporter_tests.py
|
└── main.py # Точка входа: CLI, оркестрация этапов
Используемая версия Python: 3.12.10
# Создать и активировать виртуальное окружение
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # Linux / macOS
# Установить зависимости
pip install -r requirements.txt
# Установить браузер Firefox для Playwright
playwright install firefox# Поиск по всей России
python main.py --query "котик"
# Поиск по городу
python main.py --query "котик" --city "Новосибирск"
# Несколько страниц результатов
python main.py --query "котик" --city "Новосибирск" --pages 2
# Ограничить количество объявлений
python main.py --query "котик" --city "Новосибирск" --pages 2 --limit 10| Аргумент | Обязательный | Описание |
|---|---|---|
--query |
да | Поисковый запрос на русском языке |
--city |
нет | Город на русском, без города - поиск по всей России |
--pages |
нет | Количество страниц поиска, по умолчанию 1 |
--limit |
нет | Максимальное количество объявлений, по умолчанию все |
После выполнения в корне проекта создаётся кэш cache.db и Excel-файл с именем вида query_city.xlsx.
python tests/all_tests.pyТесты не зависят от доступа к Авито и проверяют внутреннюю логику модулей.
Вспомогательные файлы сохраняются в C:\test\:
cache_test.db- созданная база данных для хранения кэшаexporter_test.xlsx- Excel-файл с данными, выгруженными из кэша
Папка содержит всё, что не зависит от способа получения данных и от формата вывода. Это чистая бизнес-логика: как выглядит объявление, как оно хранится и как обрабатываются его поля.
ad.py
Датакласс Ad описывает одно объявление. Все поля имеют значения по умолчанию, поэтому объект можно создать пустым и заполнять постепенно в процессе парсинга. Поля cached_at и updated_at имеют тип datetime и заполняются только при записи в кэш, а не при парсинге. Поле price хранится как Optional[float] - целая часть рубли, дробная копейки. Значение None означает, что цена не указана, 0.0 - бесплатно. Поле published_on хранится как Optional[date] без привязки к временной зоне.
field_parsers.py
Авито отдаёт цену и дату публикации в виде строк, причём в нестандартных форматах: «1 500 ₽», «Бесплатно», «вчера в 14:32», «2 дня назад». Этот модуль отвечает за преобразование таких строк в типизированные значения Python.
parse_price() использует регулярное выражение для извлечения числа из строки, убирает пробелы-разделители тысяч и заменяет запятую на точку для корректного преобразования в float. Специальные случаи («Бесплатно», «Договорная») обрабатываются отдельно.
parse_date() разбирает строку по нескольким паттернам последовательно: сначала проверяет относительные форматы («сегодня», «вчера», «N дней назад»), затем пробует распознать точную дату («15 марта», «15 марта 2024»). Если ни один паттерн не подошёл - возвращает None.
cache.py
SQLite-кэш с TTL-логикой (время жизни записи).
При сохранении пакета объявлений модуль делает один батчевый SELECT для всех ID сразу, а не по одному запросу на каждое объявление - это ускоряет работу при большом количестве объявлений.
Логика обновления записи:
- Если объявление новое и активное - добавляем. Новые закрытые пропускаем.
- Если объявление уже есть в кэше и
cached_atменьше TTL (по умолчанию 1 час) - считаем данные свежими и пропускаем без проверки. - Если TTL истёк - сравниваем ключевые поля (
status,price,title,description). При наличии изменений обновляем запись и проставляемupdated_at = сейчас. Если изменений нет - не трогаем.
Даты хранятся в SQLite как строки формата ISO 8601 (2026-03-28, 2026-03-28T12:34:56), поскольку SQLite не имеет типа DATE/DATETIME. При чтении строки автоматически преобразуются обратно в date и datetime через вспомогательные функции _str_to_date() и _str_to_dt().
cache_config.py
Выделен в отдельный файл, чтобы путь к БД и значение TTL можно было менять в одном месте без правки логики кэша. По умолчанию БД создаётся в корне проекта.
Папка содержит всё, что связано с получением данных с сайта Авито. Остальные модули не знают о Playwright и не зависят от способа получения данных.
parser.py
Реализует двухэтапный асинхронный парсинг через Playwright (браузерная автоматизация).
Этап 1 - сбор карточек с выдачи. Открывает страницу поиска и считывает базовые данные со всех карточек: id, url, title. Это быстро, так как все карточки на одной странице.
Этап 2 - обогащение. Параллельно заходит на страницу каждого объявления и считывает оставшиеся поля: price, address, description, published_on, views, status. Параллельность ограничена семафором asyncio.Semaphore(PARALLEL_TABS) - одновременно открывается не более PARALLEL_TABS вкладок, чтобы не создавать подозрительную нагрузку с одного IP. При считывании адреса происходит дополнительный клик на кнопку "Узнать подробности", откуда считывается весь текст с карточки указанной точки на карте. Если его не удалось спарсить, то считывается поле общего адреса, указанного на странице.
Каждая вкладка закрывается сразу после парсинга. Между запросами добавляются случайные задержки из parser_config.py. Публичная функция parse() остаётся синхронной - asyncio.run() внутри, снаружи ничего не меняется.
Определение slug города (city_to_slug) вызывается синхронно до запуска asyncio.run() - это важно, потому что slug_builder делает HTTP-запрос через httpx, и вызов внутри event loop заблокировал бы его.
parser_config.py
Все настройки парсера в одном файле. Пул из 5 User-Agent строк реальных браузеров выбирается случайно при каждом запуске. HTTP-заголовки имитируют те, что отправляет настоящий браузер Firefox. Базовые куки Авито устанавливаются до первого запроса. Задержки заданы как кортежи (min, max) и передаются в random.uniform().
Изначально использовался браузер Chromium и User-Agent разных браузеров, что приводило к несоответствию цифровых отпечатков,и как следствие открывалась главная страница, а не результат поискового запроса. Изменение на Firefox позволило открыть корректные страницы, однако при больших запросах IP всё равно блокируется.
slug_builder.py
Преобразует название города на русском в URL-slug, который Авито использует в адресах страниц: «Нижний Новгород» -> nizhniy_novgorod.
Алгоритм:
- Транслитерация через кастомный пак
_AvitoPack - Нормализация: нижний регистр, пробелы ->
_, удаление недопустимых символов - HTTP-запрос к
avito.ru/{slug}для проверки существования страницы. Авито выдаёт код 200 даже для страницы, которая не найдена, поэтому если на открывшейся странице есть фраза "такой страницы не существует" - пробует вариант с дефисами, поскольку пользователь может ввести составное название неправильно (например,санкт петербург->sankt_peterburg->sankt-peterburg).
translit_pack.py
Стандартный пак транслитерации ru из библиотеки transliterate ориентирован на ГОСТ стандарт и даёт неверные результаты для slug-ов Авито: например, й -> j, я -> ja', твёрдый и мягкий знаки в виде кавычек и апострофов.
Кастомный пак _AvitoPack переопределяет pre_processor_mapping с правилами специфичными для Авито: й -> y, ш -> sh, щ -> sch, ё -> e, ъ/ь -> ''. Правила применяются до основного маппинга, многобуквенные сочетания перечислены выше однобуквенных, чтобы не было конфликтов.
Папка содержит файлы, необходимые для формирования Excel-файла. Не зависит от источника данных - на вход принимает список объектов Ad.
exporter.py
Формирует .xlsx файл через openpyxl. Список столбцов берётся из exporter_config.py, поэтому добавить или убрать столбец можно без правки логики экспортера. Значения для каждой ячейки формирует функция _get_value(), которая обрабатывает специальные типы: datetime форматируется как «28.03.2026 12:34:56», date как «28.03.2026», статус 1/0 в «Активно»/«Закрыто». Первая строка закрепляется через freeze_panes.
exporter_config.py
Все настройки оформления: шрифты, цвета заливки для активных и закрытых объявлений, стиль границ, высота шапки. Список столбцов задан как список кортежей (заголовок, поле_Ad, ширина) - это позволяет менять порядок, добавлять и удалять столбцы без правки exporter.py.
По каждому объявлению собираются следующие данные:
| Поле | Тип | Описание |
|---|---|---|
| ID | str | Уникальный номер объявления на Авито |
| Название | str | Заголовок объявления |
| Цена | float / None | В рублях. 0.0 - бесплатно, None - не указана |
| Адрес | str | Адрес из объявления |
| Описание | str | Текст описания |
| Дата публикации | date / None | Дата без временной зоны |
| Просмотры | int | Количество просмотров |
| Статус | int | 1 - активно, 0 - закрыто |
| Город | str | Город из входного параметра |
| Запрос | str | Поисковый запрос из входного параметра |
| Ссылка | str | Прямая ссылка на объявление |
| Добавлен в кэш | datetime | Дата и время первого добавления в БД |
| Обновлён в кэше | datetime | Дата и время последнего обновления в БД |
Данные сохраняются в двух местах:
- SQLite БД (
cache.db) - Excel файл (
.xlsx)
Блокировка по IP
Авито блокирует автоматические запросы на уровне инфраструктуры. При попытке спарсить большое количество данных возникает одна из двух ситуаций: либо сайт отдаёт стартовую страницу без объявлений, либо отображает страницу с сообщением "Доступ ограничен: проблема с IP" и предлагает решить капчу. Реализация задержек, имитации скроллинга, отсутствие параллельных запросов, использование User-Agents и cookies помогают при небольшом значении --limit. Использование прокси-серверов не рассматривалось как основное решение из-за дополнительных затрат и отсутствия гарантии, что IP-адреса останутся не заблокированными.
Отсутствие официального API
Авито не предоставляет публичный API для массового сбора данных. Официальный API существует, но доступен только для бизнес-партнёров по заявке.
Хрупкость селекторов
CSS-классы и data-marker атрибуты на сайте могут меняться без предупреждения при обновлении фронтенда. При поломке парсера в первую очередь нужно проверить актуальность селекторов в parser/parser.py.
Скорость
Для парсинга объявления скрипт переходит в объявление + кликает на подробности местоположения + находится в ожидании для имитации человеческого поведения + возвращается на страницу поискового запроса + ожидает на странице поиска. Можно эмпирическим путём подобрать оптимальное время задержек, но при однопоточном запуске процесс парсинга всё равно остаётся достаточно медленным даже на небольших данных.
Даже при использовании готовых решений проблема блокировки полностью не устраняется.
Например, в открытом репозитории parser_avito реализован парсер с применением различных техник обхода ограничений. Однако даже это решение реализует сбор данных только для первой страницы результатов поиска.