Парсер видео с каналов YouTube.
Стек:
- Python 3.11.
- Управление браузером: Playwright+Chromium (асинхронный api).
- Хранилище: PostgreSQL (asyncpg).
- Брокер очередей: RabbitMQ (aio-pika).
- Запуск: Docker, Docker-compose.
- asyncio, pydantic-settings, msgspec.
- Управление зависимостями: Poetry.
- Линтер, форматтер: ruff.
Архитектурно система делится на два компонента: Crawler и Loader.
Crawler (Сборщик) асинхронно, в разных вкладках парсит данные и записывает в Rabbit.
Loader (Загрузчик) читает распаршенные данные из очереди и пишет в PostgreSQL.
Чтение и запись поддерживается в любом количестве вкладок (параметр BROWSER_MAX_WORKERS). Он же, но BROWSER_MAX_WORKERS+1 отвечает за максимальное количество соединений в пуле PostgreSQL.
Сразу после установки нужно разово запустить:
cp .env.template .env
Это установит переменные окружения для локальных запусков + поверх них докер будет писать свои.
Playwright поддерживает работу в Docker ТОЛЬКО в Headless режиме. Поэтому для локального использования нагляднее будет взять локальный вариант запуска.
В корне проекта запустить
make local_clean
К сожалению, он далеко не у всех просто ставится. Мне пришлось поставить ещё и в систему, а не только в окружение. Могу лишь дать ссылку на офф. доку :(
В корне проекта запустить
make up_clean
Прописал в Makefile несколько команд для работы с полученными образами.
Команда | Описание |
---|---|
make down |
удаляет ВСЕ образы и volume |
make up_loader |
запускает Loader, загружающий video_id в PostgreSQL |
make up_crawler |
запуск Crawler в Докере |
make up |
запускает Crawler и Loader в Докере, БД остаётся та же |
make up_clean |
запускает Crawler и Loader в Докере, предварительно сносит БД |
make local |
запускает Crawler локально и Loader в Докере, БД остаётся та же |
make local_clean |
запускает Crawler локально и Loader в Докере, предварительно сносит БД |
Большинство команд - чистый Docker Compose. В Makefile есть ещё комментарии.
Список ссылок можно посмотреть в write_links.py. С причинами отсутствия гибкой конфигурации списка ссылок можно ознакомиться там же.
Базовые DDL-операции по созданию структуры происходит через монтирование init.sql в Docker Compose. От себя добавил поля created_at (время создания записи) и updated_at (время обновление записи). Они проставляются автоматически.
Обновление updated_at происходит через триггер. Создаётся также в init.sql. Запись новых данных осуществляется с помощью создания временной таблицы и её MERGE с основной.
Playwright иногда присылает неполный список видео. Это не было частью задания, но я немного поработал над этим.
Дополнительный таймаут после загрузки BROWSER_PAGE_ADDITIONAL_LOADING_TIME_MS
позволяет получить больше данных.
Кроме того, иногда происходят запросы на гугл-сервисы, из-за чего вкладка бесконечно грузится и вылетает по таймауту.
Частично решить проблему удалось с помощью их блокировки. BROWSER_REQUEST_ABORTION_CODE
- код, имитируемый от них, можно брать из доки.
Иногда переходы по ссылке всё-таки отлетают по таймауту. Страница в таких случаях уже получена, поэтому просто мьючу ошибку и обрабатываю страницу.
Самой медленной частью системы является Playwright и получение данных из сети. Для решения проблемы, я использовал официальный асинхронный модуль Playwright.
При запуске всё работает так:
- Создаётся один браузер.
- В рамках одной TaskGroup создаются воркеры-таски в количестве
BROWSER_MAX_WORKERS
- У каждого из воркеров есть своя вкладка в браузере и доступ к: входной очереди (ссылки на каналы) и выходной очереди (списки ссылок на видео с канала).
- Воркеры работают бесконечно, вычитывая одну очередь и перекладывая в другую.
Таким образом всё выполняется в рамках одного потока и процесса. И без простоя, который был бы при полной параллельности.
Для каждого канала Сборщиком в Кролика передаётся только список id видео (не включая "v="), а в хедерах username канала, начиная с @
.
Загрузчик при прочтении самостоятельно собирает ссылки на видео.
Это уменьшает объём сообщений, которые нужно сериализовать, отправить, принять, десериализовать.