Quand Scrapy Devient Étroit et Python Lent : Collecter des Données avec Elixir et Crawly
En matière d'analyse de sites web et de collecte de données à l'échelle industrielle, presque chaque développeur pense immédiatement à Python et Scrapy. C'est une norme de l'industrie, prouvée au fil des ans. Mais Python a une particularité : quand vous avez besoin de télécharger des centaines de pages en parallèle, de maintenir des milliers de connexions ouvertes et de nettoyer les données à la volée, vous finissez par heurter les limites des threads, de l'async et du GIL.
Dans le monde de la programmation fonctionnelle, la machine virtuelle BEAM est idéale pour de telles tâches. Les processus dans Erlang et Elixir sont isolés, pèsent quelques kilobytes seulement et peuvent être générés en millions. Le framework Crawly reprend les meilleurs concepts architecturaux de Scrapy (spiders, middlewares, pipelines de traitement) et les transpose dans Elixir. Le résultat est un outil prêt à l'emploi pour gérer des charges massives de requêtes réseau sans configuration complexe de runtime async.
Ce qu'il y a à l'intérieur et comment ça fonctionne
Si vous avez déjà écrit un parser dans Scrapy, l'architecture de Crawly vous semblera native. Tout le travail est divisé en trois parties claires : les Spiders, les Middlewares et les Pipelines.
Un spider décrit les points d'entrée et la logique d'extraction des données des pages.
Voici un exemple typique de spider qui parcourt un catalogue de livres, collecte les titres avec les prix et suit les pages de pagination :
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
Le code est propre et déclaratif. L'analyse HTML se fait via la bibliothèque Floki avec des sélecteurs CSS familiers. Depuis la fonction parse_item, nous renvoyons une structure contenant des données prêtes à l'emploi et un lot de nouvelles requêtes pour le planificateur.
Configuration des pipelines et protection contre les blocages
Un parser rarement ne se compose pas seulement d'un client HTTP et d'un analyseur HTML. Vous devez suivre l'unicité des URL, limiter le nombre de requêtes concurrentes vers un domaine, modifier les en-têtes, filtrer les doublons et valider le schéma avant d'enregistrer.
Dans Crawly, tout cela est déplacé dans la configuration :
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"}
]
Que se passe-t-il ici ? Les requêtes passent d'abord par une chaîne de middlewares : un filtre de domaine étranger ne laissera pas le spider errer accidentellement pour scanner tout Internet, et UniqueRequest coupera les visites répétées aux mêmes pages. Lorsque le spider a extrait les données, elles passent dans le pipeline. Là, les champs requis sont vérifiés, les doublons sont filtrés par titre et les résultats valides sont conditionnés en JSON Lines et écrits sur le disque.
Les générateurs font gagner du temps au démarrage : la commande mix crawly.gen.spider créera un modèle de spider avec tous les rappels nécessaires, et mix crawly.gen.config préparera une configuration par défaut.
Panneau de gestion intégré
Un détail intéressant : à partir de la version 0.15.0, le projet a ajouté une interface web de gestion prête à l'emploi. Elle est disponible à l'adresse localhost:4001.
Grâce à ce panneau d'administration, vous pouvez :
- démarrer et arrêter manuellement les spiders,
- visualiser la file d'attente des requêtes planifiées,
- télécharger les éléments collectés et consulter les logs d'exécution,
- suivre l'état du crawler en temps réel.

Si vous intégrez Crawly dans une application web Phoenix ou Plug existante, vous n'êtes pas obligé de garder le panneau d'administration sur un port séparé. Vous pouvez simplement le router via le routeur commun via forward "/admin", Crawly.API.Router.
Rendu des pages dynamiques et exécution sans Elixir
Le web moderne est surchargé de JavaScript. Si le contenu se charge de manière asynchrone via AJAX ou si l'application est construite avec React, un simple GET HTTP renverra un squelette de page vide. Crawly peut fonctionner avec des renderers externes comme Chrome ou Splash. Vous configurez un navigateur headless et le framework récupère le DOM déjà rendu avec tous les scripts exécutés.
Une autre fonctionnalité non évidente est le mode autonome. Si personne dans votre équipe n'écrit en Elixir, vous n'avez pas besoin de faire tourner une base de code complète juste pour un crawler. Crawly peut s'exécuter dans un conteneur Docker minimaliste où les règles de scraping des pages sont décrites dans de simples fichiers YAML. C'est un exemple rare d'un outil Elixir ouvert aux développeurs d'autres stacks.
Scénarios pratiques
Où Crawly excelle le mieux :
- Surveillance des prix dans les boutiques en ligne. Quand vous devez régulièrement parcourir des milliers de fiches produits, vérifier les changements de stock et les réductions.
- Collecte de jeux de données d'entraînement pour le ML. Analyse d'articles, d'avis et de forums avec enregistrement au format jsonl.
- Agrégateurs de petites annonces. Collecte d'offres immobilières ou automobiles à partir de dizaines de sites régionaux.
- Archivage de contenu historique. Téléchargement rapide de blogs et de documents avec filtrage des liens rompus.
Qui bénéficiera de ce projet
Si votre langage principal est Elixir ou Erlang, Crawly est sans ambiguïté le meilleur choix pour le web scraping. Vous n'aurez pas à construire des solutions de contournement maladroites en Python aux côtés de votre service principal et à configurer la communication inter-services via des files d'attente.
Si vous écrivez en Python et que vous êtes fatigué de lutter contre les performances de Scrapy sur de grands volumes de données, Crawly mérite définitivement un regard. La barrière d'entrée est faible : les concepts correspondent presque un à un et la syntaxe Elixir se lit très facilement après Python. Vous pouvez commencer par la documentation officielle sur HexDocs et un tutoriel rapide sur le site de test books.toscrape.com.
Projets similaires