Crawly Documentation

repository·master·Indexed 22 days ago

https://github.com/elixir-crawly/crawly

An Elixir application framework for web crawling and structured data extraction. Crawly provides a system for defining spiders via the Crawly.Spider behavior, managing request lifecycles with Crawly.Request and Crawly.Response, and processing data through modular Request Middlewares and Item Pipelines. It includes a built-in management UI, CLI tools for spider generation, and support for standalone Docker deployment.

Tokens
15.7K
Snippets
61
Records
75
Agent score
78%

What's inside Crawly

  1. Selectively process items using pattern matching in pipelines

    master

    If your spider returns multiple types of items (e.g., blog posts and weather data), you can use pattern matching in your pipeline to target specific data types.

    1. Struct-based pattern matching

    Use this when you want to utilize existing Ecto schemas or pre-defined structs. Note: When using Ecto structs, you may need to convert the struct to a map before insertion to handle metadata.

    2. Key-based pattern matching

    Use this to process related items together or for bulk processing. This requires the spider's Crawly.Spider.parse_item/1 callback to return items with specific keys.

    Example: Key-based matching for blog posts

    If your spider returns:

    %{parsed_items: [%{blog_post: post}, %{weather: data}]}

    Your pipeline can target only the :blog_post key:

    defmodule MyApp.BlogPostPipeline do
      @impl Crawly.Pipeline
      def run(%{blog_post: old_blog_post} = item, state, _opts \ []) do
        # process the blog post
        updated_item = Map.put(item, :blog_post, %{my: "data"})
        {updated_item, state}
      end
    
      # Fallback: do nothing if it does not match
      def run(item, state, _opts), do: {item, state}
    end
  2. How the Crawly data flow works

    master

    Crawly operates through a linear series of operations to fetch and process data:

    1. Initialization: New Crawly.Requests are formed via Crawly.Spider.init/0 (or init/1).
    2. Pre-processing: Requests are individually pre-processed by Middlewares.
    3. Fetching: Data is fetched (currently via HTTPoison), and a Response is returned.
    4. Parsing: The Spider receives the response and executes parse_item/1, which returns a %Crawly.ParsedItem{} struct containing new Crawly.Requests to follow and new parsed items to store.
    5. Post-processing: Parsed items are individually post-processed by Item Pipelines.

    New requests generated during the parsing step return to step 2 to continue the cycle.

  3. Use the Crawly Management UI

    master

    Crawly provides a built-in management UI accessible at localhost:4001 by default. It allows you to:

    • Start and stop spiders
    • Preview scheduled requests
    • View and download extracted items
    • View and download logs

    Integration as a Plug: You can integrate the management UI into your own application's router using Crawly.API.Router.

    Disabling the UI: To disable the management UI and the REST API, set start_http_api?: false in your Crawly configuration.

    defmodule MyApp.Router do
      use Plug.Router
    
      ... 
      forward "/admin", Crawly.API.Router
      ...
    end
  4. Use Crawly.Pipeline for modular manipulations

    master

    Crawly uses the Crawly.Pipeline behaviour to allow modular, sequential manipulations of both requests and items.

    • Request Middlewares: Pipelines that modify a Crawly.Request before it is fetched.
    • Item Pipelines: Pipelines that modify and pre-process scraped items after they are parsed.
    • Response Parsers: Pipelines that parse a fetcher's response. If a Response Parser is declared, the spider's parse_item/1 callback is ignored.

    Each pipeline module is applied sequentially using Crawly.Utils.pipe/3. You can use Crawly.Utils.pipe/3 in your own tests to verify that your custom pipeline modules behave as expected.

  5. Send logs to CrawlyUI

    master

    You can stream logs directly to the CrawlyUI by configuring the Elixir :logger to use the Crawly.Loggers.SendToUiBackend.

    You must define the backend in your :logger configuration and then provide the specific configuration for the :send_log_to_ui module, specifying the destination node, the module to handle the log, and the function to call.

    # In config.exs
    config :logger,
      backends: [
        :console,
        {Crawly.Loggers.SendToUiBackend, :send_log_to_ui}
      ],
      level: :debug
    
    config :logger, :send_log_to_ui, destination: {:'ui@127.0.0.1', CrawlyUI, :store_log}
  6. Create a spider using Crawly.Spider

    master

    A spider is a module that implements the Crawly.Spider behavior. You must implement the following callbacks:

    • base_url/(): Returns the root URL of the site.
    • init/(): Returns a keyword list of starting URLs (e.g., [start_urls: ["..."]]).
    • parse_item(response): The core logic where you parse the HTML response, extract items, and return a %Crawly.ParsedItem{} struct containing extracted items and next_requests.
    # lib/crawly_example/books_to_scrape.ex
    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
        # Parse response body to document
        {:ok, document} = Floki.parse_document(response.body)
    
        # Create item (for pages where items exists)
        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
  7. Enable browser rendering with crawly-render-server

    master

    For dynamic content rendered via JavaScript, you can use the experimental crawly-render-server (a Puppeteer-based tool).

    Setup

    1. Clone and build the server:
      git clone https://github.com/elixir-crawly/crawly-render-server.git
      cd ./crawly-render-server
      docker run -p 3000:3000 --rm -it $(docker build -q .)

    Configuration

    Configure it at the project level in your config.exs:

    import Config
    
    config :crawly,
      fetcher: {Crawly.Fetchers.CrawlyRenderServer, [base_url: "http://localhost:3000/render"]}
  8. Implement the Crawly.Spider behaviour

    master

    To create a custom crawler, you must implement the Crawly.Spider behaviour. Spiders define how to crawl a site (following links) and how to extract structured data (scraping items).

    Required/Key behaviour functions:

    • init() or init(options): Returns a keyword list containing start_urls (a list of URLs to begin crawling). Alternatively, use start_requests if you need to prepare specific initial requests (e.g., including session cookies). Note that start_requests are processed before start_urls.
    • base_url(): Defines the base URL of the spider. This is used by Crawly.Middlewares.DomainFilter to prevent the crawler from leaving the target website.
    • parse_item(response): The core logic that translates a Response into a %Crawly.ParsedItem{} struct. This function must return a struct containing both items (the scraped data) and requests (new URLs to visit).

    Spiders run within Crawly.Worker processes. You can control concurrency using the concurrent_requests_per_domain setting.

    defmodule MySpider do
      use Crawly.Spider
    
      def init, do: [start_urls: ["https://example.com"]]
    
      def base_url, do: "https://example.com"
    
      def parse_item(response) do
        # Use a library like Floki to parse response.body
        items = [%{title: "Example Item"}]
        requests = [%Crawly.Request{url: "https://example.com/next-page"}]
        
        %Crawly.ParsedItem{items: items, requests: requests}
      end
    end
  9. Set up the Experimental UI for Crawly

    master

    To use the experimental management UI, you must integrate the SendToUI pipeline into your Crawly configuration and ensure your Erlang nodes can discover the UI node.

    1. Add the Pipeline: Add {Crawly.Pipelines.Experimental.SendToUI, ui_node: :'ui@127.0.0.1'} to your list of item pipelines. This must be placed before your encoder pipelines.
    2. Node Discovery: Ensure your Crawly nodes can find the CrawlyUI node. A common method is using the erlang-node-discovery application.
    # In your item pipelines configuration
    item_pipelines: [
      {Crawly.Pipelines.Experimental.SendToUI, ui_node: :'ui@127.0.0.1'},
      # ... other encoder pipelines follow
    ]
  10. Define spiders using YML files

    master

    Starting from version 0.15.0, you can define spiders directly as YML files via the Crawly Management interface. This approach reduces boilerplate for simple spiders that only require CSS selectors for data extraction and link following.

    To use this feature:

    1. Start the Crawly application.
    2. Open the Crawly Management interface at localhost:4001.
    3. Define your spider using the required YML structure (see YML Spider Structure).
    4. Use the Preview button in the interface to verify the extracted data format.
    5. Save the spider to enable scheduling via the Management interface.
    name: BooksSpiderForTest
    base_url: "https://books.toscrape.com/"
    start_urls:
        - "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
    fields:
        - name: title
        selector: ".product_main"
        - name: price
        selector: ".product_main .price_color"
    links_to_follow:
        - selector: "a"
        attribute: "href"