>_ DevTrendsit

Lingua

Home

Linguaggi

Sezioni

Frontend Backend Mobile DevOps AI / ML GameDev Blockchain Embedded Sicurezza
Elixir

Quando Scrapy Diventa Stretto e Python È Lento: Raccogliere Dati con Elixir Usando Crawly

Quando si tratta di parsing di siti web e raccolta dati su scala industriale, quasi ogni sviluppatore pensa immediatamente a Python e Scrapy. È uno standard del settore, collaudato nel corso degli anni. Ma Python ha una particolarità: quando hai bisogno di scaricare centinaia di pagine in parallelo, mantenere migliaia di connessioni aperte e pulire i dati al volo, finisci per incontrare limitazioni di thread, async e GIL.

Nel mondo della programmazione funzionale, la macchina virtuale BEAM è ideale per questi compiti. I processi in Erlang ed Elixir sono isolati, pesano solo pochi kilobyte e possono essere generati in milioni. Il framework Crawly prende i migliori concetti architetturali da Scrapy (spider, middleware, pipeline di elaborazione) e li porta in Elixir. Il risultato è uno strumento pronto all'uso per gestire carichi massicci di richieste di rete senza complicate configurazioni di runtime async.

Cosa c'è dentro e come funziona

Se hai mai scritto un parser in Scrapy, l'architettura di Crawly ti sembrerà familiare. Tutto il lavoro è diviso in tre parti chiare: Spider, Middleware e Pipeline.

Uno spider descrive i punti di ingresso e la logica per estrarre i dati dalle pagine.

Ecco un esempio tipico di uno spider che naviga un catalogo di libri, raccoglie titoli con prezzi e segue le pagine di paginazione:

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

Il codice è pulito e dichiarativo. Il parsing HTML viene fatto attraverso la libreria Floki con selettori CSS familiari. Dalla funzione parse_item restituiamo una struttura contenente dati pronti all'uso e un batch di nuove richieste per lo scheduler.

Configurare le pipeline e la protezione contro i blocchi

Un parser raramente consiste solo in un client HTTP e un parser HTML. Devi tracciare l'unicità degli URL, limitare il numero di richieste concorrenti per dominio, modificare gli header, filtrare i duplicati e validare lo schema prima di salvare.

In Crawly, tutto questo viene spostato nella configurazione:

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"}
  ]

Cosa sta succedendo qui? Le richieste passano prima attraverso una catena di middleware: un filtro per domini esterni non permetterà allo spider di vagare accidentalmente per scansionare l'intero internet, e UniqueRequest taglierà le visite ripetute alle stesse pagine. Quando lo spider ha estratto i dati, questi passano nella pipeline. Lì, vengono controllati i campi obbligatori, i duplicati vengono filtrati per titolo e i risultati validi vengono impacchettati in JSON Lines e scritti su disco.

I generatori fanno risparmiare tempo all'avvio: il comando mix crawly.gen.spider creerà un template di spider con tutti i callback necessari, e mix crawly.gen.config preparerà una configurazione predefinita.

Pannello di gestione integrato

Un dettaglio interessante: a partire dalla versione 0.15.0, il progetto ha aggiunto un'interfaccia web di gestione pronta all'uso. È disponibile su localhost:4001.

Attraverso questo pannello admin puoi:

  • avviare e fermare manualmente gli spider,
  • visualizzare la coda delle richieste programmate,
  • scaricare gli elementi raccolti e visualizzare i log di esecuzione,
  • tracciare lo stato del crawler in tempo reale.

Crawly Management UI

Se stai integrando Crawly in un'applicazione web Phoenix o Plug esistente, non devi mantenere il pannello admin su una porta separata. Puoi semplicemente instradarlo attraverso il router comune tramite forward "/admin", Crawly.API.Router.

Rendering di pagine dinamiche ed esecuzione senza Elixir

Il web moderno è sovraccarico di JavaScript. Se il contenuto viene caricato in modo asincrono tramite AJAX o l'applicazione è costruita con React, un normale HTTP GET restituirà uno scheletro di pagina vuoto. Crawly può lavorare con renderer esterni come Chrome o Splash. Configuri un browser headless e il framework recupera il DOM già renderizzato con tutti gli script eseguiti.

Un'altra funzionalità non ovvia è la modalità standalone. Se nessuno nel tuo team scrive in Elixir, non hai bisogno di avviare un intero codebase solo per un crawler. Crawly può essere eseguito in un container Docker minimalista dove le regole di scraping delle pagine sono descritte in semplici file YAML. Questo è un esempio raro di uno strumento Elixir aperto a sviluppatori di altri stack.

Scenari pratici

Dove Crawly eccelle:

  1. Monitoraggio prezzi nei negozi online. Quando hai bisogno di scansionare regolarmente migliaia di schede prodotto, controllare i cambiamenti di inventario e gli sconti.
  2. Raccolta di dataset di training per ML. Parsing di articoli, recensioni e forum con salvataggio in formati jsonl.
  3. Aggregatori di annunci. Raccolta di offerte immobiliari o automobilistiche da decine di bacheche regionali.
  4. Archiviazione di contenuti storici. Download rapido di blog e documenti con filtro dei link interrotti.

Chi trarrà beneficio da questo progetto

Se il tuo linguaggio principale è Elixir o Erlang, Crawly è senza dubbio la scelta migliore per il web scraping. Non dovrai costruire workaround goffi in Python accanto al tuo servizio principale e configurare la comunicazione tra servizi attraverso code.

Se scrivi in Python e sei stanco di combattere con le prestazioni di Scrapy su grandi volumi di dati, Crawly merita sicuramente un'attenzione. La barriera d'ingresso è bassa: i concetti corrispondono quasi uno a uno, e la sintassi di Elixir è molto leggibile dopo Python. Puoi iniziare con la documentazione ufficiale su HexDocs e un tutorial rapido sul sito di test books.toscrape.com.

Progetti correlati