[PHP]xml_parseとは?Expatベースのイベント駆動型XMLパーサーの使い方を徹底解説

PHP

はじめに

これまでの記事で、XML解析エラーの詳細を取得するための関連関数(xml_error_string()xml_get_error_code()xml_get_current_line_number() など)を数多く紹介してきました。それらすべての土台となっているのが、今回解説する xml_parse() 関数です。

xml_parse() は、PHPに標準搭載されているExpatベースのXMLパーサーを使って、実際にXML文書の解析処理を実行するための中心的な関数です。SimpleXMLやDOMDocumentのようにXML全体を一度にツリー構造として読み込む方式(DOM方式)とは異なり、xml_parse()SAX(Simple API for XML)方式と呼ばれる、文書を順番に読み進めながら要素の開始・終了などのイベントごとにコールバック関数を呼び出す方式を採用しています。本記事では、この関数の基本的な使い方から、実践的な活用パターンまで詳しく解説します。


関数概要

項目内容
関数名xml_parse()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_parse(XMLParser $parser, string $data, bool $is_final = false): int
引数1$parserxml_parser_create() で生成したパーサーインスタンス
引数2$data — 解析対象のXML文字列(分割して複数回渡すことも可能)
引数3$is_final — これが最後のデータチャンクであればtrue
戻り値成功時に 1、失敗時に 0
対応バージョンPHP 4以降(PHP 8.0以降は引数の型がXMLParserオブジェクトに変更)
解析方式SAX方式(イベント駆動型、ストリーミング解析)

SAX方式の全体像(イメージ図)

  XML文書
  <catalog>
    <item id="1">Note</item>
  </catalog>
              │
              ▼
        xml_parse() が先頭から順番に読み進める
              │
   ┌──────────┴──────────────────────┐
   │ 要素の開始タグを検出        → 開始タグハンドラを呼び出す │
   │ 文字データを検出            → 文字データハンドラを呼び出す │
   │ 要素の終了タグを検出        → 終了タグハンドラを呼び出す │
   └──────────┬──────────────────────┘
              ▼
  各イベントのたびに、事前に登録した
  コールバック関数(ハンドラ)が実行される

ポイントは、xml_parse()文書全体を一度にメモリ上のツリー構造として保持しないという点です。DOM方式(DOMDocumentなど)と比べてメモリ効率が良く、非常に大きなXMLファイルをストリーミング処理する際に有利です。一方で、要素同士の親子関係を後から自由に辿るような処理には向いていません。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicXmlParserDemo
{
    public function parse(string $xml): bool
    {
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) {
                echo "開始タグ: <{$name}>" . PHP_EOL;
            },
            function ($parser, $name) {
                echo "終了タグ: </{$name}>" . PHP_EOL;
            }
        );

        // 第3引数をtrueにして、これが最後のデータであることを明示する
        $result = xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return (bool) $result;
    }
}

$demo = new BasicXmlParserDemo();
$demo->parse('<root><item>value</item></root>');

例2:文字データも含めて要素の内容を抽出するクラス

<?php

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

    /**
     * 開始タグ・文字データ・終了タグの3種類のハンドラを組み合わせ、
     * "タグ名 => 内容" の連想配列を組み立てる
     */
    public function extract(string $xml): array
    {
        $this->result = [];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name) {
                $this->currentTag = $name;
            },
            fn () => null
        );

        xml_set_character_data_handler($parser, function ($parser, $data) {
            $trimmed = trim($data);
            if ($trimmed !== '') {
                $this->result[$this->currentTag] = $trimmed;
            }
        });

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

        return $this->result;
    }
}

$extractor = new ElementContentExtractor();
print_r($extractor->extract('<product><name>ノート</name><price>300</price></product>'));

例3:属性を含めた要素情報を階層構造として組み立てるクラス

<?php

class HierarchicalXmlParser
{
    private array $stack = [];
    private array $root = [];

