Когда Scrapy тесно, а Python медленный: собираем данные на Elixir с помощью Crawly

16 авг 2026

Репозиторий давно не обновлялся

Последнее обновление было 1 год назад.

1,116
125
17
1 год

Когда речь заходит о парсинге сайтов и сборе данных в промышленных масштабах, почти каждый разработчик сразу вспоминает Python и Scrapy. Это стандарт индустрии, проверенный годами. Но у Python есть особенность: когда нужно выкачивать сотни страниц параллельно, удерживать тысячи открытых соединений и сразу на лету чистить данные, приходится упираться в потоки, асинхронность и GIL.

В мире функционального программирования для таких задач идеально подходит виртуальная машина BEAM. Процессы в Erlang и Elixir изолированы, весят считанные килобайты и создаются миллионами. Фреймворк Crawly берет лучшую архитектурную концепцию из Scrapy (пауки, мидлвары, пайплайны обработки) и переносит ее на рельсы Elixir. Получается инструмент, который из коробки готов держать огромную нагрузку по сетевым запросам без сложного тюнинга асинхронного рантайма.

Что внутри и как это работает

Если вы хоть раз писали парсер на Scrapy, архитектура Crawly покажется родной. Вся работа делится на три понятные части: пауки (Spiders), промежуточные слои (Middlewares) и конвейеры обработки элементов (Pipelines).

Паук описывает начальные точки входа и логику извлечения данных со страниц.

Вот типичный пример паука, который ходит по каталогу книг, собирает названия с ценами и переходит по страницам пагинации:

Реклама
defmodule BooksToScrape do
  use Crawly.Spider

  @impl Crawly.Spider
  def base_url(), do: "https://books.toscrape.com/"

  @impl Crawly.Spider
  def init() do
    [start_urls: ["https://books.toscrape.com/"]]
  end

  @impl Crawly.Spider
  def parse_item(response) do
    {:ok, document} = Floki.parse_document(response.body)

    items =
      document
      |> Floki.find(".product_pod")
      |> Enum.map(fn x ->
        %{
          title: Floki.find(x, "h3 a") |> Floki.attribute("title") |> Floki.text(),
          price: Floki.find(x, ".product_price .price_color") |> Floki.text(),
          url: response.request_url
        }
      end)

    next_requests =
      document
      |> Floki.find(".next a")
      |> Floki.attribute("href")
      |> Enum.map(fn url ->
        Crawly.Utils.build_absolute_url(url, response.request.url)
        |> Crawly.Utils.request_from_url()
      end)

    %Crawly.ParsedItem{items: items, requests: next_requests}
  end
end

Код чистый и декларативный. Парсинг HTML делается через библиотеку Floki с привычными CSS-селекторами. Из функции parse_item мы возвращаем структуру, где лежат готовые данные и пачка новых запросов для планировщика.

Настройка пайплайнов и защита от блокировок

Парсер редко состоит только из HTTP-клиента и парсера HTML. Нужно следить за уникальностью URL, ограничивать число одновременных запросов к домену, менять заголовки, фильтровать дубликаты и валидировать схему перед сохранением.

В Crawly все эти вещи вынесены в конфигурацию:

import Config

config :crawly,
  closespider_timeout: 10,
  concurrent_requests_per_domain: 8,
  closespider_itemcount: 100,
  middlewares: [
    Crawly.Middlewares.DomainFilter,
    Crawly.Middlewares.UniqueRequest,
    {Crawly.Middlewares.UserAgent, user_agents: ["Crawly Bot"]}
  ],
  pipelines: [
    {Crawly.Pipelines.Validate, fields: [:url, :title, :price]},
    {Crawly.Pipelines.DuplicatesFilter, item_id: :title},
    Crawly.Pipelines.JSONEncoder,
    {Crawly.Pipelines.WriteToFile, extension: "jl", folder: "/tmp"}
  ]

Что здесь происходит? Запросы сначала проходят через цепочку мидлваров: фильтр чужих доменов не даст пауку случайно уйти сканировать весь интернет, а UniqueRequest отсечет повторные заходы на одни и те же страницы. Когда паук вытащил данные, они попадают в пайплайн. Там проверяются обязательные поля, отсеиваются дубликаты по заголовку, а валидный результат пакуется в JSON Lines и ложится на диск.

Генераторы экономят время при старте: команда mix crawly.gen.spider создаст заготовку паука со всеми нужными коллбеками, а mix crawly.gen.config подготовит дефолтный конфиг.

Встроенная панель управления

Интересная деталь: начиная с версии 0.15.0 в проект добавили веб-интерфейс управления прямо из коробки. Он доступен по адресу localhost:4001.

Через эту админку можно:

  • запускать и останавливать пауков вручную,
  • смотреть очередь запланированных запросов,
  • скачивать собранные элементы и просматривать логи выполнения,
  • отслеживать состояние краулера в реальном времени.

Crawly Management UI

Если вы встраиваете Crawly в существующее веб-приложение на Phoenix или Plug, админку не обязательно держать на отдельном порту. Ее можно просто пробросить в общий роутер через forward "/admin", Crawly.API.Router.

Рендеринг динамических страниц и запуск без Elixir

Современный веб перегружен JavaScript. Если контент подгружается асинхронно через AJAX или приложение собрано на React, обычный HTTP GET вернет пустой скелет страницы. Crawly умеет работать с внешними рендерерами вроде Chrome или Splash. Вы настраиваете headless-браузер, и фреймворк забирает уже отрендеренный DOM со всеми выполненными скриптами.

Еще одна неочевидная фича — автономный режим (Standalone). Если в вашей команде никто не пишет на Elixir, поднимать ради краулера полноценную кодовую базу не нужно. Crawly умеет работать в минималистичном Docker-контейнере, где правила сбора страниц описываются простыми YAML-файлами. Это редкий пример, когда эликсировский инструмент открыт для разработчиков из других стеков.

Практические сценарии

Где Crawly раскрывается лучше всего:

  1. Мониторинг цен в интернет-магазинах. Когда нужно регулярно обходить тысячи карточек товаров, проверять изменения остатков и скидок.
  2. Сбор обучающих выборок для ML. Парсинг статей, отзывов и форумов с сохранением в форматы jsonl.
  3. Агрегаторы объявлений. Сбор предложений по недвижимости или автомобилям с десятков региональных досок.
  4. Историческое архивирование контента. Быстрая выкачка блогов и документов с фильтрацией битых ссылок.

Кому проект пригодится

Если ваш основной язык Elixir или Erlang, Crawly — безальтернативно лучший выбор для веб-скрапинга. Вам не придется городить костыли на Python рядом с основным сервисом и настраивать межсервисное общение через очереди.

Если вы пишете на Python и устали бороться с производительностью Scrapy на больших объемах данных, на Crawly точно имеет смысл взглянуть. Порог входа здесь низкий: концепции совпадают практически один в один, а синтаксис Elixir после Python читается очень легко. Начать можно с официальной документации на HexDocs и быстрого туториала на тестовом сайте books.toscrape.com.

🍪 Мы используем файлы cookie и сервис аналитики Яндекс.Метрика, чтобы сайт работал лучше. Продолжая пользоваться devtrends.ru, вы соглашаетесь с обработкой данных согласно Политике конфиденциальности.