[PHP]xml_set_character_data_handlerとは?XML要素内のテキストを取得するハンドラの登録方法を徹底解説

PHP

はじめに

これまでの記事で、xml_parse() を使ったSAX方式のXML解析を数多く紹介してきましたが、その中で頻繁に登場していたのが xml_set_character_data_handler() です。今回はこの関数を正面から取り上げ、詳しく解説します。

XML文書における「要素の開始・終了」というイベントだけでは、<name>太郎</name>太郎 という実際のテキスト内容を取得することはできません。この「要素の中身のテキストデータ」を検知するためのコールバック関数(ハンドラ)を登録するのが xml_set_character_data_handler() の役割です。xml_set_element_handler() と並んで、SAX方式のXML処理において最も基本的で重要な関数の一つです。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


関数概要

項目内容
関数名xml_set_character_data_handler()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_set_character_data_handler(XMLParser $parser, ?callable $handler): true
引数1$parserxml_parser_create()等で生成したパーサーインスタンス
引数2$handler — テキストデータ検知時に呼び出すコールバック(nullで解除)
コールバックの引数(XMLParser $parser, string $data)
戻り値常に true
対応バージョンPHP 4以降
関連関数xml_set_element_handler()(要素の開始・終了)、xml_parse()(解析実行)

呼び出しの仕組み(イメージ図)

  XML文書
  "<name>太郎です</name>"
              │
              ▼
        xml_parse()による解析
              │
   ┌──────────┼──────────────┐
   ▼          ▼              ▼
  開始タグ検知  文字データ検知   終了タグ検知
  <name>       "太郎です"      </name>
   │            │              │
   ▼            ▼              ▼
  要素ハンドラ  character_data  要素ハンドラ
  (開始側)      ハンドラ         (終了側)
               ← ★この記事の対象