    /**
     * 要素のネスト構造をスタックで管理しながら、
     * 属性を含む階層的な配列データに変換する
     */
    public function parse(string $xml): array
    {
        $this->stack = [&$this->root];
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) {
                $node = ['tag' => $name, 'attributes' => $attrs, 'children' => []];
                $this->stack[count($this->stack) - 1]['children'][] = $node;
                $this->stack[] = &$this->stack[count($this->stack) - 1]['children'][count($this->stack[count($this->stack) - 1]['children']) - 1];
            },
            function ($parser, $name) {
                array_pop($this->stack);
            }
        );

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

        return $this->root['children'] ?? [];
    }
}

$parser = new HierarchicalXmlParser();
print_r($parser->parse('<items><item id="1"><name>ノート</name></item></items>'));

例4:大容量XMLファイルをチャンク単位でストリーミング解析するクラス

<?php

class StreamingLargeXmlParser
{
    private int $itemCount = 0;

    /**
     * ファイル全体を一度にメモリに載せず、
     * 8KBずつ読み込みながら逐次解析する(SAX方式の最大の利点)
     */
    public function countItems(string $filePath, string $targetTag): int
    {
        $this->itemCount = 0;
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name) use ($targetTag) {
                if ($name === $targetTag) {
                    $this->itemCount++;
                }
            },
            fn () => null
        );

        $handle = fopen($filePath, 'r');
        while (!feof($handle)) {
            $chunk = fread($handle, 8192);
            // 最後のチャンクかどうかをfeof()で判定して第3引数に渡す
            xml_parse($parser, $chunk, feof($handle));
        }
        fclose($handle);
        xml_parser_free($parser);

        return $this->itemCount;
    }
}

// $parser = new StreamingLargeXmlParser();
// echo $parser->countItems('/path/to/huge_catalog.xml', 'item');

例5:解析失敗時に詳細なエラー情報を含めて例外をスローする堅牢な実装

<?php

class RobustXmlParser
{
    /**
     * xml_parse()の戻り値をチェックし、
     * 失敗時には位置情報付きの例外をスローする
     */
    public function parseStrict(string $xml, callable $onStart, callable $onEnd): void
    {
        $parser = xml_parser_create();
        xml_set_element_handler($parser, $onStart, $onEnd);

        if (!xml_parse($parser, $xml, true)) {
            $code = xml_get_error_code($parser);
            $line = xml_get_current_line_number($parser);
            $message = xml_error_string($code);
            xml_parser_free($parser);

            throw new RuntimeException("XML解析エラー({$line}行目): {$message}");
        }

        xml_parser_free($parser);
    }
}

$parser = new RobustXmlParser();
try {
    $parser->parseStrict(
        '<root><item>value</root>',
        fn ($p, $name, $attrs) => print("開始: {$name}\n"),
        fn ($p, $name) => print("終了: {$name}\n")
    );
} catch (RuntimeException $e) {
    echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}

例6:文字エンコーディングを指定して非UTF-8のXMLを扱うクラス

<?php

class EncodingAwareXmlParser
{
    /**
     * xml_parser_create()の第1引数でエンコーディングを指定し、
     * Shift_JISなど非UTF-8のXMLにも対応する
     */
    public function parseWithEncoding(string $xml, string $encoding = 'UTF-8'): array
    {
        $result = [];
        // 出力エンコーディングをUTF-8に統一する例
        $parser = xml_parser_create($encoding);
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');

        xml_set_character_data_handler($parser, function ($parser, $data) use (&$result) {
            $trimmed = trim($data);
            if ($trimmed !== '') {
                $result[] = $trimmed;
            }
        });

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

        return $result;
    }
}

$parser = new EncodingAwareXmlParser();
print_r($parser->parseWithEncoding('<root><name>テスト</name></root>', 'UTF-8'));

例7:case-foldingオプションを制御してタグ名の大文字小文字を保持する

<?php

