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