web-dev-qa-db-ja.com

大きなJSONファイルをPHP

やや大きい(おそらく最大2億)JSONファイルを処理しようとしています。ファイルの構造は基本的にオブジェクトの配列です。

したがって、次のようなものがあります。

[
  {"property":"value", "property2":"value2"},
  {"prop":"val"},
  ...
  {"foo":"bar"}
]

各オブジェクトには任意のプロパティがあり、配列内の他のオブジェクトと共有する必要はありません(同じように)。

配列内の各オブジェクトに処理を適用したいのですが、ファイルが巨大になる可能性があるため、メモリ内のファイルコンテンツ全体をスラップして、JSONをデコードし、PHP配列を反復処理することはできません。

したがって、理想的には、ファイルを読み取り、各オブジェクトについて十分な情報をフェッチして処理したいと思います。 JSONで利用できる同様のライブラリがあれば、SAXタイプのアプローチで問題ありません。

この問題に最もよく対処する方法について何か提案はありますか?

イベントベースのパーサーに取り組むことにしました。まだ完了しておらず、満足のいくバージョンを公開すると、私の作品へのリンクが付いた質問が編集されます。

編集:

私はついに私が満足しているパーサーのバージョンを作り上げました。 GitHubで入手できます。

https://github.com/kuma-giyomu/JSONParser

おそらく改善の余地があり、フィードバックを歓迎しています。

ストリーミングJSONプルパーサーを作成しました pcrov/JsonReader for PHP 7 with api based on XMLReader

イベントベースのパーサーとは大きく異なり、コールバックを設定してパーサーに処理を任せる代わりに、パーサーのメソッドを呼び出して、必要に応じてデータを移動または取得します。必要なビットが見つかり、解析を停止したいですか?次に、構文解析を停止します(そして、それは素晴らしいことなので、close()を呼び出します)。

(プルとイベントベースのパーサーの概要については、 XMLリーダーモデル:SAXとXMLプルパーサー 。)を参照してください。


例1:

JSONから各オブジェクト全体を読み取ります。

use pcrov\JsonReader\JsonReader;

$reader = new JsonReader();
$reader->open("data.json");

$reader->read(); // Outer array.
$depth = $reader->depth(); // Check in a moment to break when the array is done.
$reader->read(); // Step to the first object.
do {
    print_r($reader->value()); // Do your thing.
} while ($reader->next() && $reader->depth() > $depth); // Read each sibling.

$reader->close();

出力:

Array
(
    [property] => value
    [property2] => value2
)
Array
(
    [prop] => val
)
Array
(
    [foo] => bar
)

有効なJSONがPHPオブジェクトで許可されていないプロパティ名を生成するエッジケースのために、オブジェクトは文字列キーの配列として返されます。これらの競合を回避することは、 anemic stdClassオブジェクトは、とにかく単純な配列に値をもたらしません。


例2:

名前付きの各要素を個別に読み取ります。

$reader = new pcrov\JsonReader\JsonReader();
$reader->open("data.json");

while ($reader->read()) {
    $name = $reader->name();
    if ($name !== null) {
        echo "$name: {$reader->value()}\n";
    }
}

$reader->close();

出力:

property: value
property2: value2
prop: val
foo: bar

例3:

指定された名前の各プロパティを読み取ります。ボーナス:URIの代わりに文字列から読み取り、さらに同じオブジェクト内の重複した名前を持つプロパティからデータを取得します(これは、JSONで許可されています。とても楽しいです)。

$json = <<<'JSON'
[
    {"property":"value", "property2":"value2"},
    {"foo":"foo", "foo":"bar"},
    {"prop":"val"},
    {"foo":"baz"},
    {"foo":"quux"}
]
JSON;

$reader = new pcrov\JsonReader\JsonReader();
$reader->json($json);

while ($reader->read("foo")) {
    echo "{$reader->name()}: {$reader->value()}\n";
}

$reader->close();

出力:

foo: foo
foo: bar
foo: baz
foo: quux

JSONをどの程度正確に読み取るかは、JSONの構造とJSONで何をしたいかによって異なります。これらの例は、開始する場所を提供するはずです。

15
user3942918

これは、大きなJSONドキュメントを処理するためのシンプルなストリーミングパーサーです。非常に大きなJSONドキュメントを解析して、すべてをメモリにロードしないようにするために使用します。これは、PHPの他のすべてのJSONパーサーが機能する方法です。

https://github.com/salsify/jsonstreamingparser

2
Aaron Averill

