🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

Scala网络爬虫:获取、解析和解锁受保护的页面

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

23-Jun-2026

简要说明:

  • Scala通过两个部分抓取网络:一个JVM HTTP客户端用于获取,一个基于jsoup的解析器用于提取。 requests-scala负责请求;scala-scraper将HTML转换为可用CSS选择器的节点。
  • 整个项目只有三个sbt依赖。 requests-scala 0.9.0用于HTTP,scala-scraper 3.2.0用于解析,ujson 4.1.0用于稍后解码的一个JSON包——无需学习框架。
  • 分页是对“下一页”链接的循环。 scala-scraper通过一个可选选择器读取下一页的href,因此目录遍历是一个尾递归函数,而不是一个队列。
  • 静态获取有一个严格的上限:它无法运行JavaScript或通过机器人挑战。 普通的requests.get返回一个客户端渲染页面的空壳,并在受保护网站上出现挑战插页。
  • Scrapeless通用抓取API通过简单的HTTP POST弥补了这一缺口。 js_render: true在服务器端运行页面并返回完成的DOM;与网站通信的相同requests-scala客户端可以与API通信。
  • 解锁调用是在端点上实时运行的:HTTP 200,51,275字节的渲染HTML,20个产品标题。 本指南中的请求和响应形状直接来自于那次实时运行。
  • 免费开始。 新的Scrapeless账户包括免费运行时间——在app.scrapeless.com注册。

引言:Scala在抓取中的位置

Scala运行在JVM上,这意味着用它编写的抓取器可以免费继承jsoup、Akka和成熟的HTTP生态系统。当抓取的内容已经在JVM上时——一个Spark作业,一个Kafka生产者,一个数据服务——并且你希望在同一代码库内以相同类型进行提取时,这种语言非常合适。

该任务的获取和解析部分很简短。少量代码可以拉取页面并用CSS选择器读取值。阻力开始于每个抓取器的共同点:网络的越来越多的内容使用需要实际运行的JavaScript构建,在数据存在之前,受保护网站则在TLS指纹识别和挑战页面之后限制访问,而普通HTTP客户端无法通过这些。

本指南首先构建静态抓取器——sbt项目、HTTP获取、基于选择器的提取、分页——然后明确指出这种方法停止的地方,将困难页面交给Scrapeless通用抓取API。最后的API调用是在实时中运行的;它的数据是一个真实的捕获。

你可以用这个栈做什么

  • 在JVM上获取和解析——使用requests-scala进行请求,使用scala-scraper(一个jsoup包装器)进行CSS选择器提取。
  • 在你的数据代码库中保持提取——将值直接读取为Scala类型,紧邻使用它们的Spark或Kafka作业。
  • 遍历分页列表——在尾递归循环中跟随下一页链接,直到它用完。
  • 访问JavaScript渲染和受保护的页面——将它们POST到通用抓取API,并以相同的方式解析渲染的HTML。
  • 跳过反机器人栈——TLS指纹识别、住宅IP和挑战解决在API端发生,而不是在你的Scala代码中。

为什么选择Scrapeless通用抓取API

Scrapeless通用抓取API接收目标URL并返回渲染的、未被阻止的HTML。特别是对于Scala客户端,它提供:

  • 服务器端JavaScript渲染——js_render: true返回完成的DOM,因此scala-scraper看到的是真实内容,而不是空壳。
  • 在195多个国家的住宅代理——抓取从受信任的IP出发;你从不需要在Scala中构建或轮换池。
  • 反机器人处理——TLS指纹识别和挑战解决发生在API端,脱离你的JVM进程。
  • 一个简单的HTTPS POST——无需在build.sbt中添加SDK;你已经拥有的requests-scala客户端就足够了。
  • 一个小包——{"code":200,"data":"<html>…"},用你在其他地方使用的相同ujson解码。

app.scrapeless.com的免费计划上获取你的API密钥。

先决条件

  • 安装JDK(11或更新)和sbt
  • Scala 2.13(下面的依赖版本是2.13构建)
  • 一个Scrapeless账户和API密钥——在app.scrapeless.com注册
  • 对终端有基本的熟悉

注意:下面构建和步骤部分的Scala代码是本指南验证中的先决条件间隙——在验证机上没有可用的JVM/sbt运行时,因此这些代码块是根据库的当前API和Maven Central版本编写和检查的,而不是执行的。负载承载的Scrapeless解锁调用在端点上实时运行;其请求和响应是一个真实的捕获。

安装

创建一个包含两个文件的项目目录。build.sbt锁定了语言和三个依赖项:

scala Copy
ThisBuild / scalaVersion := "2.13.16"

lazy val scraper = (project in file("."))
  .settings(
    name := "scala-scraper-demo",
zh Copy
libraryDependencies ++= Seq(
      "com.lihaoyi"      %% "requests"      % "0.9.0",
      "net.ruippeixotog" %% "scala-scraper" % "3.2.0",
      "com.lihaoyi"      %% "ujson"         % "4.1.0"
    )
  )

project/build.properties 锁定了 sbt 的版本:

text Copy
sbt.version=1.12.13

scala-scraper 通过传递依赖引入 jsoup,因此您可以通过类型化的 Scala DSL 使用 jsoup 的引擎进行解析,而无需直接依赖 jsoup。运行一次 sbt update 可以解析所有内容,然后运行 sbt console 进入 REPL,或运行 sbt run 进行 main


步骤 1 — 获取页面

requests-scala 是一个轻量级的同步 HTTP 客户端。一次调用可以获取页面主体作为字符串:

scala Copy
val res = requests.get(
  "https://books.toscrape.com/",
  headers = Map("User-Agent" -> "Mozilla/5.0 (compatible; scala-scraper-demo)")
)

println(res.statusCode)   // 200
val html: String = res.text()

res.text() 是原始 HTML。对于这样的服务器渲染页面,该字符串已经包含了数据;而对于客户端渲染页面,它将包含一个空的外壳,这就是步骤 4 所要解决的限制。


步骤 2 — 使用 scala-scraper 解析

scala-scraper 将字符串解析为文档,并通过其 DSL 使用 CSS 选择器选择节点。>> 运算符提取;elementListattrtexts 将结果转换为 Scala 值:

scala Copy
import net.ruippeixotog.scalascraper.browser.JsoupBrowser
import net.ruippeixotog.scalascraper.dsl.DSL._
import net.ruippeixotog.scalascraper.dsl.DSL.Extract._

val doc = JsoupBrowser().parseString(html)

val titles: List[String] = doc >> elementList("article.product_pod h3 a") >> attr("title")
val prices: List[String] = doc >> texts("p.price_color")

val books = titles.zip(prices)
books.foreach { case (t, p) => println(s"$p  $t") }

article.product_pod h3 a 是这里的持久选择器 - 产品卡片类加上其标题内的链接 - 而 title 即使在可见文本被截断时也会携带完整名称。从属性中提取值而不是渲染文本是更稳定的读取方式,只要网站提供该属性。


步骤 3 — 跟随分页

目录在多页之间继续,每一页通过 li.next a 元素链接到下一页。scala-scraper 的可选选择器 >?> 在该链接缺失时返回 None,这正是循环的停止条件:

scala Copy
import net.ruippeixotog.scalascraper.model.Document

def nextUrl(doc: Document, base: String): Option[String] =
  (doc >?> element("li.next a")).map(a => base + a.attr("href"))

@annotation.tailrec
def crawl(url: String, base: String, acc: List[String]): List[String] = {
  val doc   = JsoupBrowser().parseString(requests.get(url).text())
  val names = doc >> elementList("article.product_pod h3 a") >> attr("title")
  nextUrl(doc, base) match {
    case Some(next) => crawl(next, base, acc ++ names)
    case None       => acc ++ names
  }
}

val all = crawl("https://books.toscrape.com/catalogue/page-1.html",
                "https://books.toscrape.com/catalogue/", Nil)
println(all.size)

保持循环礼貌 - 一次一个主机,页面之间有小的延迟 - 并将缺失的字段视为 Option,绝不要假设它是存在的值。

在免费计划上获取您的 API 密钥:app.scrapeless.com


静态获取的止境

requests.get 做一件事:它返回服务器发送给匿名客户端的字节。对于一个服务器渲染的目录来说,这已经足够,没什么更多的。两个案例会打破它,而且这两者都很常见:

  • 客户端渲染的页面。 当网站用 JavaScript 构建其内容时,您获取的 HTML 是一个带有数据仍被锁在脚本中的空外壳。scala-scraper 无法选择,因为内容从未在字节中。
  • 受保护的页面。 具有主动反机器人防御的网站会对匿名请求响应一个挑战插图,而不是页面。普通的 HTTP 客户端无法清除它。

在 Scala 中再次实现这个修复 - 运行 JavaScript 的无头浏览器,住宅代理池,挑战解决器 - 是一个远比抓取本身大得多的项目。务实的方法是停止让 Scala 执行那部分,并将这些 URL 交给渲染 API。

云的变化:服务器端渲染,在 Scala 中解析

Scrapeless 通用抓取 API 以目标 URL 为输入,通过真实浏览器和住宅出口在服务器端运行它,并返回处理后的 HTML。从 Scala 中,使用与 requests-scala 客户端相同的 POST,ujson 解码响应:

scala Copy
val apiKey = sys.env("SCRAPELESS_API_KEY")

val payload = ujson.Obj(
  "actor" -> "unlocker.webunlocker",
  "input" -> ujson.Obj(
    "url"       -> "https://books.toscrape.com/",
    "method"    -> "GET",
    "redirect"  -> true,
    "js_render" -> true
  )
)

val res = requests.post(
  "https://api.scrapeless.com/api/v1/unlocker/request",
plaintext Copy
headers = Map("Content-Type" -> "application/json", "x-api-token" -> apiKey),
  data    = ujson.write(payload),
  readTimeout = 120000
)

val env  = ujson.read(res.text())
val html = env("data").str        // 渲染的 DOM 作为字符串

js_render: true 是一个重要的标志:它告诉 API 运行页面的 JavaScript,并返回完成的 DOM,因此客户端构建内容的网站作为真实的标记返回。从这里,html 直接传入相同的 JsoupBrowser().parseString(html) 和第 2 步的相同选择器——您的爬虫的解析部分没有变化,仅提取方式不同。

您得到的返回信息

API 响应是一个小而可预测的封装:

json Copy
{
  "code": 200,
  "data": "<html>...渲染的 DOM...</html>"
}
// 说明性示例: schema 是来自实时调用的真实形状;这里“data”字符串被截断。在经过验证的运行中,“data”持有 51,275 字节的 JSON 转义渲染 HTML。

对上述目录页面的实时调用返回了 HTTP 200,渲染的 HTML 为 51,275 字节;对该 HTML 运行第 2 步的选择器产生了 20 个产品标题,第一个是“阁楼里的光”价格为 £51.77。运行的几点说明:

  • js_render: true 增加延迟但获取了内容。 对于静态页面将其关闭可以加快速度;当页面在关闭时为空时则打开。
  • ujson 读取您所需的一个字段。 env("data").str 是整个解码;封装的其余部分只是状态 code
  • 选择器保持在 Scala 中。 API 返回 HTML,因此提取逻辑、类型和测试在您的代码库中,而不是在管理模式后面。
  • 将缺失字段视为 Option 具有 >?> 的可为 null 选择器是当卡片可能省略价格或标题时正确的读取方式。

结论: Scala 负责解析,API 负责困难的提取

Scala 爬虫在 JVM 强项的地方简洁——使用 requests-scala 进行请求,scala-scraper 进行 CSS 选择器提取,递归遍历下一页链接。它会遇到每个静态爬虫所面临的同样壁垒:客户端渲染页面和活动的反机器人防护,普通的 HTTP 客户端无法解决。通过 通用抓取 API 路由这些 URL,使修复仅限于单个 POST,并保持您的解析不变。有关其他语言中相同的提取-解析分离,请参见 JavaScript 和 Node.js 抓取指南文档 涵盖了完整的 API 及其参数。根据页面的需求固定 js_render,将选择器保留为 Scala,并将每个字段视为可选。

准备好构建您的 AI 驱动的数据管道了吗?

加入我们的社区,领取免费计划,并与构建 JVM 爬虫的开发人员连接:Discord · Telegram

app.scrapeless.com 注册获取免费运行时间,并将上述程序适应于您的 Scala 管道所需的网站和选择器。有关规模的定价,请参见 定价

常见问题

问:用 Scala 抓取是否合法?
抓取公开可见的数据通常是允许的,但规则因地区和网站而异。请审查目标的服务条款,遵守爬虫指令,避免个人或受限数据,并就任何商业事宜咨询法律顾问。

问:我需要代理吗?
对于轻量级抓取服务器呈现的网站,不需要。对于受保护或客户端渲染的页面,请求通过通用抓取 API 的住宅代理在 195 个国家中进行出网,因此您不需要在 Scala 中构建一个池。

问:机器人挑战是怎样的,我如何获得干净的渲染?
而不是页面,匿名请求会收到一个挑战中介。通过带有 js_render: true 的通用抓取 API 路由该 URL;它从受信任的住宅 IP 在服务器端运行页面并返回完成的 HTML。

问:为什么使用 scala-scraper 而不是直接调用 jsoup?
scala-scraper 在带类型的 Scala DSL 中封装了 jsoup,因此选择器返回 List[String]Option[Element] 而不是 Java 集合。您可以使用符合 Scala 模式匹配的结果获取 jsoup 的解析器。

问:网站更改后我的选择器失效了。现在怎么办?
标记会旋转。重新检查页面并收紧选择器——优先选择一个稳定的容器类和属性读取(article.product_pod h3 atitle),而不是下次重新设计时可能更改的哈希 CSS 类。

问:我可以并行运行多个页面吗?
是的,但每个主机大致保持三名工人,以保持礼貌并避免速度限制。带有小延迟的尾递归单主机遍历是安全的默认设置。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录