class CaseSensitiveXmlParser
{
    /**
     * デフォルトではExpatパーサーは要素名を大文字に変換するが、
     * XML_OPTION_CASE_FOLDINGをオフにすると元の大文字小文字を維持できる
     */
    public function parsePreservingCase(string $xml): array
    {
        $tags = [];
        $parser = xml_parser_create();

        // 大文字小文字を区別したい場合はcase foldingを無効化する
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);

        xml_set_element_handler(
            $parser,
            function ($parser, $name) use (&$tags) {
                $tags[] = $name;
            },
            fn () => null
        );

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

        return $tags;
    }
}

$parser = new CaseSensitiveXmlParser();
print_r($parser->parsePreservingCase('<Root><MyItem>value</MyItem></Root>'));
// ['Root', 'MyItem'] (case foldingが有効な場合は ['ROOT', 'MYITEM'] になる)

関連関数との比較

関数/クラス役割xml_parseとの違い
xml_parse()Expatベースのパーサーで解析を実行本記事の対象。SAX方式(イベント駆動型)でメモリ効率が良い
xml_parser_create()パーサーインスタンスを生成xml_parse()を呼び出す前の準備段階の関数
SimpleXMLElement / simplexml_load_string()XMLをオブジェクトツリーとして読み込むDOM方式。文書全体をメモリに展開し、直感的な操作が可能
DOMDocumentXMLをDOMツリーとして読み込み・操作するDOM方式。より高機能で、XPathやXML編集にも対応
XMLReaderプルベース(pull-parsing)でXMLを読み進めるSAXのプッシュ型と異なり、呼び出し側が能動的に次の要素を取得する方式

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

  1. ハンドラを登録する前にxml_parse()を呼び出してしまう xml_set_element_handler() などでコールバック関数を登録する前に xml_parse() を実行しても、イベントを捕捉できません。必ずハンドラ登録後に解析を開始しましょう。
  2. 第3引数($is_final)の指定を誤る 分割してデータを渡す場合、最後のチャンクで true を指定し忘れると、パーサーが「まだ続きがある」と判断してしまい、本来検出されるべきエラー(閉じタグ忘れなど)が正しく検出されないことがあります(例4を参照)。
  3. 戻り値のチェックを省略する xml_parse() の戻り値(1または0)を確認せずに処理を続けると、不正なXMLを正常なデータとして扱ってしまうバグにつながります。必ず戻り値を確認し、失敗時のエラーハンドリングを実装しましょう。
  4. 要素名が意図せず大文字に変換される Expatパーサーのデフォルト設定では、要素名が自動的に大文字に変換される「case folding」が有効になっています。元の大文字小文字を保持したい場合は、XML_OPTION_CASE_FOLDING オプションを明示的に無効化する必要があります(例7を参照)。
  5. 親子関係の把握にはスタック管理が必要になる SAX方式は「開始・終了イベントの連続」として文書を扱うため、要素同士の階層構造(親子関係)を把握したい場合は、開発者自身がスタック構造などを使って管理する必要があります(例3を参照)。単純な階層データを扱いたいだけであれば、SimpleXMLElement の方が手軽な場合もあります。

まとめ

観点まとめ
何をする関数かExpatベースのパーサーを使い、SAX方式(イベント駆動型)でXML文書を解析する
主な用途大容量XMLファイルのストリーミング処理、メモリ効率を重視したXML解析
解析方式の特徴文書全体をメモリに保持せず、要素の開始・終了イベントごとにコールバックを呼び出す
DOM方式との違いSimpleXMLElementDOMDocumentと異なり、階層構造は自前でスタック管理する必要がある
注意点ハンドラ登録の順序、$is_final引数の正しい指定、要素名のcase folding、戻り値チェックの徹底

xml_parse() は、PHPにおけるXML処理の中でも特にメモリ効率に優れた、実践的な選択肢です。大容量のXMLフィードやログファイルをストリーミング処理したい場合には非常に強力な武器になりますが、SAX方式特有の「自分で状態管理をする」という設計思想を理解した上で活用することが重要です。

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