[PHP]xml_get_current_byte_indexとは?XML解析中の現在位置をバイト単位で取得する方法を徹底解説

PHP

はじめに

前回の記事では、XMLパーサーのエラーコードを人間が読める文字列に変換する xml_error_string() を解説しました。今回紹介する xml_get_current_byte_index() は、同じくExpatベースのXMLパーサー関連関数の一つで、現在パーサーが処理している位置を、XML文書全体の先頭からのバイトオフセットとして取得するための関数です。

行番号や列番号による位置特定(xml_get_current_line_number()xml_get_current_column_number())は人間にとって分かりやすい一方、プログラム的にXML文書中の正確な位置を特定したい場合(該当箇所の抜粋表示、大容量ファイルのシーク位置の記録など)には、バイト単位のオフセットの方が扱いやすいことがあります。本記事では、この関数の基本的な使い方と実践的な活用例を詳しく解説します。


関数概要

項目内容
関数名xml_get_current_byte_index()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_get_current_byte_index(XMLParser $parser): int
引数$parserxml_parser_create() で生成したパーサーインスタンス
戻り値現在の解析位置を表すバイトオフセット(0始まり)。取得できない場合は -1
対応バージョンPHP 4以降(PHP 8.0以降は引数の型がXMLParserオブジェクトに変更)
関連関数xml_get_current_line_number()xml_get_current_column_number()

位置情報の全体像(イメージ図)

  XML文書
  "<root>\n  <item>value</item>\n</root>"
              │
              │ ハンドラ内やエラー発生時に呼び出す
              ▼
  ┌──────────────────────────────┐
  │ xml_get_current_line_number()    │ → 2 (何行目か)
  │ xml_get_current_column_number()  │ → 8 (その行の何文字目か)
  │ xml_get_current_byte_index()     │ → 15 (文書全体の先頭から何バイト目か)★本記事の対象
  └──────────────────────────────┘

ポイントは、これら3つの「現在位置取得系」の関数がそれぞれ異なる粒度で同じ「現在位置」を表現しているという点です。行・列番号は人間にとって読みやすいエラーメッセージに向いていますが、バイトオフセットは substr() などを使って元の文字列から該当箇所を直接切り出したい場合に特に有用です。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicByteIndexDemo
{
    public function reportPosition(string $xml): void
    {
        $parser = xml_parser_create();

        xml_set_character_data_handler($parser, function ($parser, $data) {
            // データを受け取るたびに、その時点でのバイト位置を確認する
            $index = xml_get_current_byte_index($parser);
            echo "位置 {$index}: " . trim($data) . PHP_EOL;
        });

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

$demo = new BasicByteIndexDemo();
$demo->reportPosition('<root><item>hello</item><item>world</item></root>');

例2:エラー発生箇所の前後をバイト単位で抜粋表示するツール

<?php

class XmlErrorContextExtractor
{
    /**
     * エラー発生地点のバイト位置を使って、
     * 元のXML文字列から問題箇所の前後を切り出して表示する
     */
    public function extractErrorContext(string $xml, int $contextRadius = 20): ?string
    {
        $parser = xml_parser_create();

        if (xml_parse($parser, $xml, true)) {
            xml_parser_free($parser);
            return null; // エラーなし
        }

        $byteIndex = xml_get_current_byte_index($parser);
        $errorMessage = xml_error_string(xml_get_error_code($parser));
        xml_parser_free($parser);

        $start = max(0, $byteIndex - $contextRadius);
        $length = $contextRadius * 2;
        $snippet = substr($xml, $start, $length);

        return "エラー: {$errorMessage}\n該当箇所付近: ...{$snippet}...";
    }
}

$extractor = new XmlErrorContextExtractor();
echo $extractor->extractErrorContext('<root><item>value</root>') . PHP_EOL;

例3:大容量XMLファイルのストリーム解析における進捗率計算

<?php

class LargeXmlProgressTracker
{
    /**
     * ファイルサイズとバイトオフセットを比較することで、
     * 大容量XMLファイルの解析進捗をパーセンテージで把握する
     */
    public function parseWithProgress(string $filePath): void
    {
        $fileSize = filesize($filePath);
        $parser = xml_parser_create();
        $handle = fopen($filePath, 'r');

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) use ($fileSize) {
                $currentByte = xml_get_current_byte_index($parser);
                $progress = $fileSize > 0 ? round(($currentByte / $fileSize) * 100, 1) : 0;
                echo "\r解析進捗: {$progress}% ({$name})";
            },
            fn () => null
        );

        while ($data = fread($handle, 8192)) {
            xml_parse($parser, $data, feof($handle));
        }

        fclose($handle);
        xml_parser_free($parser);
        echo "\n完了しました。\n";
    }
}

// $tracker = new LargeXmlProgressTracker();
// $tracker->parseWithProgress('/path/to/large_file.xml');

例4:バイト位置から行・列番号に変換する補助クラス

<?php

class BytePositionConverter
{
    /**
     * xml_get_current_byte_index()の結果と、
     * xml_get_current_line_number()/column_number()を
     * 1つの構造化データとしてまとめる
     */
    public function getFullPosition($parser): array
    {
        return [
            'byte_index' => xml_get_current_byte_index($parser),
            'line'       => xml_get_current_line_number($parser),
            'column'     => xml_get_current_column_number($parser),
        ];
    }
}

$parser = xml_parser_create();
$converter = new BytePositionConverter();

xml_set_element_handler(
    $parser,
    function ($parser, $name) use ($converter) {
        print_r($converter->getFullPosition($parser));
    },
    fn () => null
);

xml_parse($parser, '<root><item>value</item></root>', true);
xml_parser_free($parser);

例5:ログシステムに位置情報を含めた詳細エラーレポートを記録する

