当 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 嵌入到现有的 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 最擅长的领域:
- 在线商店的价格监控。当你需要定期抓取数千张产品卡片、检查库存变化和折扣时。
- 为机器学习收集训练数据集。解析文章、评论和论坛并保存为 jsonl 格式。
- 列表聚合器。从数十个区域性网站收集房地产或汽车报价。
- 历史内容归档。快速下载博客和文档,并过滤损坏的链接。
谁将从这个项目中受益
如果你的主要语言是 Elixir 或 Erlang,Crawly 无疑是网页抓取的最佳选择。你不必在主服务旁边构建笨拙的 Python 变通方案,也不必通过队列设置服务间通信。
如果你使用 Python 编写,并且厌倦了在大量数据上与 Scrapy 的性能搏斗,Crawly 绝对值得一看。入门门槛很低:概念几乎一一对应,Elixir 语法在 Python 之后阅读起来非常容易。你可以从 HexDocs 上的官方文档和测试网站 books.toscrape.com 上的快速教程开始。
相关项目