サイトアイコン Omomuki Tech

Java標準ライブラリ javax.swing.text.html.parser の詳細解説

この記事から得られる知識

  • Javaの標準機能だけでHTMLを解析する方法を理解できる。
  • javax.swing.text.html.parserパッケージの主要クラスであるParserDelegatorHTMLEditorKit.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ファイルであっても、ドキュメント全体をメモリに読み込むことなく処理を進めることができ、メモリ効率が良いという利点があります。

注意点と限界

このパーサーは、HTML 3.2仕様に基づいて設計されており、非常に古いものです。そのため、以下のような限界点があることを理解しておくことが重要です。
  • HTML5非対応: HTML5で導入された新しいセマンティックタグ(<article>, <section>, <nav>など)を正しく認識できません。これらの未知のタグは、一般的なタグとして処理されます。
  • 寛容な解析: 多少構文が間違っているHTMLでも、エラーを発生させずに解析を試みます。これは便利な場合もありますが、厳密な解析には向きません。
  • JavaScript/CSS: JavaScriptによって動的に生成・変更されるコンテンツの解析はできません。また、CSSセレクタによる柔軟な要素の選択も不可能です。

したがって、現代の複雑なWebサイトのスクレイピングなど、高度な機能が求められる用途には、Jsoupなどのよりモダンで高機能なライブラリの利用が強く推奨されます。 このjavax.swing.text.html.parserは、シンプルなHTMLの解析や、Java標準ライブラリの範囲内で処理を完結させたい場合に適した選択肢と言えるでしょう。

第2章: 基本的な使い方 – パース処理の全体像

それでは、実際にjavax.swing.text.html.parserを使ってHTMLをパースする基本的な手順を見ていきましょう。処理の核となるのは、ParserDelegatorクラスとHTMLEditorKit.ParserCallbackクラスです。

処理の全体的な流れは、以下の4つのステップで構成されます。

  1. HTMLEditorKit.ParserCallbackの継承: HTMLの各要素(タグ、テキストなど)が解析されたときに実行したい処理を定義した、独自のコールバッククラスを作成します。
  2. ParserDelegatorのインスタンス化: 実際にパース処理を実行するパーサー本体のインスタンスを生成します。
  3. Readerの準備: 解析対象のHTMLコンテンツを読み込むためのReader(例: StringReaderFileReader)を用意します。
  4. パースの実行: ParserDelegatorparse()メソッドを呼び出し、パース処理を開始します。

シンプルなサンプルコード

以下のコードは、与えられた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>)が検出されたときに呼び出されます。
  • HTML.Tag t: 検出されたタグの種類(HTML.Tag.Pなど)。
  • MutableAttributeSet a: タグが持つ属性のセット。
  • int pos: ドキュメント内でのタグの開始位置。
handleEndTag(...) 終了タグ(例: </p>, </div>)が検出されたときに呼び出されます。
  • HTML.Tag t: 検出されたタグの種類。
  • int pos: ドキュメント内でのタグの開始位置。
handleSimpleTag(...) 終了タグを持たない空要素タグ(例: <br>, <img>, <hr>)が検出されたときに呼び出されます。
  • HTML.Tag t: 検出されたタグの種類。
  • MutableAttributeSet a: タグが持つ属性のセット。
  • int pos: ドキュメント内でのタグの開始位置。
handleText(...) タグに囲まれたテキストコンテンツが検出されたときに呼び出されます。
  • char[] data: 検出されたテキストデータ。
  • int pos: ドキュメント内でのテキストの開始位置。
handleComment(...) HTMLコメント(<!-- ... -->)が検出されたときに呼び出されます。
  • char[] data: コメントの内容。
  • int pos: ドキュメント内でのコメントの開始位置。
handleError(...) パース中にエラーが発生した場合に呼び出されます。
  • String errorMsg: エラーメッセージ。
  • int pos: エラーが発生した位置。
flush() パーサーがバッファをフラッシュする必要があるときに呼び出されます。通常、大きなドキュメントの解析中に複数回呼び出される可能性があります。 なし

属性の取得方法 – MutableAttributeSet

handleStartTaghandleSimpleTagの引数である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クラスに定義されている定数を利用するのが基本です。idclassなども定数が用意されています。

第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の機能と使い方を解説してきました。非常に手軽で便利なライブラリですが、最後に改めてその特性と注意点を整理し、どのような場面で有効かをまとめます。

利用上の主な注意点

  • スレッドセーフではない: ほとんどのSwing APIと同様に、このパーサーもスレッドセーフではありません。複数のスレッドから同時にアクセスすると、予期せぬ動作を引き起こす可能性があります。マルチスレッド環境で使用する場合は、適切な同期処理を施すか、スレッドごとに新しいインスタンスを作成する必要があります。
  • HTML5への不完全な対応: 前述の通り、このパーサーはHTML5で追加された新しい要素をネイティブにはサポートしていません。HTML.Tagに定義されていないタグはHTML.Tag.UNKNOWNとして扱われます。タグ名自体は取得可能ですが、型安全な扱いはできません。
  • 柔軟性の欠如: Jsoupなどのモダンなライブラリが提供する、CSSセレクタ(例: div#main-content > p.highlight)による強力で直感的な要素選択はできません。特定の要素にアクセスするには、本記事で紹介したようなフラグ管理やカウンタを用いた地道な実装が必要になります。
  • パフォーマンス: イベント駆動型のためメモリ効率は良いですが、極端に巨大な(数百MBを超えるような)HTMLファイルのパースでは、処理速度が問題になる可能性があります。パフォーマンスが最優先される要件の場合は、より高速な専用パーサーを検討する価値があります。

まとめ

javax.swing.text.html.parserは、Java黎明期から存在する、歴史あるHTML解析ライブラリです。その設計は古く、現代の複雑なWeb環境には力不足な面も否めません。

しかし、その一方で、「Java標準機能だけで動作する」という大きなメリットは、今なお色褪せません。外部ライブラリの依存関係を追加できない制約のある環境や、ごく単純なHTMLから少数の情報を抜き出すといった用途、あるいはJavaの学習の一環としてパーサーの仕組みを学ぶ目的においては、非常に有用なツールです。

本記事で解説したイベント駆動の仕組みとParserCallbackの使い方をマスターすれば、このライブラリの能力を最大限に引き出すことができるでしょう。プロジェクトの要件を見極め、時にはJsoupのような高機能ライブラリと使い分けることで、より効率的な開発を進めることができます。

モバイルバージョンを終了