はじめに
前回の記事では、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 |
| 引数 | $parser — xml_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パーサーの解析状態とは独立している |
よくある落とし穴(注意点)
- ハンドラの外側で呼び出しても意味のある値が得られない
xml_get_current_byte_index()は、要素ハンドラや文字データハンドラの内部で呼び出すことで初めて意味を持つ関数です。xml_parse()の呼び出しが完了した後に呼び出す場合は、主にエラー発生時の位置特定を目的とします。 - マルチバイト文字を含む場合、バイト数と文字数が一致しない 日本語などのマルチバイト文字が含まれるXML文書では、バイトオフセットは実際の「文字数」とは異なります。
substr()で切り出す際は、バイト単位の関数であることを前提に扱いましょう(mb_substr()ではなくsubstr()を使う)。 - ストリーミング解析(分割パース)時の累積バイト数に注意する
xml_parse()を複数回に分けて呼び出す(大容量ファイルをチャンクごとに処理する)場合、バイトインデックスは解析開始からの累積値になります。チャンクごとのオフセットではない点を理解しておく必要があります。 - 戻り値
-1のケースを見落とす 何らかの理由で位置情報が取得できない場合、-1が返ることがあります。位置情報を前提とした処理(例2の抜粋表示など)を行う際は、この値のチェックを入れておくと安全です。 - PHP 8.0以降での型変更に注意する PHP 8.0以降、
xml_parser_create()の戻り値がリソース型からXMLParserオブジェクトに変更されています。型チェックを行っている古いコードでは、この変更に対応した修正が必要になる場合があります。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | XMLパーサーの現在の解析位置を、文書先頭からのバイトオフセットとして取得する |
| 主な用途 | エラー箇所の前後抜粋表示、大容量ファイルの解析進捗計算、特定要素の位置インデックス作成 |
| 他の位置取得関数との違い | 行・列番号よりも、substr()などバイト単位の処理と相性が良い |
| 呼び出しタイミング | 主にハンドラ内部、またはエラー発生直後に呼び出す |
| 注意点 | マルチバイト文字とのバイト数の不一致、分割パース時の累積値であること、-1が返るケース |
xml_get_current_byte_index() は、XML解析中の「今どこを処理しているか」をプログラム的に正確に把握したいときに役立つ関数です。行・列番号による人間向けの位置表示と使い分けながら、エラー箇所の特定や大容量ファイルの処理進捗管理などに活用していきましょう。
