Javaによるウェブスクレイピング: jsoupを使って取得、解析、レンダリングする
Expert in Web Scraping Technologies
TL;DR:
- jsoupは、CSSセレクタエンジンを持つJavaのHTMLパーサーであり、独自のHTTPクライアントが付属しています。動作するスクレイパーは1つの依存関係と約20行のコードで実現できます。
- jsoupはデフォルトで応答の最初の2 MiBだけを保持します。2,235,648バイトのページでは、ちょうど2,097,152バイトを保持し、255の参照エントリのうち68を破棄し、セクション全体を失い、何も投げませんでした。
attr("href")は、HTMLに記載された通りのhrefを返します。attr("abs:href")はそれを文書の基準URIに対して解決します。- jsoupはJavaScriptを実行しません。クライアントレンダリングされたページでは、同じセレクタコードが0件を直接検出し、HTMLが事前レンダリングされた時には10件を検出しました。
- FetchルーティングをScrapeless Universal Scraping APIに通すことで、トランスポートが変更されるだけで、パース方法には影響を与えません。
- Scrapelessの無料プランで、このガイドのすべてのリクエストを実行するのに十分です。
Javaは多くのスクレイピングデータが宛てられる場所です。データを消費するサービスがSpringやQuarkusアプリケーションである場合、同じJVM内での抽出を維持すると、言語の境界、シリアル化のステップ、二次デプロイメントターゲットが不要になります。
それを実現するライブラリがjsoupです。jsoupは、ブラウザが行うように実世界のHTMLをパースします—未閉じタグの閉鎖、ネストの修正、属性の標準化を行い、HTMLパース仕様のエラーハンドリングルールに従います—その後、結果をCSSセレクタAPIを通じて公開します。
このガイドでは、2つのライブサイトに対して動作するスクレイパーを構築し、スクレイパーが正しいかどうかを決定する2つの動作を測定します:大きなページでjsoupが静かに破棄するものと、ブラウザでレンダリングされたページで返すものです。
jsoupが提供するもの
jsoupはまずパーサーで、次にHTTPクライアントです。Jsoup.connect(url)は流暢なリクエストビルダーを返し、.get()はリクエストを実行して、セレクタでクエリを行うDocumentを返します。
java
String url = "https://books.toscrape.com/";
Document doc = Jsoup.connect(url).get();
String title = doc.selectFirst("h1").text();
そのDocumentは文字列ではなく解析されたツリーなので、誤った形式のページでもクエリ可能な構造を提供します。また、取得元のベースURIを持っており、これは後の絶対URL解決を可能にします。
jsoupが行わないことはスクリプトを実行することです。JavaScriptエンジンやDOMイベントループはありません。サーバーが送信するものが、そのまま解析に供されます。
プロジェクトのセットアップ
解析には1つの依存関係が必要です。Gsonは、最後のセクションでJSONエンベロープを読み取るためだけにあります。そのセクションをスキップすれば、それを削除できます。
xml
<dependencies>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.21.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
コンパイラプラグインを明示的に固定します。Maven 3.8.7はデフォルトでmaven-compiler-plugin 3.1をバインドするため、maven.compiler.releaseを無視し、Source option 5 is no longer supportedで停止します:
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>com.example.Scraper</mainClass>
</configuration>
</plugin>
</plugins>
</build>
maven.compiler.releaseを17に設定すると、以下のコードは任意の現在のJDKでコンパイルできます。検証実行にはOpenJDK 21.0.11が使用されました。
ページを取得してパースする
すべてのリクエストにユーザーエージェントとタイムアウトを設定します。jsoupのデフォルトエージェントはjsoupとして自己を識別し、多くのサイトはそれだけで応答を変えます。
java
static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
static Document fetch(String url) throws IOException {
return Jsoup.connect(url)
.userAgent(USER_AGENT)
.timeout(30_000)
.get();
}
timeoutはミリ秒単位で接続と読み取りの両方に適用されます。これを超えるget()はSocketTimeoutExceptionを発生させ、2xx以外のステータスはコードを含むHttpStatusExceptionを引き起こします。
データを選択する
セレクタは標準CSSです。selectはすべての一致をElementsコレクションとして返し、selectFirstは1つのElementまたはnullを返します。
java
record Book(String title, String price, String url) {}
static List<Book> books(Document doc) {
List<Book> found = new ArrayList<>();
for (Element card : doc.select("article.product_pod")) {
Element link = card.selectFirst("h3 > a");
found.add(new Book(
link.attr("title"),
card.selectFirst("p.price_color").text(),
リンク.attr("abs:href")));
}
return found;
}
ライブカタログに対して、これにより20冊の本が返され、最初は『屋根裏の光』が51.77ポンドです。
最後の行の`abs:`プレフィックスは実際に機能しています。ソースのアンカーは次のように表示されます:
```text
catalogue/a-light-in-the-attic_1000/index.html
attr("href")はその文字列をそのまま返します。属性名にabs:をプレフィックスすると、それは文書のベースURIに対して解決され、WHATWG URL標準のアルゴリズムを使用して、次のようになります:
text
https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
生の値を保存するスクレイパーは、他の何かがそれを取得しようとする日までうまく機能します。
あなたのデータを奪う制限
jsoupは読み取るレスポンスボディに上限を設定しています。デフォルトは2MiBで、jsoupリクエストインターフェースに文書化されています。そのポイントを超えると、読み取りを停止し、持っているものを解析します。例外を上げず、警告をログに記録せず、ボディが短縮されたことを示すフラグをDocumentに残しません。
解析する前にレスポンスを見たい場合は、get()の代わりにexecute()を使用してください:
java
Connection.Response capped = Jsoup.connect(big)
.userAgent(USER_AGENT).timeout(60_000).execute();
Connection.Response whole = Jsoup.connect(big)
.userAgent(USER_AGENT).timeout(60_000).maxBodySize(0).execute();
2,235,648バイトのウィキペディア比較ページに対して実行すると、2つのレスポンスはちょうど上限によって異なります:
text
http status, default limit: 200
bytes received, default limit: 2097152
bytes received, maxBodySize(0): 2235648
table rows, default limit: 544
table rows, maxBodySize(0): 544
reference entries, default limit: 187
reference entries, maxBodySize(0): 255
last section, default limit: References
last section, maxBodySize(0): External links
どちらの実行もHTTP 200を返しました。どちらもDocumentを生成しました。ページが存在する比較テーブルは、文書の上部にあるため、どちらの場合も544行で同じ結果です。
カット以下のすべては単に存在しません。参考リストは255エントリのうち68を失い、最後の外部リンクセクションはトランケートされたツリーにはまったく存在しません。テーブルを読んでいるスクレイパーは決して気付かないでしょう。引用を集めるスクレイパーは、静かに四分の一を過小報告し、依然として健全に見えます。
maxBodySize(0)は制限を取り除きます。反射的に設定するのではなく、意図的に設定してください。予期しないレスポンスでの無制限の読み取りはそれ自体の問題です。しかし、それを意識的に設定し、サーバーが報告するContent-Lengthと比較してください。それは、HTTPセマンティクス仕様によってボディのオクテット数として定義されています。
ページがブラウザでレンダリングされるとき
https://quotes.toscrape.com/js/が境界を示します。これはクォートをJavaScript配列として提供し、DOMをクライアントサイドで構築します。そしてjsoupはそれを正常に取得します:
text
html bytes fetched directly: 5479
quotes found by jsoup: 0
5,479バイト、HTTP 200、結果ゼロ。jsoupが受け取ったマークアップには本当にdiv.quote要素が含まれていません。これらはスクリプトが実行された後に作成され、jsoupにはスクリプトエンジンがありません。
ほとんどのガイドは、ブラウザ自動化ツールに切り替えることでこの問題に対処します。つまり、抽出コードも書き直す必要があります。そのトレードオフは、CheerioとPuppeteerがNodeの両側に存在するものと同じで、Javaでも同じコストがかかります。より狭い修正は、HTMLが到着する方法だけを変更することです。その仕事はScrapeless Universal Scraping APIに属します。それはページをレンダリングし、その結果のHTMLを文字列として返します。あなたはそれを同じパーサーに渡します。
JDKの組み込みHttpClientで十分です。HTTP依存関係は必要ありません:
java
static String render(String url) throws IOException, InterruptedException {
JsonObject input = new JsonObject();
input.addProperty("url", url);
input.addProperty("proxy_country", "US");
input.addProperty("js_render", true);
JsonObject payload = new JsonObject();
payload.addProperty("actor", "unlocker.webunlocker");
payload.add("input", input);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.scrapeless.com/api/v2/unlocker/request"))
.header("Content-Type", "application/json")
.header("x-api-token", System.getenv("SCRAPELESS_API_KEY"))
.timeout(Duration.ofSeconds(180))
.POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("アンロッカーがHTTP " + response.statusCode() を返しました);
}
return JsonParser.parseString(response.body())
.getAsJsonObject().get("data").getAsString();
}
レスポンスエンベロープは {"code": ..., "data": "<rendered html>"} です。この文字列を Jsoup.parse(html, url) で解析します — URLを渡すことでベースURIが設定されるので、abs:href が機能し続けます — そして同じセレクターメソッドを実行します:
java
static List<String> quotes(Document doc) {
List<String> found = new ArrayList<>();
for (Element quote : doc.select("div.quote")) {
found.add(quote.selectFirst("span.text").text()
+ " -- " + quote.selectFirst("small.author").text());
}
return found;
}
text
レンダリングされたHTMLバイト数: 8940
同じパーサーで見つかった引用: 10
同じメソッド、同じセレクター、同じ Document API。変更されたのは、8,940バイトがどこから来たかだけです。APIキーは、上記の SCRAPELESS_API_KEY のように環境に保持し、ソースの中には入れないでください。ユニバーサルスクレイピングAPIの開始ガイドが残りのリクエストパラメータをカバーします。
始めるのは簡単です — 無料のScrapelessアカウントを作成すれば、このガイドで紹介されているすべてのことをカバーする無料プランがあります。
実行
クラスが組み立てられたら、1つのコマンドでコンパイルして実行します:
bash
export SCRAPELESS_API_KEY="your-api-key"
mvn -q -B compile exec:java
検証実行の完全な出力:
text
jsoup 1.21.1 | java 21.0.11
--- 静的ページ、直接パース ---
ページ1の書籍: 20
最初のタイトル: A Light in the Attic
最初の価格: £51.77
最初のURL: https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
HTMLに書かれたhref: catalogue/a-light-in-the-attic_1000/index.html
--- デフォルトのボディサイズ制限 ---
HTTPステータス、デフォルト制限: 200
受信バイト、デフォルト制限: 2097152
受信バイト、maxBodySize(0): 2235648
テーブル行、デフォルト制限: 544
テーブル行、maxBodySize(0): 544
参照エントリ、デフォルト制限: 187
参照エントリ、maxBodySize(0): 255
最後のセクション、デフォルト制限: 参照
最後のセクション、maxBodySize(0): 外部リンク
--- Javascriptレンダリングページ ---
直接取得したHTMLバイト: 5479
jsoupが見つけた引用: 0
--- ユニバーサルスクレイピングAPIを通じて同じページ ---
レンダリングされたHTMLバイト: 8940
同じパーサーで見つかった引用: 10
トラブルシューティング
Source option 5 is no longer supported. Mavenがデフォルトの maven-compiler-plugin をバインドしました。 <build><plugins> でプラグインのバージョンを指定してください。
selectFirst がnullを返し、次の行で NullPointerException が発生しました。 セレクタが何も一致しませんでした。HTML jsoupが実際に受け取った要素をチェックしてください — doc.html() — ではなく、ブラウザのインスペクタが表示するものに対してチェックしてください、それはポストスクリプトDOMです。
カウントがページに表示されているものよりも少ない。 response.bodyAsBytes().length をサーバーの Content-Length と比較してください。如果それらが 2,097,152 で一致する場合、ボディサイズの制限が原因です。
HttpStatusException: 403. サーバーがリクエストを拒否しました。まず現実的なユーザーエージェントを設定してください。ページがレンダリングを必要とする場合、上記のピボットが適用されます。
抽出されたテキストにモジバケがある。 jsoupは Content-Type ヘッダーから文字セットを読み込み、その後メタタグから読み込みます。サーバーがどちらも宣言しない場合、エンコーディングを Jsoup.parse(InputStream, String, String) に明示的に渡してください。
結論
静的HTMLの場合、jsoupとJDKが全体のツールチェーンです: 1つの依存関係、流れるリクエスト、CSSセレクターAPI、および不良マークアップでも生き残る解析されたツリー。結果が信頼できるかどうかを決定する2つの動作は、デフォルトではどちらも見えません — 健康的に見える部分的なドキュメントを返す2 MiBのボディ制限と、クライアントレンダリングされたページに対する空の結果セットです。
両方チェックするのは簡単です。受信バイト数を Content-Length と比較し、セレクターのカウントをページに表示されるものと比較します。2番目のチェックがマークアップが空の状態で失敗した場合、修正は輸送手段を変更し、パーサーに手を加えないことです。
試してみる準備はできていますか?Scrapelessの無料プランを始める そして、現在の価格設定を確認してください。
よくある質問
Q: jsoupだけで十分ですか、それともSeleniumが必要ですか?
サーバーでレンダリングされたHTMLの場合、jsoupだけで十分であり、ブラウザを起動しないためかなり速いです。データがJavaScriptによって生成されている場合のみレンダリングステップが必要であり、上記の測定が示すように、そのステップはフルブラウザではなく、レンダリングされたHTMLを返すHTTP呼び出しになることがあります。
Q: セレクタを書く前に、ページがJavaScriptを必要とするかどうかはどうやって見分けますか?
通常のjsoupのフェッチから doc.html() を印刷し、ページで見ることのできる値を検索してください。その文字列に値が存在しないが、ブラウザでは表示される場合、それはクライアント側でレンダリングされています。可視数に対してセレクタ数を比較することで同じことを確認できます。
Q: maxBodySize を変更する実用的な理由は何ですか?
デフォルトは2 MiBを保持しますが、これは多くのリスト、アーカイブ、および比較ページよりも小さいです。ターゲットがこれを超える場合、カットを超えた部分が解析ツリーから欠落し、エラーは発生しません。ドキュメント全体が必要な場合は maxBodySize(0) を設定し、受信したバイトを Content-Length と比較して、重要な時を知りましょう。
Q: jsoupは不正なHTMLを処理しますか?
はい。ブラウザが行うのと同じエラー回復ルールを適用するため、閉じていないタグやネストが間違った要素でも、クエリ可能なツリーが生成されます。これが、現実のページに対して厳密なXMLパーサーよりもこれを好む主な理由です。
Q: Javaでのスクレイピングは合法ですか?
言語は法的な質問には関係ありません。重要なのは収集するデータ、サイトの利用規約、遵守すべきロボット指令、そしてあなたが運営している管轄区域です。公開ページに収集を制限し、リクエストの量を控えめに保ち、個人データに関わることについては法的アドバイスを受けることが重要です。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



