この記事から得られる知識
- Javaの標準機能だけでHTMLを解析する方法を理解できる。
javax.swing.text.html.parser
パッケージの主要クラスであるParserDelegator
とHTMLEditorKit.ParserCallback
の役割と基本的な使い方を学べる。- HTMLドキュメントから特定のタグ、属性、テキストコンテンツを抽出する具体的な実装方法がわかる。
- イベント駆動型のパーサーの仕組みと、コールバックメソッドのカスタマイズ方法を習得できる。
- このライブラリの利点、注意点、そして現代的なWebスクレイピングにおける立ち位置を把握できる。
第1章: javax.swing.text.html.parserとは?
javax.swing.text.html.parser
は、Javaの標準クラスライブラリに含まれている、HTMLドキュメントを解析するためのパッケージです。Java SE 1.2から導入されており、JavaのGUIライブラリであるSwingの一部として提供されています。具体的には、このパッケージはjava.desktop
モジュール内に含まれています。
このパーサーの主な目的は、HTMLのタグ構造を読み解き、その内容をプログラムで扱えるように分解することです。もともとは、SwingのコンポーネントであるJEditorPane
などがHTMLコンテンツを表示する際に、内部的に使用されることを想定して設計されました。
最大の特徴は、外部ライブラリを追加することなく、Javaの標準機能だけでHTMLの基本的なパース処理を行える点にあります。これにより、環境構築の手間を省き、手軽にHTML解析を始めることができます。
パーサーのアーキテクチャ
このパーサーはイベント駆動型のアーキテクチャを採用しています。これは、HTMLドキュメントを先頭から順に読み込んでいき、「開始タグが見つかった」「テキストが見つかった」「終了タグが見つかった」といったイベントが発生するたびに、あらかじめ登録しておいた処理(コールバックメソッド)を呼び出す方式です。
この仕組みにより、巨大なHTMLファイルであっても、ドキュメント全体をメモリに読み込むことなく処理を進めることができ、メモリ効率が良いという利点があります。
第2章: 基本的な使い方 – パース処理の全体像
それでは、実際にjavax.swing.text.html.parser
を使ってHTMLをパースする基本的な手順を見ていきましょう。処理の核となるのは、ParserDelegator
クラスとHTMLEditorKit.ParserCallback
クラスです。
処理の全体的な流れは、以下の4つのステップで構成されます。
HTMLEditorKit.ParserCallback
の継承: HTMLの各要素(タグ、テキストなど)が解析されたときに実行したい処理を定義した、独自のコールバッククラスを作成します。ParserDelegator
のインスタンス化: 実際にパース処理を実行するパーサー本体のインスタンスを生成します。Reader
の準備: 解析対象のHTMLコンテンツを読み込むためのReader
(例:StringReader
やFileReader
)を用意します。- パースの実行:
ParserDelegator
のparse()
メソッドを呼び出し、パース処理を開始します。
シンプルなサンプルコード
以下のコードは、与えられたHTML文字列をパースし、検出したイベント(開始タグ、テキスト、終了タグ)をコンソールに出力する最も基本的な例です。
<?xml version="1.0" encoding="UTF-8"?>
import java.io.IOException;
import java.io.StringReader;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
public class BasicHtmlParserExample {
public static void main(String[] args) throws IOException {
String htmlString = "<html><head><title>テストページ</title></head>"
+ "<body><h1>こんにちは!</h1><p>これはサンプルテキストです。</p></body></html>";
// 1. ParserCallbackを実装したクラスのインスタンスを作成
HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() {
@Override
public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int pos) {
System.out.println("開始タグ検出: " + tag);
}
@Override
public void handleEndTag(HTML.Tag tag, int pos) {
System.out.println("終了タグ検出: " + tag);
}
@Override
public void handleText(char[] data, int pos) {
System.out.println("テキスト検出: " + new String(data));
}
@Override
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
// <br>や<img>のような空要素タグを処理
System.out.println("シンプルタグ検出: " + t);
}
};
// 2. ParserDelegatorのインスタンスを生成し、parseメソッドを呼び出す
// 3. StringReaderでHTML文字列を読み込む
// 4. パース実行
new ParserDelegator().parse(new StringReader(htmlString), callback, true);
}
}
実行結果
開始タグ検出: html
開始タグ検出: head
開始タグ検出: title
テキスト検出: テストページ
終了タグ検出: title
終了タグ検出: head
開始タグ検出: body
開始タグ検出: h1
テキスト検出: こんにちは!
終了タグ検出: h1
開始タグ検出: p
テキスト検出: これはサンプルテキストです。
終了タグ検出: p
終了タグ検出: body
終了タグ検出: html
この例からわかるように、パーサーはHTMLを順番に読み解き、タグやテキストを見つけるたびに、ParserCallback
でオーバーライドされた対応するメソッド(handleStartTag
, handleText
など)を呼び出しています。このシンプルな仕組みを理解することが、javax.swing.text.html.parser
を使いこなす第一歩となります。
第3章: ParserCallbackの詳細解説
HTMLEditorKit.ParserCallback
は、HTMLパーサーからの通知を受け取るための心臓部です。この抽象クラスを継承し、必要なメソッドをオーバーライドすることで、パース処理中に発生する様々なイベントを捉え、独自の処理を実装できます。
ここでは、主要なコールバックメソッドの役割と使い方を詳しく見ていきましょう。
メソッド | 説明 | 引数 |
---|---|---|
handleStartTag(...) |
開始タグ(例: <p> , <div> )が検出されたときに呼び出されます。 |
|
handleEndTag(...) |
終了タグ(例: </p> , </div> )が検出されたときに呼び出されます。 |
|
handleSimpleTag(...) |
終了タグを持たない空要素タグ(例: <br> , <img> , <hr> )が検出されたときに呼び出されます。 |
|
handleText(...) |
タグに囲まれたテキストコンテンツが検出されたときに呼び出されます。 |
|
handleComment(...) |
HTMLコメント(<!-- ... --> )が検出されたときに呼び出されます。 |
|
handleError(...) |
パース中にエラーが発生した場合に呼び出されます。 |
|
flush() |
パーサーがバッファをフラッシュする必要があるときに呼び出されます。通常、大きなドキュメントの解析中に複数回呼び出される可能性があります。 | なし |
属性の取得方法 – MutableAttributeSet
handleStartTag
やhandleSimpleTag
の引数であるMutableAttributeSet
は、タグが持つ属性情報を格納したオブジェクトです。このオブジェクトから特定の属性値を取得する方法は非常に重要です。
属性はキーと値のペアで格納されています。キーにはHTML.Attribute
で定義済みの定数(例: HTML.Attribute.HREF
, HTML.Attribute.SRC
)か、未定義の場合は属性名を文字列として使用します。
コード例: 属性を持つタグの解析
<?xml version="1.0" encoding="UTF-8"?>
import java.io.IOException;
import java.io.StringReader;
import java.util.Enumeration;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
public class AttributeParserExample {
public static void main(String[] args) throws IOException {
String htmlWithAttributes = "<html><body>"
+ "<h2>リンクと画像</h2>"
+ "<p>詳細は<a href=\\"https://www.google.com\\" id=\\"link-1\\">こちら</a>をクリック。</p>"
+ "<img src=\\"/images/logo.png\\" alt=\\"ロゴ画像\\">"
+ "</body></html>";
HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() {
@Override
public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int pos) {
if (tag == HTML.Tag.A) {
System.out.println("---- Aタグを検出 ----");
// getAttributeメソッドで特定の属性値を取得
String href = (String) attributes.getAttribute(HTML.Attribute.HREF);
String id = (String) attributes.getAttribute(HTML.Attribute.ID);
System.out.println("href属性: " + href);
System.out.println("id属性: " + id);
}
}
@Override
public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attributes, int pos) {
if (tag == HTML.Tag.IMG) {
System.out.println("---- IMGタグを検出 ----");
// すべての属性をループで取得
Enumeration<?> attributeNames = attributes.getAttributeNames();
while (attributeNames.hasMoreElements()) {
Object name = attributeNames.nextElement();
Object value = attributes.getAttribute(name);
System.out.println(name + "属性: " + value);
}
}
}
};
new ParserDelegator().parse(new StringReader(htmlWithAttributes), callback, true);
}
}
実行結果
---- Aタグを検出 ----
href属性: https://www.google.com
id属性: link-1
---- IMGタグを検出 ----
src属性: /images/logo.png
alt属性: ロゴ画像
このように、attributes.getAttribute(ATTRIBUTE_KEY)
とすることで、目的の属性値を簡単に取り出すことができます。属性キーにはHTML.Attribute
クラスに定義されている定数を利用するのが基本です。id
やclass
なども定数が用意されています。
第4章: 実践的な用例 – 特定の情報を抽出しよう
基本的な仕組みを理解したところで、より実践的なデータ抽出の例を見ていきましょう。コールバックメソッド内で状態を管理するためのフィールド(変数)を適切に使うことがポイントになります。
用例1: 全てのリンク(`<a>`タグ)のURLとアンカーテキストを抽出する
Webページからリンク一覧を作成する、典型的なスクレイピングのタスクです。この場合、<a>
タグが始まったことを示すフラグ変数を用意し、そのフラグが立っている間だけhandleText
でテキストを収集します。
<?xml version="1.0" encoding="UTF-8"?>
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
public class LinkExtractor extends HTMLEditorKit.ParserCallback {
private boolean inAnchorTag = false;
private String currentHref;
private StringBuilder currentAnchorText = new StringBuilder();
private List<String> results = new ArrayList<>();
@Override
public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int pos) {
if (tag == HTML.Tag.A) {
inAnchorTag = true;
currentHref = (String) attributes.getAttribute(HTML.Attribute.HREF);
}
}
@Override
public void handleText(char[] data, int pos) {
if (inAnchorTag) {
currentAnchorText.append(data);
}
}
@Override
public void handleEndTag(HTML.Tag tag, int pos) {
if (tag == HTML.Tag.A) {
inAnchorTag = false;
// 結果を整形してリストに追加
String result = "テキスト: [" + currentAnchorText.toString().trim() + "], URL: [" + currentHref + "]";
results.add(result);
// 次のAタグのためにリセット
currentAnchorText.setLength(0);
currentHref = null;
}
}
public List<String> getResults() {
return results;
}
public static void main(String[] args) throws IOException {
String html = "<html><body>"
+ "<p>公式サイトは<a href=\\"/official\\">こちら</a>です。</p>"
+ "<p>関連情報は<a href=\\"/related\\">ここをクリック</a>。</p>"
+ "<a href=\\"/lonely\\"></a>" // テキストがないリンク
+ "</body></html>";
LinkExtractor linkExtractor = new LinkExtractor();
new ParserDelegator().parse(new StringReader(html), linkExtractor, true);
System.out.println("---- 抽出したリンクリスト ----");
linkExtractor.getResults().forEach(System.out::println);
}
}
実行結果
---- 抽出したリンクリスト ----
テキスト: [こちら], URL: [/official]
テキスト: [ここをクリック], URL: [/related]
テキスト: [], URL: [/lonely]
この例では、クラスのフィールドとしてinAnchorTag
という真偽値フラグを持たせています。<a>
の開始タグでフラグをtrue
にし、終了タグでfalse
に戻します。これにより、handleText
が呼び出された際に、それが<a>
タグの内部のテキストかどうかを判断できます。
用例2: 特定のIDを持つdiv要素内の全てのテキストを抽出する
特定のセクション、例えば<div id="main-content">
の中身だけを取得したい、というケースもよくあります。これもフラグ管理で実現できますが、入れ子構造に対応するためにカウンタ変数を使うとより堅牢になります。
<?xml version="1.0" encoding="UTF-8"?>
import java.io.IOException;
import java.io.StringReader;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
public class DivContentExtractor extends HTMLEditorKit.ParserCallback {
private int divDepth = 0;
private StringBuilder content = new StringBuilder();
@Override
public void handleStartTag(HTML.Tag tag, MutableAttributeSet attributes, int pos) {
if (tag == HTML.Tag.DIV) {
if (divDepth > 0) { // 既に対象のdiv内にいる場合
divDepth++;
} else {
String id = (String) attributes.getAttribute(HTML.Attribute.ID);
if ("main-content".equals(id)) {
// 対象のdivが始まった
divDepth = 1;
}
}
}
}
@Override
public void handleText(char[] data, int pos) {
if (divDepth > 0) {
content.append(new String(data).trim()).append(" ");
}
}
@Override
public void handleEndTag(HTML.Tag tag, int pos) {
if (tag == HTML.Tag.DIV) {
if (divDepth > 0) {
divDepth--;
}
}
}
public String getContent() {
return content.toString().trim();
}
public static void main(String[] args) throws IOException {
String html = "<html><body>"
+ "<div id='header'>ヘッダー情報</div>"
+ "<div id='main-content'>"
+ " <h1>主要コンテンツ</h1>"
+ " <p>これは本文です。</p>"
+ " <div>入れ子のdiv</div>"
+ "</div>"
+ "<div id='footer'>フッター情報</div>"
+ "</body></html>";
DivContentExtractor extractor = new DivContentExtractor();
new ParserDelegator().parse(new StringReader(html), extractor, true);
System.out.println("---- 抽出したコンテンツ ----");
System.out.println(extractor.getContent());
}
}
実行結果
---- 抽出したコンテンツ ----
主要コンテンツ これは本文です。 入れ子のdiv
このコードではdivDepth
というカウンタ変数を使用しています。目的のIDを持つdiv
を見つけたらカウンタを1にし、内部で別のdiv
が始まったらインクリメント、終わったらデクリメントします。カウンタが0に戻った時点で、目的のdiv
ブロックが終了したと判断できます。これにより、入れ子構造があっても正確に範囲を特定できます。
第5章: 注意点とまとめ
ここまでjavax.swing.text.html.parser
の機能と使い方を解説してきました。非常に手軽で便利なライブラリですが、最後に改めてその特性と注意点を整理し、どのような場面で有効かをまとめます。
まとめ
javax.swing.text.html.parser
は、Java黎明期から存在する、歴史あるHTML解析ライブラリです。その設計は古く、現代の複雑なWeb環境には力不足な面も否めません。
しかし、その一方で、「Java標準機能だけで動作する」という大きなメリットは、今なお色褪せません。外部ライブラリの依存関係を追加できない制約のある環境や、ごく単純なHTMLから少数の情報を抜き出すといった用途、あるいはJavaの学習の一環としてパーサーの仕組みを学ぶ目的においては、非常に有用なツールです。
本記事で解説したイベント駆動の仕組みとParserCallback
の使い方をマスターすれば、このライブラリの能力を最大限に引き出すことができるでしょう。プロジェクトの要件を見極め、時にはJsoupのような高機能ライブラリと使い分けることで、より効率的な開発を進めることができます。