Skip to content

kravchuuuchka/parser_avito

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Avito Parser

Дерево каталогов

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

Аргументы CLI

Аргумент Обязательный Описание
--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-файл с данными, выгруженными из кэша

Описание модулей

core/

Папка содержит всё, что не зависит от способа получения данных и от формата вывода. Это чистая бизнес-логика: как выглядит объявление, как оно хранится и как обрабатываются его поля.

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 можно было менять в одном месте без правки логики кэша. По умолчанию БД создаётся в корне проекта.


parser/

Папка содержит всё, что связано с получением данных с сайта Авито. Остальные модули не знают о 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.

Алгоритм:

  1. Транслитерация через кастомный пак _AvitoPack
  2. Нормализация: нижний регистр, пробелы -> _, удаление недопустимых символов
  3. 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, ъ/ь -> ''. Правила применяются до основного маппинга, многобуквенные сочетания перечислены выше однобуквенных, чтобы не было конфликтов.


exporter/

Папка содержит файлы, необходимые для формирования 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 реализован парсер с применением различных техник обхода ограничений. Однако даже это решение реализует сбор данных только для первой страницы результатов поиска.

About

Тестовое задание

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages