スカラウェブスクレイピング:取得、解析、保護されたページの解除
Expert in Web Scraping Technologies
TL;DR:
- Scalaは2つのパーツでウェブをスクレイピングします:取得のためのJVM HTTPクライアントと、抽出のためのjsoupベースのパーサーです。 requests-scalaがリクエストを行い、scala-scraperがHTMLをCSS選択可能なノードに変換します。
- プロジェクト全体は3つのsbt依存関係です。 HTTP用のrequests-scala 0.9.0、パース用のscala-scraper 3.2.0、後でデコードするJSONエンベロープ用のujson 4.1.0 — 学ぶべきフレームワークはありません。
- ページネーションは「次へ」リンクをループします。 scala-scraperはオプショナルなセレクタを使って次ページのhrefを読み取るので、カタログウォークは尾再帰関数であり、キューではありません。
- 静的取得には厳しい上限があります:JavaScriptを実行したり、ボットチャレンジをクリアすることはできません。 プレーンな
requests.getは、クライアントレンダリングされたページの空のシェルを返し、保護されたサイトではチャレンジの中間ページを取得します。 - Scrapeless Universal Scraping APIは、このギャップを素直なHTTP POSTで埋めます。
js_render: trueは、ページをサーバー側で実行し、完成したDOMを返します;サイトと通信する同じrequests-scalaクライアントがAPIと通信できます。 - アンロックコールはエンドポイントに対してライブで実行されました:HTTP 200、51,275バイトのレンダリングされたHTML、20の製品タイトル。 このガイドのリクエストとレスポンスの形状はそのライブ実行から来ています。
- 開始は無料です。 新しいScrapelessアカウントには無料ランタイムが含まれています — app.scrapeless.comでサインアップしてください。
はじめに:Scalaがスクレイピングに適している理由
ScalaはJVM上で動作します。つまり、Scalaで書かれたスクレイパーは、jsoupやAkka、成熟したHTTPエコシステムを無料で継承します。この言語は、スクレイピングがすでにJVM上にあるもの — Sparkジョブ、Kafkaプロデューサー、データサービス — にフィードする場合に自然に適しています。抽出を同じコードベース、同じ型で行いたい場合、つまりそれを消費するパイプラインと同じコードベースで行いたいのです。
その仕事の取得と解析の半分は短いです。数行でページを取得し、CSSセレクタを使って値を読み取ります。摩擦は、すべてのスクレイパーが直面するところから始まります:ウェブの成分の増加の多くが、データが実際に存在する前に実行されなければならないJavaScriptを使ってコンテンツを作成しています。そして、保護されたサイトは、TLSフィンガープリンティングや、通常のHTTPクライアントではクリアできないチャレンジページによってアクセスを制限しています。
このガイドではまず静的スクレイパーを構築します — sbtプロジェクト、HTTP取得、セレクタベースの抽出、ページネーション — そしてそのアプローチがどこで止まり、Scrapeless Universal Scraping APIにハードなページを渡すかの正直な境界線を引きます。最後のAPIコールはライブで実行され、その数字は実際のキャプチャです。
このスタックでできること
- JVM上で取得と解析を行う — リクエストのためのrequests-scala、CSSセレクション抽出のためのscala-scraper(jsoupラッパー)。
- 抽出をデータコードベースに保つ — 値をScala型に読み込み、それを使うSparkやKafkaのジョブのすぐ隣に置きます。
- ページネーションされたリストを歩く — 次ページのリンクを尾再帰ループで追い続け、尽きるまで行います。
- JavaScriptでレンダリングされるページや保護されたページにアクセス — それらをUniversal Scraping APIにPOSTし、同じ方法でレンダリングされたHTMLを解析します。
- アンチボットスタックをスキップ — TLSフィンガープリンティング、住宅IP、そしてチャレンジ解決はAPI側で実行され、あなたのScalaコードの外で行われます。
なぜScrapeless Universal Scraping APIなのか
Scrapeless Universal Scraping 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でデコードされます。
無料プランでAPIキーを取得するには、app.scrapeless.comでサインアップしてください。
前提条件
- JDK(11以上)とsbtがインストールされていること
- Scala 2.13(以下の依存関係のバージョンは2.13ビルドです)
- ScrapelessアカウントとAPIキー — app.scrapeless.comでサインアップ
- ターミナルの基本的な使い方の理解
注意:以下のビルドとステップのセクションにあるScalaコードは、このガイドの検証における前提条件のギャップに関するものであり、検証マシンではJVM/sbtランタイムが利用できなかったため、そのブロックはライブラリの現在のAPIおよびMaven Centralのバージョンに対して作成され、チェックされました。負荷を支えるScrapelessアンロックコールはエンドポイントに対してライブで実行され、そのリクエストとレスポンスは実際のキャプチャです。
インストール
2つのファイルを持つプロジェクトディレクトリを作成します。build.sbtは言語と3つの依存関係を固定します:
scala
ThisBuild / scalaVersion := "2.13.16"
lazy val scraper = (project in file("."))
.settings(
name := "scala-scraper-demo",
```ja
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
sbt.version=1.12.13
scala-scraper は jsoup を間接的に引き込むため、jsoup に直接依存することなく、型付きの Scala DSL を通じて jsoup のエンジンで解析します。すべてを解決するために一度 sbt update を実行し、次に REPL のための sbt console または main のための sbt run を実行します。
ステップ 1 — ページを取得する
requests-scala は薄い同期 HTTP クライアントです。一回の呼び出しでページの本文を文字列として取得します:
scala
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 セレクタでノードを選択します。>> 演算子は抽出を行い、elementList、attr、texts によりスカラーの値に結果を形作ります:
scala
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
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)
ループを礼儀正しく保つために、1つのホストを一度に、ページ間に少しの遅れを設け、欠落しているフィールドを Option として扱い、決して存在すると仮定しないようにします。
無料プランで API キーを取得してください: app.scrapeless.com
静的取得が止まるところ
requests.get は一つのことを行います: 匿名クライアントにサーバーが送信するバイトを返します。それはサーバー生成されたカタログには十分であり、他には何もありません。二つのケースがそれを壊し、どちらも一般的です:
- クライアント生成ページ。 サイトが JavaScript で内容を構築する場合、取得する HTML は空のシェルで、データはまだスクリプトにロックされています。scala-scraper は内容がバイトに存在しなかったために選択するものがありません。
- 保護されたページ。 アクティブな対ボット防御を持つサイトは、匿名のリクエストに対してチャレンジインタースティシャルで応答し、ページを返しません。通常の HTTP クライアントにはそれをクリアする方法がありません。
Scala での修正を再現すること — JavaScript を実行するためのヘッドレスブラウザ、住宅プロキシプール、チャレンジソルバー — は、スクレイプ自体よりもはるかに大きなプロジェクトです。実用的な方法は、Scala にその部分を行わせるのをやめ、これらの URL をレンダリング API に渡すことです。
クラウドのひねり: サーバーサイドでレンダリングし、Scala で解析する
Scrapeless Universal Scraping API はターゲット URL を受け取り、実際のブラウザと住宅エグレスを通じてサーバーサイドで実行し、完了した HTML を返します。Scala からは、同じ requests-scala クライアントを使って一つの POST を行い、ujson がレスポンスをデコードします:
scala
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",
ヘッダー = Map("Content-Type" -> "application/json", "x-api-token" -> apiKey),
データ = ujson.write(payload),
読み取りタイムアウト = 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
{
"code": 200,
"data": "<html>...レンダリングされたDOM...</html>"
}
// 説明サンプル:スキーマはライブコールからの実際の形状です。「data」文字列はここで省略されています。検証された実行では「data」は51,275バイトのJSONエスケープされたレンダリングHTMLを保持していました。
上記のカタログページのエンドポイントへのライブコールはHTTP 200を返し、51,275バイトのレンダリングされたHTMLが含まれていました。ステップ2のセレクターをそのHTMLに対して実行すると、最初の製品タイトル「A Light in the Attic」が£51.77で得られました。実行からのいくつかのメモ:
js_render: trueはレイテンシーを招くが、コンテンツを得ます。 静的ページのためにはオフにして速くし、何も表示されないページではオンにします。ujsonは必要な1つのフィールドを読み取ります。env("data").strは全体のデコードです。封筒の残りは単にステータスcodeです。- セレクターはScalaに留まります。 APIはHTMLを返しますので、抽出ロジック、型、テストはコードベースにあり、管理されたスキーマの背後にはありません。
- 欠落フィールドは
Optionとして扱います。 値がない場合のセレクターに>?>を使うのが正しい読み取りです。
結論:解析のためのScala、ハードフェッチのためのAPI
ScalaスクレイパーはJVMが得意なところで短くなります — リクエストにはrequests-scalaを、CSSセレクターの抽出にはscala-scraperを使い、次のページリンクを再帰的にたどります。同じ壁にぶつかります:クライアントレンダリングされたページとプレーンHTTPクライアントではクリアできない積極的なアンチボット防御。これらのURLをユニバーサルスクレイピングAPI経由でルーティングすると、修正は単一のPOSTに収まり、パースはそのままです。別の言語での同様のフェッチ-その後のパースの分割については、JavaScriptとNode.jsのスクレイピングガイドを参照してください;ドキュメントは完全なAPIとそのパラメータをカバーしています。js_renderをページの必要に合わせて調整し、セレクターはScalaに保持し、すべてのフィールドをオプションとして扱います。
AI駆動のデータパイプラインを構築する準備はできましたか?
私たちのコミュニティに参加して無料プランを取得し、JVMスクレイパーを構築する開発者とつながりましょう:Discord · Telegram。
app.scrapeless.com にサインアップして、無料のランタイムを利用し、上記のプログラムをあなたのScalaパイプラインが必要とするサイトとセレクターに適応させてください。価格を参照して、スケールを確認してください。
よくある質問
Q: Scalaでのスクレイピングは合法ですか?
公開されているデータのスクレイピングは一般的に許可されますが、ルールは管轄区域やサイトによって異なります。ターゲットの利用規約を確認し、ロボットの指示を尊重し、個人情報や制限されたデータを避け、商業的なものについては法律顧問に相談してください。
Q: プロキシは必要ですか?
サーバーレンダリングされたサイトの軽いスクレイピングでは不要です。保護されたページやクライアントレンダリングされたページでは、リクエストが195か国以上のユニバーサルスクレイピングAPIの住宅用プロキシを経由するため、Scalaでプールを構築する必要はありません。
Q: ボットチャレンジはどのようなもので、クリーンなレンダリングをどう取得しますか?
ページの代わりに、匿名リクエストはチャレンジインタースティシャルを受け取ります。そのURLをjs_render: trueでユニバーサルスクレイピングAPIを経由させると、信頼できる住宅用IPからサーバーサイドでページが実行され、完成したHTMLが返されます。
Q: 直接jsoupを呼び出す代わりにscala-scraperを選ぶ理由は何ですか?
scala-scraperはjsoupを型付けされたScala DSLでラップしているため、セレクターはJavaコレクションの代わりにList[String]やOption[Element]を返します。Scalaのパターンマッチングに合う結果を得るため、jsoupのパーサーが動作します。
Q: サイトが変更された後、セレクターが壊れました。どうすればいいですか?
マークアップは回転します。ページを再検査し、セレクターを厳密にし、次のリデザインで変更されるハッシュ付きCSSクラスよりも安定したコンテナクラスと属性を優先します(article.product_pod h3 a → title)。
Q: 複数のページを並行して実行できますか?
はい、しかし、ホストごとに約3人の作業者に抑えて、礼儀を保ち、レート制限を避けるようにしてください。小さな遅延を伴う尾再帰の単一ホストウォークが安全なデフォルトです。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