<?php

class DetailedXmlErrorLogger
{
    public function __construct(private string $logFilePath)
    {
    }

    /**
     * バイト位置を含む詳細な情報をログに記録し、
     * 後で問題箇所をピンポイントで特定できるようにする
     */
    public function logParseError(string $xml, string $sourceLabel): void
    {
        $parser = xml_parser_create();

        if (!xml_parse($parser, $xml, true)) {
            $entry = sprintf(
                "[%s] source=%s byte=%d line=%d column=%d message=%s\n",
                date('Y-m-d H:i:s'),
                $sourceLabel,
                xml_get_current_byte_index($parser),
                xml_get_current_line_number($parser),
                xml_get_current_column_number($parser),
                xml_error_string(xml_get_error_code($parser))
            );
            file_put_contents($this->logFilePath, $entry, FILE_APPEND);
        }

        xml_parser_free($parser);
    }
}

$logger = new DetailedXmlErrorLogger('/tmp/xml_detailed_errors.log');
$logger->logParseError('<root><a></root>', 'partner_feed');

例6:特定要素の出現バイト位置を記録するインデックス作成ツール

<?php

class ElementByteIndexer
{
    private array $index = [];

    /**
     * 特定のタグ名がXML文書中のどのバイト位置に出現するかを
     * インデックス化しておくことで、後から高速にアクセスできるようにする
     */
    public function buildIndex(string $xml, string $targetTag): array
    {
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($parser, $name, $attrs) use ($targetTag) {
                if ($name === $targetTag) {
                    $this->index[] = xml_get_current_byte_index($parser);
                }
            },
            fn () => null
        );

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

        return $this->index;
    }
}

$indexer = new ElementByteIndexer();
$positions = $indexer->buildIndex(
    '<catalog><item>A</item><item>B</item><item>C</item></catalog>',
    'item'
);
print_r($positions);

例7:xml_get_current_byte_indexとxml_get_current_line_numberの違いを比較する

<?php

class PositionMetricsComparator
{
    /**
     * 複数行にまたがるXMLに対して、
     * バイト位置と行・列番号の違いを実際に確認する
     */
    public function compare(string $xml): array
    {
        $records = [];
        $parser = xml_parser_create();

        xml_set_character_data_handler($parser, function ($parser, $data) use (&$records) {
            if (trim($data) === '') {
                return;
            }
            $records[] = [
                'data'   => trim($data),
                'byte'   => xml_get_current_byte_index($parser),
                'line'   => xml_get_current_line_number($parser),
                'column' => xml_get_current_column_number($parser),
            ];
        });

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

        return $records;
    }
}

$comparator = new PositionMetricsComparator();
print_r($comparator->compare("<root>\n  <a>first</a>\n  <b>second</b>\n</root>"));

関連関数との比較

関数役割xml_get_current_byte_indexとの違い
xml_get_current_byte_index()現在位置を文書先頭からのバイトオフセットで取得本記事の対象。substr()などでの箇所切り出しに使いやすい
xml_get_current_line_number()現在位置を行番号で取得人間が読みやすいエラーメッセージ向けの粒度
xml_get_current_column_number()現在位置を行内の列番号で取得同上、より細かい位置の特定に使う
xml_error_string()エラーコードを説明文字列に変換位置情報ではなく、エラーの「内容」を扱う関数
ftell()ファイルポインタの現在位置を取得ファイルストリームレベルでの位置取得であり、XMLパーサーの解析状態とは独立している

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

  1. ハンドラの外側で呼び出しても意味のある値が得られない xml_get_current_byte_index() は、要素ハンドラや文字データハンドラの内部で呼び出すことで初めて意味を持つ関数です。xml_parse() の呼び出しが完了した後に呼び出す場合は、主にエラー発生時の位置特定を目的とします。
  2. マルチバイト文字を含む場合、バイト数と文字数が一致しない 日本語などのマルチバイト文字が含まれるXML文書では、バイトオフセットは実際の「文字数」とは異なります。substr() で切り出す際は、バイト単位の関数であることを前提に扱いましょう(mb_substr()ではなくsubstr()を使う)。
  3. ストリーミング解析(分割パース)時の累積バイト数に注意する xml_parse() を複数回に分けて呼び出す(大容量ファイルをチャンクごとに処理する)場合、バイトインデックスは解析開始からの累積値になります。チャンクごとのオフセットではない点を理解しておく必要があります。
  4. 戻り値-1のケースを見落とす 何らかの理由で位置情報が取得できない場合、-1 が返ることがあります。位置情報を前提とした処理(例2の抜粋表示など)を行う際は、この値のチェックを入れておくと安全です。
  5. PHP 8.0以降での型変更に注意する PHP 8.0以降、xml_parser_create() の戻り値がリソース型から XMLParser オブジェクトに変更されています。型チェックを行っている古いコードでは、この変更に対応した修正が必要になる場合があります。

まとめ

観点まとめ
何をする関数かXMLパーサーの現在の解析位置を、文書先頭からのバイトオフセットとして取得する
主な用途エラー箇所の前後抜粋表示、大容量ファイルの解析進捗計算、特定要素の位置インデックス作成
他の位置取得関数との違い行・列番号よりも、substr()などバイト単位の処理と相性が良い
呼び出しタイミング主にハンドラ内部、またはエラー発生直後に呼び出す
注意点マルチバイト文字とのバイト数の不一致、分割パース時の累積値であること、-1が返るケース

xml_get_current_byte_index() は、XML解析中の「今どこを処理しているか」をプログラム的に正確に把握したいときに役立つ関数です。行・列番号による人間向けの位置表示と使い分けながら、エラー箇所の特定や大容量ファイルの処理進捗管理などに活用していきましょう。

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