ポイントは、xml_set_character_data_handler() に登録したコールバックが、要素の中身のテキストが検出されるたびに(場合によっては複数回に分割されて)呼び出されるという点です。この「複数回に分割されうる」という特性が、初心者が最もつまずきやすいポイントです。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicCharacterDataDemo
{
    public function extractText(string $xml): array
    {
        $texts = [];
        $parser = xml_parser_create();

        // テキストデータを検知するたびに呼び出されるハンドラを登録
        xml_set_character_data_handler($parser, function ($p, $data) use (&$texts) {
            $trimmed = trim($data);
            if ($trimmed !== '') {
                $texts[] = $trimmed;
            }
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $texts;
    }
}

$demo = new BasicCharacterDataDemo();
print_r($demo->extractText('<root><name>太郎</name><age>30</age></root>'));

例2:分割されて呼び出される可能性を考慮した文字列連結クラス

<?php

class ConcatenatingTextCollector
{
    private string $buffer = '';

    /**
     * character_dataハンドラは1つの要素の中身であっても
     * 複数回に分けて呼び出されることがあるため、
     * バッファに追記していく方式で正確に文字列を組み立てる
     */
    public function collectFullText(string $xml): string
    {
        $this->buffer = '';
        $parser = xml_parser_create();

        xml_set_character_data_handler($parser, function ($p, $data) {
            // 単純に代入するのではなく、必ず追記する
            $this->buffer .= $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return trim($this->buffer);
    }
}

$collector = new ConcatenatingTextCollector();
echo $collector->collectFullText('<message>これは&amp;長い&amp;テキストです</message>') . PHP_EOL;

例3:要素の開始・終了ハンドラと組み合わせてタグごとのテキストを紐付ける

<?php

class TagTextMapper
{
    private array $result = [];
    private string $currentTag = '';
    private string $currentText = '';

    /**
     * 要素ハンドラで「今どのタグの中にいるか」を記録し、
     * character_dataハンドラでそのタグに対応するテキストを蓄積する
     */
    public function map(string $xml): array
    {
        $this->result = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) {
                $this->currentTag = $name;
                $this->currentText = '';
            },
            function ($p, $name) {
                $this->result[$this->currentTag] = trim($this->currentText);
            }
        );

        xml_set_character_data_handler($parser, function ($p, $data) {
            $this->currentText .= $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->result;
    }
}

$mapper = new TagTextMapper();
print_r($mapper->map('<product><name>ノート</name><price>300</price></product>'));

例4:CDATAセクションを含むテキストを扱うクラス

<?php

class CdataAwareTextExtractor
{
    /**
     * character_dataハンドラは通常のテキストだけでなく、
     * CDATAセクション内の内容も同様に受け取る
     */
    public function extract(string $xml): string
    {
        $result = '';
        $parser = xml_parser_create();

        xml_set_character_data_handler($parser, function ($p, $data) use (&$result) {
            $result .= $data;
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $result;
    }
}

$extractor = new CdataAwareTextExtractor();
echo $extractor->extract('<script><![CDATA[if (a < b) { alert("hi"); }]]></script>') . PHP_EOL;

例5:空白のみのテキストデータを除外するフィルタリング処理

<?php

class MeaningfulTextFilter
{
    /**
     * インデントや改行のためだけの空白文字データを除外し、
     * 意味のあるテキストのみを収集する
     */
    public function extractMeaningfulText(string $xml): array
    {
        $texts = [];
        $parser = xml_parser_create();

        xml_set_character_data_handler($parser, function ($p, $data) use (&$texts) {
            // trim後に空文字列になるものは除外する
            if (trim($data) !== '') {
                $texts[] = $data;
            }
        });

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $texts;
    }
}

$filter = new MeaningfulTextFilter();
$xml = "<root>\n  <item>value1</item>\n  <item>value2</item>\n</root>";
print_r($filter->extractMeaningfulText($xml));

例6:ハンドラをクラスメソッドとして登録する(配列コールバック構文)

<?php

class HandlerClassBasedExtractor
{
    private array $collected = [];

    public function parse(string $xml): array
    {
        $this->collected = [];
        $parser = xml_parser_create();

        // クラスのメソッドをコールバックとして登録するパターン
        xml_set_character_data_handler($parser, [$this, 'onCharacterData']);

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->collected;
    }

    public function onCharacterData(XMLParser $parser, string $data): void
    {
        $trimmed = trim($data);
        if ($trimmed !== '') {
            $this->collected[] = $trimmed;
        }
    }
}

$extractor = new HandlerClassBasedExtractor();
print_r($extractor->parse('<root><a>text1</a><b>text2</b></root>'));

例7:ハンドラを途中で解除(null設定)してテキスト取得を一時停止する

<?php

class SelectiveTextCapture
{
    private array $texts = [];
    private bool $capturing = false;

    /**
     * 特定のタグの中にいる間だけテキストを取得したい場合、
     * ハンドラをnullにして一時的に無効化するテクニック
     */
    public function captureOnlyInsideTag(string $xml, string $targetTag): array
    {
        $this->texts = [];
        $this->capturing = false;
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) use ($targetTag, $parser) {
                if ($name === $targetTag) {
                    $this->capturing = true;
                    xml_set_character_data_handler($parser, [$this, 'onCharacterData']);
                }
            },
            function ($p, $name) use ($targetTag, $parser) {
                if ($name === $targetTag) {
                    $this->capturing = false;
                    // nullを渡してハンドラを解除する
                    xml_set_character_data_handler($parser, null);
                }
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->texts;
    }

    public function onCharacterData(XMLParser $parser, string $data): void
    {
        if ($this->capturing && trim($data) !== '') {
            $this->texts[] = trim($data);
        }
    }
}

$capture = new SelectiveTextCapture();
print_r($capture->captureOnlyInsideTag('<root><ignore>skip</ignore><target>keep this</target></root>', 'target'));

関連関数との比較

関数役割xml_set_character_data_handlerとの違い
xml_set_character_data_handler()要素内のテキストデータを検知するハンドラを登録本記事の対象。要素の「中身」を扱う
xml_set_element_handler()要素の開始・終了を検知するハンドラを登録要素の「構造(境界)」を扱い、テキストの中身は扱わない
xml_set_processing_instruction_handler()XML処理命令(<?xml-stylesheet ...?>など)を検知テキストデータではなく処理命令を対象とする
xml_set_default_handler()他のハンドラで処理されなかったデータを検知より汎用的な「その他すべて」を捕捉するハンドラ
xml_set_comment_handler()XMLコメント(<!-- -->)を検知するハンドラを登録コメント専用であり、要素のテキストとは区別される

よくある落とし穴(注意点)

  1. 1つの要素の中身が複数回に分けてハンドラに渡されることがある これがこの関数を使う上で最も重要な注意点です。パーサーの実装上の理由(バッファサイズや特殊文字の処理など)により、"太郎です" というテキストが "太郎""です" のように分割されてコールバックされる可能性があります。文字列を単純に上書き代入するのではなく、必ず**追記(.=)**で連結するようにしましょう(例2を参照)。
  2. 空白のみのデータも通知される インデントや改行のためだけの空白文字も、character dataとしてハンドラに渡されます。意味のあるテキストだけを扱いたい場合は、trim() などで明示的にフィルタリングする必要があります(例5を参照)。XML_OPTION_SKIP_WHITEオプションを使うことでも、ある程度は制御可能です。
  3. 「今どの要素の中にいるか」は自分で管理する必要がある xml_set_character_data_handler() のコールバックには、対象のタグ名が直接渡されません。どの要素の中身なのかを知りたい場合は、xml_set_element_handler() と組み合わせて、現在の要素名を別途記録しておく必要があります(例3を参照)。
  4. HTMLエンティティ(&amp;など)は既にデコードされた状態で渡される &amp; のようなエンティティ参照は、パーサーによって自動的にデコードされた状態(&)でハンドラに渡されます。これは便利な反面、元の生のXML文字列とは異なる点に注意しましょう。
  5. ハンドラを登録するタイミングを誤る xml_parse() を呼び出した後にハンドラを登録しても、それ以前に処理された部分のイベントは捕捉できません。ハンドラの登録は必ず xml_parse() の呼び出し前に行いましょう。

まとめ

観点まとめ
何をする関数かXML要素内のテキストデータを検知するコールバック関数を登録する
主な用途要素の中身(テキスト内容)の抽出、CDATAセクションの取得
最重要の注意点テキストが複数回に分割されて渡される可能性があるため、必ず追記で連結する
組み合わせる関数xml_set_element_handler()(現在どの要素の中身かを把握するために必須)
その他の注意点空白データの扱い、エンティティのデコード済み状態、登録タイミング

xml_set_character_data_handler() は、SAX方式のXML解析において「要素の中身」を取得するために欠かせない関数です。分割呼び出しの可能性を正しく理解し、xml_set_element_handler() と組み合わせて現在のコンテキストを管理することで、正確で堅牢なテキスト抽出処理を実装できます。

タイトルとURLをコピーしました