>_ DevTrendszh

语言

首页

语言

板块

前端 后端 移动端 DevOps AI / ML 游戏开发 区块链 嵌入式 安全
Elixir

当 Scrapy 显得力不从心且 Python 速度缓慢时:使用 Elixir 和 Crawly 收集数据

在工业级规模的网站解析和数据收集领域,几乎每个开发者首先想到的都是 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 版本开始,项目添加了一个开箱即用的 Web 管理界面。它可以在 localhost:4001 访问。

通过这个管理面板,你可以:

  • 手动启动和停止爬虫,
  • 查看已调度请求的队列,
  • 下载收集的条目并查看执行日志,
  • 实时跟踪爬虫状态。

Crawly Management UI

如果你将 Crawly 嵌入到现有的 Phoenix 或 Plug Web 应用程序中,你不必将管理面板放在单独的端口上。你可以通过 forward "/admin", Crawly.API.Router 简单地通过公共路由器路由它。

动态页面渲染和无 Elixir 运行

现代 Web 充斥着 JavaScript。如果内容通过 AJAX 异步加载,或者应用程序使用 React 构建,常规的 HTTP GET 将返回空的页面骨架。Crawly 可以与外部渲染器(如 Chrome 或 Splash)配合使用。你配置一个无头浏览器,框架就会获取已渲染的 DOM 和所有已执行的脚本。

另一个不明显的特性是独立模式。如果你的团队中没有人用 Elixir 编写,你不需要仅为爬虫启动一个完整的代码库。Crawly 可以在一个极简的 Docker 容器中运行,其中页面抓取规则在简单的 YAML 文件中描述。这是 Elixir 工具向其他技术栈的开发者敞开大门的罕见例子。

实际应用场景

Crawly 最擅长的领域:

  1. 在线商店的价格监控。当你需要定期抓取数千张产品卡片、检查库存变化和折扣时。
  2. 为机器学习收集训练数据集。解析文章、评论和论坛并保存为 jsonl 格式。
  3. 列表聚合器。从数十个区域性网站收集房地产或汽车报价。
  4. 历史内容归档。快速下载博客和文档,并过滤损坏的链接。

谁将从这个项目中受益

如果你的主要语言是 Elixir 或 Erlang,Crawly 无疑是网页抓取的最佳选择。你不必在主服务旁边构建笨拙的 Python 变通方案,也不必通过队列设置服务间通信。

如果你使用 Python 编写,并且厌倦了在大量数据上与 Scrapy 的性能搏斗,Crawly 绝对值得一看。入门门槛很低:概念几乎一一对应,Elixir 语法在 Python 之后阅读起来非常容易。你可以从 HexDocs 上的官方文档和测试网站 books.toscrape.com 上的快速教程开始。

相关项目