このようなものが存在しますが、 C++ および Java の場合のみです。 PHPからこれらのライブラリのいずれかにアクセスできない限り、PHPですが、私が知る限り、json_read()にはこれの実装はありません。ただし、jsonがそのように単純に構造化されている場合、次の_}_までファイルを読み取り、json_read()を介して受信したJSONを処理するのは簡単です。ただし、見つからない場合は、10kbを読み取り、}で分割するなど、バッファリングした方がよいでしょう。 、さらに10kを読み取り、それ以外の場合は見つかった値を処理します。次に、次のブロックを読み取ります。

2
joni

最近、予想外に大きなJSONファイルを効率的に解析するJSONMachineというライブラリを作成しました。使用法は単純なforeachを介して行われます。私は自分でプロジェクトに使用しています。

例:

foreach (JsonMachine::fromFile('employees.json') as $employee) {
    $employee['name']; // etc
}

https://github.com/halaxa/json-machine を参照してください

2
Filip Halaxa

http://github.com/sfalvo/php-yajl/ 私はそれを自分で使用しませんでした。

0
Alex Jasmin

JSONストリーミングパーサー https://github.com/salsify/jsonstreamingparser はすでに言及されていることを私は知っています。しかし、最近(ish)に新しいリスナーを追加して、箱から出してすぐに使いやすくしようと思ったので、(変更のために)それが何をするのかについていくつかの情報を出すと思いました...

https://www.salsify.com/blog/engineering/json-streaming-parser-for-php に基本的なパーサーについての非常に良い記事がありますが、私が抱えている問題は標準的な設定では、ファイルを処理するために常にリスナーを作成する必要がありました。これは必ずしも単純なタスクではなく、JSONが変更された場合は、ある程度のメンテナンスが必要になることもあります。だから私はRegexListenerを書きました。

基本的な原則は、(正規表現を介して)関心のある要素を指定し、データが見つかったときに何をすべきかを指定するコールバックを提供できるようにすることです。 JSONを読み取っている間、ディレクトリ構造と同様に、各コンポーネントへのパスを追跡します。したがって、/name/forenameまたは配列の場合は/items/item/2/partid-これは正規表現が一致するものです。

例は( githubのソース から)...

$filename = __DIR__.'/../tests/data/example.json';
$listener = new RegexListener([
    '/1/name' => function ($data): void {
        echo PHP_EOL."Extract the second 'name' element...".PHP_EOL;
        echo '/1/name='.print_r($data, true).PHP_EOL;
    },
    '(/\d*)' => function ($data, $path): void {
        echo PHP_EOL."Extract each base element and print 'name'...".PHP_EOL;
        echo $path.'='.$data['name'].PHP_EOL;
    },
    '(/.*/nested array)' => function ($data, $path): void {
        echo PHP_EOL."Extract 'nested array' element...".PHP_EOL;
        echo $path.'='.print_r($data, true).PHP_EOL;
    },
]);
$parser = new Parser(fopen($filename, 'r'), $listener);
$parser->parse();

ほんの2、3の説明...

'/1/name' => function ($data)

したがって、/1は配列の2番目の要素(0ベース)であるため、これにより要素の特定のインスタンスにアクセスできます。 /namename要素です。次に、値は$dataとしてクロージャに渡されます。

"(/\d*)" => function ($data, $path )

これにより、配列の各要素が選択され、一度に1つずつ渡されます。これは、キャプチャグループを使用しているため、この情報は$pathとして渡されます。つまり、レコードのセットがファイルに存在する場合、各アイテムを一度に1つずつ処理できます。また、追跡する必要なしにどの要素を知っています。

最後のもの

'(/.*/nested array)' => function ($data, $path):

nested arrayと呼ばれる要素を効果的にスキャンし、ドキュメント内の場所とともに各要素を渡します。

私が見つけたもう1つの便利な機能は、大きなJSONファイルで、上部に要約の詳細が必要な場合は、それらのビットを取得して停止できることです...

$filename = __DIR__.'/../tests/data/ratherBig.json';
$listener = new RegexListener();
$parser = new Parser(fopen($filename, 'rb'), $listener);
$listener->setMatch(["/total_rows" => function ($data ) use ($parser) {
    echo "/total_rows=".$data.PHP_EOL;
    $parser->stop();
}]);

これにより、残りのコンテンツに興味がないときに時間を節約できます。

注意すべき点の1つは、これらはコンテンツに反応するため、一致するコンテンツの終わりが見つかったときにそれぞれがトリガーされ、さまざまな順序になる可能性があることです。ただし、パーサーは関心のあるコンテンツのみを追跡し、それ以外は破棄します。

興味深い機能(バグとして恐ろしく知られていることもあります)を見つけた場合は、githubページで問題を通知または報告してください。

0
Nigel Ren