Run Crawly in standalone mode
masterCrawly can run as a standalone Docker container. In this mode, spiders are provided as YML files or Elixir modules mounted into the container.
Refer to the following documentation for details:
repository·master·Indexed 22 days ago
https://github.com/elixir-crawly/crawlyAn 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.
Crawly can run as a standalone Docker container. In this mode, spiders are provided as YML files or Elixir modules mounted into the container.
Refer to the following documentation for details:
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.
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.
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}
endCrawly operates through a linear series of operations to fetch and process data:
Crawly.Requests are formed via Crawly.Spider.init/0 (or init/1).HTTPoison), and a Response is returned.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.New requests generated during the parsing step return to step 2 to continue the cycle.
Crawly provides a built-in management UI accessible at localhost:4001 by default. It allows you to:
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
...
endCrawly uses the Crawly.Pipeline behaviour to allow modular, sequential manipulations of both requests and items.
Crawly.Request before it is fetched.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.
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}localhost:4001) to control the Engine's behavior remotely. You can use curl to start, stop, and monitor spiders without interacting directly with the Elixir runtime.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
endFor dynamic content rendered via JavaScript, you can use the experimental crawly-render-server (a Puppeteer-based tool).
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 .)Configure it at the project level in your config.exs:
import Config
config :crawly,
fetcher: {Crawly.Fetchers.CrawlyRenderServer, [base_url: "http://localhost:3000/render"]}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
endTo 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.
{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.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
]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:
localhost:4001.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"