网络爬虫是指从公开网站(如论坛、社交媒体、新闻网站、电商平台等)中提取数据的过程。为了让你对今天将要构建的内容有所了解,本文将介绍如何使用 Elixir 创建一个网络爬虫。
如果您对网页抓取的定义仍不明确,不妨将从网站保存一张图片视为手动网页抓取。如果您想手动保存网站上的所有图片,根据网站的复杂程度,这可能需要数小时甚至数天的时间。
通过构建一个网络爬虫,你可以将这一过程自动化。
也许您正在思考网络爬虫有哪些应用场景。以下是常见的几种:

网络爬虫是指从公开网站(如论坛、社交媒体、新闻网站、电商平台等)中提取数据的过程。为了让你对今天将要构建的内容有所了解,本文将介绍如何使用 Elixir 创建一个网络爬虫。
如果您对网页抓取的定义仍不明确,不妨将从网站保存一张图片视为手动网页抓取。如果您想手动保存网站上的所有图片,根据网站的复杂程度,这可能需要数小时甚至数天的时间。
通过构建一个网络爬虫,你可以将这一过程自动化。
也许您正在思考网络爬虫有哪些应用场景。以下是常见的几种:
您可以从您最喜欢的财经新闻网站抓取最新新闻,运行情绪分析算法,并在市场开盘并开始波动前的几分钟内,了解该投资什么
您可以抓取社交媒体页面上的评论,分析订阅者正在讨论什么,以及他们对您的产品或服务持何种态度。
如果您热衷于收藏游戏主机和电子游戏,但又不愿为最新的PS5花费巨资,您可以制作一个网络爬虫,用于抓取eBay上的商品列表,并在市场上出现便宜的主机时向您发送通知。
若想开发一款能识别任意图片中猫咪品种的移动应用,您将需要大量训练数据;与其手动保存数十万张猫咪图片来训练模型,不如使用网络爬虫自动完成这项工作。
我们将使用 Elixir 构建这个网络爬虫。Elixir 是一种基于 Erlang 构建的编程语言,由 Ruby on Rails 核心团队成员 José Valim 创建。该语言借鉴了 Ruby 语法的简洁性,并结合了 Erlang 构建低延迟、分布式和容错系统的能力。
在编写第一行代码之前,请确保您的计算机已安装 Elixir。请下载适用于您操作系统的安装程序,并按照安装页面上的说明进行操作。
安装过程中,你会发现还需安装 Erlang 编程语言。请注意,Elixir 在 Erlang 虚拟机(VM)中运行,因此两者缺一不可。
在本文中,您将学习如何使用 Elixir 创建一个 Web 爬虫,从 eBay 上抓取 PS5 相关商品信息,并将提取的数据(名称、URL、价格)存储在本地。
现在是时候检查 eBay 页面上的搜索结果并收集一些选择器了。
访问 ebay.com,在搜索框中输入“PS5”,然后点击搜索按钮。搜索结果页面加载完成后,打开浏览器的检查工具(在页面任意位置右键点击并选择“检查”)。
您需要收集以下选择器:
使用“选择器”工具,查找产品列表(ul)和产品条目(li):
利用这两个元素,您可以提取爬虫进行数据提取所需的类名:
让我们使用 mix 命令创建一个 Elixir 项目:
mix new elixir_spider --sup
--sup 标志会生成一个包含监督树的 OTP 应用程序骨架,对于像爬虫这样管理多个并发进程的应用程序来说,监督树是一项必不可少的功能。
创建临时文件夹
将当前目录切换到项目根目录:
cd elixir_spider
创建 temp 目录:
mkdir temp
我们将使用此目录来存储抓取到的项目。
创建 config/config.exs 文件,并将以下配置粘贴其中:
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: ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"]}
],
pipelines: [
{Crawly.Pipelines.Validate, fields: [:url, :title, :price]},
{Crawly.Pipelines.DuplicatesFilter, item_id: :title},
Crawly.Pipelines.JSONEncoder,
{Crawly.Pipelines.WriteToFile, extension: "jl", folder: "./temp"}
]让我们逐一了解每个属性并进行说明:
通过设置用户代理,您可以模拟真实浏览器,从而提升抓取效果。网站通常不欢迎抓取工具,并会尝试屏蔽任何看似不真实的用户代理。您可以使用此类工具获取您的浏览器用户代理。
WebScrapingAPI 会在每次请求时轮换用户代理和 IP 地址,并实现了无数种规避策略以防止此类情况发生。您的请求不会被拦截,而重试机制的实施将为您带来卓越的效果。
管道是一系列自上而下处理的命令,用于对处理后的数据项进行操作。我们使用以下管道:
网络爬虫(或称蜘蛛)是一种机器人程序,它会遍历网站,并通过 CSS 选择器根据用户定义的字段提取数据。爬虫可以提取页面上的所有链接,并利用特定链接(如分页链接)来爬取更多数据。
现在是时候为爬虫打好基础了:在 lib/elixir_spider 文件夹中创建 ebay_scraper.ex 文件,并将以下代码粘贴进去:
# lib/elixir_spider/ebay.ex
defmodule EbayScraper do
use Crawly.Spider
@impl Crawly.Spider
def base_url(), do: ""
@impl Crawly.Spider
def init() do
end
@impl Crawly.Spider
def parse_item(response) do
end
end这只是文件的框架,目前无法运行也不会返回任何结果。让我们先逐一讲解每个函数,然后逐步填充代码。
base_url() 函数仅被调用一次,它返回爬虫将要抓取的目标网站的基础 URL;该函数还用于过滤外部链接,防止爬虫跟随这些链接。毕竟,您并不想抓取整个互联网。
@impl Crawly.Spiderdef base_url(), do: "https://www.ebay.com/"
init() 函数仅被调用一次,用于初始化爬虫的默认状态;在此示例中,该函数返回爬取将从何处开始的 start_url。
请用以下代码替换您的空白函数:
@impl Crawly.Spider
def init() do
[start_urls: ["https://www.ebay.com/sch/i.html?_nkw=ps5"]]end
所有数据提取的“魔法”都在 parse_item() 函数中完成。该函数会在每个被抓取的 URL 上被调用。在此函数内,我们使用 Floki HTML 解析器来提取所需的字段:title、url 和 price。
该函数的代码如下:
@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(".srp-results .s-item")
|> Enum.map(fn x ->
%{
title: Floki.find(x, ".s-item__title span") |> Floki.text(),
price: Floki.find(x, ".s-item__price") |> Floki.text(),
url: Floki.find(x, ".s-item__link") |> Floki.attribute("href") |> Floki.text(),
}
end)
%{items: items}
end您可能已经注意到,我们正在使用“入门 - 检查目标”部分中找到的类,从 DOM 元素中提取所需数据。
现在是时候测试代码并确保其正常运行了。在项目根目录下,运行以下命令:
iex -S mix run -e "Crawly.Engine.start_spider(EbayScraper)"
如果您使用的是 PowerShell,请务必将 iex 替换为 iex.bat,否则会因 -S 参数不存在而报错。在 PowerShell 中请使用以下命令:
iex.bat -S mix run -e "Crawly.Engine.start_spider(EbayScraper)"
打开 ./temp 文件夹并查看 .jl 文件。您应该会看到一个文本文件,其中包含 JSON 对象列表,每行一个对象。每个对象都包含我们从 eBay 产品列表中需要的信息:标题、价格和 URL。
商品对象应呈现如下形式:
{"url":"https://www.ebay.com/itm/204096893295?epid=19040936896&hash=item2f851f716f:g:3G8AAOSwNslhoSZW&amdata=enc%3AAQAHAAAA0Nq2ODU0vEdnTBtnKgiVKIcOMvqJDPem%2BrNHrG4nsY9c3Ny1bzsybI0zClPHX1w4URLWSfXWX%2FeKXpdgpOe%2BF8IO%2FCh77%2FycTnMxDQNr5JfvTQZTF4%2Fu450uJ3RC7c%2B9ze0JHQ%2BWrbWP4yvDJnsTTWmjSONi2Cw71QMP6BnpfHBkn2mNzJ7j3Y1%2FSTIqcZ%2F8akkVNhUT0SQN7%2FBD38ue9kiUNDw9YDTUI1PhY14VbXB6ZMWZkN4hCt6gCDCl5mM7ZRpfYiDaVjaWVCbxUIm3rIg%3D%7Ctkp%3ABFBMwpvFwvRg","title":"PS5 Sony PlayStation 5 Console Disc Version! US VERSION!","price":"$669.99"}
我们已从商品列表的首页获取了所有商品,但这还不够。现在是时候实现分页功能,让爬虫提取所有可用的商品了。
让我们修改 parse_item() 函数,并添加一个新代码块,用于创建包含下一页分页链接的 requests 结构体。在 items 代码之后添加以下代码:
# Extract the next page link and convert it to a request
requests =
document
|> Floki.find(".s-pagination a.pagination__next")
|> Floki.attribute("href")
|> Crawly.Utils.build_absolute_urls(response.request_url)
|> Crawly.Utils.requests_from_urls()更新 parse_item() 函数的返回语句,使其同时包含下一个请求。该结构将如下所示:
%{
:requests => requests,
:items => items
}再次运行爬虫,不过这次不妨先泡杯咖啡。抓取所有 PS5 商品列表页面需要几分钟时间。
爬虫完成任务后,请检查 ./temp 文件夹中的抓取结果。您已成功从 eBay 抓取了 PS5 主机信息,并获得了一份包含价格的列表。您可以扩展此爬虫程序,用于抓取任何其他商品。
通过本文,你了解了什么是网页爬虫、这些爬虫有哪些应用场景、如何利用现成的库在几分钟内用 Elixir 搭建爬虫,以及如何运行并提取实际数据。
如果您觉得这已经很费劲了,那我得告诉您一个不太好的消息:我们目前只是浅尝辄止。长期运行这个爬虫会引发您意想不到的诸多问题。
eBay 会检测到你的活动并将其标记为可疑;爬虫将开始收到验证码提示;你将不得不扩展爬虫功能来解决验证码
eBay的检测系统可能会标记你的IP地址并阻止你访问网站;你将不得不准备一个代理池,并在每次请求时轮换IP地址。
你是否已经感到头晕目眩了?让我们再谈谈另一个问题:用户代理。你需要建立一个庞大的用户代理数据库,并在每次请求时轮换该值。检测系统会根据 IP 地址和用户代理来封锁爬虫。
如果您希望更专注于业务层面,将时间投入数据提取而非解决检测问题,使用爬虫即服务(Scraper as a Service)将是更优的选择。像 WebScrapingAPI 这样的解决方案可以解决上述所有问题以及更多其他问题。
