對於 PHP >=7.2 的大 JSON 檔案或串流的低效迭代,非常易於使用和記憶體高效的直接替換。參見 TL;DR。除了可選的ext-json
之外,生產中沒有任何依賴項。 README 與程式碼同步
1.2.0
版本中的新功能 - 遞歸迭代
<?php
use JsonMachineItems;
// this often causes Allowed Memory Size Exhausted,
// because it loads all the items in the JSON into memory
- $users = json_decode(file_get_contents('500MB-users.json'));
// this has very small memory footprint no matter the file size
// because it loads items into memory one by one
+ $users = Items::fromFile('500MB-users.json');
foreach ($users as $id => $user) {
// just process $user as usual
var_dump($user->name);
}
像$users[42]
這樣的隨機存取尚不可能。使用上面提到的foreach
並尋找項目或使用 JSON Pointer。
透過iterator_count($users)
計算項目數。請記住,它仍然需要在內部迭代整個過程才能獲得計數,因此所需的時間與迭代和手動計數大約相同。
如果開箱即用,則需要ext-json
但如果使用自訂解碼器則不需要。請參閱解碼器。
遵循變更日誌。
JSON Machine 是一個高效、易於使用且快速的 JSON 流/拉動/增量/惰性(無論你怎麼稱呼它)解析器,基於為不可預測的長 JSON 流或文件開發的生成器。主要特點是:
foreach
迭代任意大小的 JSON 即可。沒有事件和回調。json_decode
來解碼 JSON 文檔項目。請參閱解碼器。假設fruits.json
包含這個巨大的 JSON 文件:
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
可以這樣解析:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' );
foreach ( $ fruits as $ name => $ data ) {
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
解析 json 數組而不是 json 物件遵循相同的邏輯。 foreach 中的按鍵是項目的數字索引。
如果您希望 JSON Machine 傳回數組而不是對象,請使用new ExtJsonDecoder(true)
作為解碼器。
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
如果您只想迭代此fruits.json
中的results
子樹:
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
使用 JSON Pointer /results
作為pointer
選項:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /results ' ]);
foreach ( $ fruits as $ name => $ data ) {
// The same as above, which means:
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
筆記:
results
的值不會立即載入到記憶體中,而是一次只載入results
中的一項。在您目前迭代的層級/子樹上,記憶體中每次始終只有一項。因此,記憶體消耗是恆定的。
JSON 指標規格也允許使用連字號 ( -
) 而不是特定的陣列索引。 JSON Machine 將其解釋為匹配任何數組索引(而不是任何物件鍵)的通配符。這使您能夠迭代數組中的嵌套值,而無需加載整個專案。
例子:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
若要迭代水果的所有顏色,請使用 JSON 指標"/results/-/color"
。
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruitsArray.json ' , [ ' pointer ' => ' /results/-/color ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ key == ' color ' ;
$ value == ' red ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/0/color ' ;
// 2nd iteration:
$ key == ' color ' ;
$ value == ' yellow ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/1/color ' ;
}
您可以像解析集合一樣解析文件中任何位置的單一標量值。考慮這個例子:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
取得lastModified
鍵的標量值,如下所示:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st and final iteration:
// $key === 'lastModified'
// $value === '2012-12-12'
}
當解析器找到該值並將其提供給您時,它會停止解析。因此,當單一標量值位於千兆位元組大小的檔案或流的開頭時,它會立即從開頭獲取該值,並且幾乎不消耗任何記憶體。
明顯的捷徑是:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
單標量值存取也支援 JSON 指標中的陣列索引。
也可以使用多個 JSON 指標來解析多個子樹。考慮這個例子:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"berries" : [
{
"name" : " strawberry " , // not a berry, but whatever ...
"color" : " red "
},
{
"name" : " raspberry " , // the same ...
"color" : " red "
}
],
"citruses" : [
{
"name" : " orange " ,
"color" : " orange "
},
{
"name" : " lime " ,
"color" : " green "
}
]
}
若要迭代所有莓果和柑橘類水果,請使用 JSON 指標["/berries", "/citrus"]
。指針的順序並不重要。這些項目將按照文件中出現的順序進行迭代。
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [
' pointer ' => [ ' /berries ' , ' /citruses ' ]
]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ value == [ " name " => " strawberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 2nd iteration:
$ value == [ " name " => " raspberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 3rd iteration:
$ value == [ " name " => " orange " , " color " => " orange " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
// 4th iteration:
$ value == [ " name " => " lime " , " color " => " green " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
}
當 JSON 結構很難甚至無法使用Items
和 JSON 指標處理,或者您迭代的單一項目太大而無法處理時,請使用RecursiveItems
而不是Items
。另一方面,它明顯比Items
慢,所以請記住這一點。
當RecursiveItems
遇到 JSON 中的列表或字典時,它會傳回自身的一個新實例,然後可以對其進行迭代並重複循環。因此,它從不傳回 PHP 數組或對象,而僅傳回標量值或RecursiveItems
。 JSON 字典或列表都不會一次完全載入到記憶體中。
讓我們來看一個有很多很多用戶和很多很多朋友的例子:
// users.json
[
{
"username" : " user " ,
"e-mail" : " [email protected] " ,
"friends" : [
{
"username" : " friend1 " ,
"e-mail" : " [email protected] "
},
{
"username" : " friend2 " ,
"e-mail" : " [email protected] "
}
]
}
]
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user as $ field => $ value ) {
if ( $ field === ' friends ' ) {
/** @var $value RecursiveItems */
foreach ( $ value as $ friend ) {
/** @var $friend RecursiveItems */
foreach ( $ friend as $ friendField => $ friendValue ) {
$ friendField == ' username ' ;
$ friendValue == ' friend1 ' ;
}
}
}
}
}
如果您中斷這種惰性更深層的迭代(即您通過
break
跳過一些"friends"
)並前進到下一個值(即下一個user
),您將無法稍後對其進行迭代。 JSON Machine 必須在背景迭代它才能讀取下一個值。這種嘗試將導致關閉生成器異常。
RecursiveItems
的便利方法toArray(): array
如果您確定 RecursiveItems 的某個實例指向記憶體可管理的資料結構(例如 $friend),您可以呼叫$friend->toArray()
,該項目將具體化為純 PHP 陣列。
advanceToKey(int|string $key): scalar|RecursiveItems
在集合中搜尋特定鍵(例如, $user
中的'friends'
)時,不需要使用循環和條件來搜尋它。相反,您可以簡單地呼叫$user->advanceToKey("friends")
。它將為您迭代並返回該鍵的值。呼叫可以被連結。它還支援類似數組的語法,用於前進並獲取後續索引。因此$user['friends']
將是$user->advanceToKey('friends')
的別名。呼叫可以被連結。請記住,它只是一個別名 - 在RecursiveItems
上直接使用它後,您將無法隨機存取先前的索引。它只是一個語法糖。如果您需要隨機存取記錄/項目上的索引,請使用toArray()
。
因此,前面的範例可以簡化如下:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user [ ' friends ' ] as $ friend ) { // or $user->advanceToKey('friends')
/** @var $friend RecursiveItems */
$ friendArray = $ friend -> toArray ();
$ friendArray [ ' username ' ] === ' friend1 ' ;
}
}
連結可讓您執行以下操作:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
因此,您可以使用 PHP 的內建工具來處理RecursiveIterator
,如下所示:
這是一種尋址 JSON 文件中的一項的方法。請參閱 JSON Pointer RFC 6901。因此,您只需指定指向要迭代的 JSON 陣列或物件(甚至標量值)的指標即可。當解析器命中您指定的集合時,迭代開始。您可以將其作為所有Items::from*
函數中的pointer
選項傳遞。如果指定指向文件中不存在的位置的指針,則會引發異常。它也可用於存取標量值。 JSON 指標本身必須是有效的 JSON 字串。參考標記(斜線之間的部分)的字面比較是針對 JSON 文件鍵/成員名稱執行的。
一些例子:
JSON 指標值 | 將迭代 |
---|---|
(空字串 - 預設) | ["this", "array"] 或{"a": "this", "b": "object"} 將會被迭代(主層級) |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (支援數組索引) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (連字符作為數組索引通配符) |
/ (陷阱! - 斜線後跟空字串,請參閱規格) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
選項可能會更改 JSON 的解析方式。選項數組是所有Items::from*
函數的第二個參數。可用選項有:
pointer
- 一個 JSON 指標字串,告訴您要迭代文件的哪一部分。decoder
- ItemDecoder
介面的實例。debug
- true
或false
以啟用或停用偵錯模式。啟用偵錯模式後,文件中的行、列和位置等資料在解析期間或異常時可用。保持調試禁用會增加輕微的效能優勢。 流 API 回應或任何其他 JSON 流的解析方式與檔案完全相同。唯一的區別是,您使用Items::fromStream($streamResource)
來實現,其中$streamResource
是帶有 JSON 文件的串流資源。其餘的與解析文件相同。以下是支援串流響應的流行 http 客戶端的一些範例:
Guzzle 使用自己的流,但可以透過呼叫GuzzleHttpPsr7StreamWrapper::getResource()
將它們轉換回 PHP 流。將此函數的結果傳遞給Items::fromStream
函數,然後您就完成了設定。請參閱工作 GuzzleHttp 範例。
Symfony HttpClient 的串流響應可作為迭代器。而且由於 JSON Machine 是基於迭代器,因此與 Symfony HttpClient 的整合非常簡單。請參閱 HttpClient 範例。
debug
)大文檔可能需要一段時間才能解析。在foreach
中呼叫Items::getPosition()
來取得從頭開始處理的目前位元組數。百分比很容易計算為position / total * 100
。要了解文件的總大小(以位元組為單位),您可能需要檢查:
strlen($document)
如果解析字串filesize($file)
如果解析文件Content-Length
http 標頭如果停用debug
, getPosition()
始終回傳0
。
<?php
use JsonMachine Items ;
$ fileSize = filesize ( ' fruits.json ' );
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' debug ' => true ]);
foreach ( $ fruits as $ name => $ data ) {
echo ' Progress: ' . intval ( $ fruits -> getPosition () / $ fileSize * 100 ) . ' % ' ;
}
Items::from*
函數也接受decoder
選項。它必須是JsonMachineJsonDecoderItemDecoder
的實例。如果未指定,則預設使用ExtJsonDecoder
。它需要ext-json
PHP 擴充存在,因為它使用json_decode
。當json_decode
不能滿足您的要求時,請實作JsonMachineJsonDecoderItemDecoder
並製作您自己的。
ExtJsonDecoder
-預設。使用json_decode
來解碼鍵和值。建構子與json_decode
具有相同的參數。
PassThruDecoder
- 不進行解碼。鍵和值均以純 JSON 字串的形式產生。當您想要直接在 foreach 中使用其他內容解析 JSON 項目並且不想實作JsonMachineJsonDecoderItemDecoder
時很有用。從1.0.0
開始不使用json_decode
。
例子:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
- 一個裝飾器,它將解碼錯誤包裝在DecodingError
物件中,使您能夠跳過格式錯誤的項目,而不是因SyntaxError
異常而死亡。例子: <?php
use JsonMachine Items ;
use JsonMachine JsonDecoder DecodingError ;
use JsonMachine JsonDecoder ErrorWrappingDecoder ;
use JsonMachine JsonDecoder ExtJsonDecoder ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ErrorWrappingDecoder ( new ExtJsonDecoder ())]);
foreach ( $ items as $ key => $ item ) {
if ( $ key instanceof DecodingError || $ item instanceof DecodingError) {
// handle error of this malformed json item
continue ;
}
var_dump ( $ key , $ item );
}
從 0.4.0 開始,每個異常都會擴展JsonMachineException
,因此您可以捕獲它以過濾 JSON Machine 庫中的任何錯誤。
如果 json 流中的任何位置發生錯誤,則會引發SyntaxError
異常。這非常不方便,因為如果一個 json 專案中有錯誤,您將無法解析文件的其餘部分,因為其中一個項目格式錯誤。 ErrorWrappingDecoder
是一個解碼器裝飾器,可以幫助您解決這個問題。用它包裝一個解碼器,您正在迭代的所有格式錯誤的項目都將透過DecodingError
在 foreach 中提供給您。這樣您就可以跳過它們並繼續閱讀文件。請參閱可用解碼器中的範例。不過,迭代項之間的 json 流結構中的語法錯誤仍然會拋出SyntaxError
異常。
時間複雜度始終為O(n)
TL;DR:記憶體複雜度為O(2)
JSON Machine 一次讀取流(或檔案)1 個 JSON 項,並一次產生對應的 1 個 PHP 項。這是最有效的方法,因為如果您在 JSON 檔案中有 10,000 個用戶並希望使用json_decode(file_get_contents('big.json'))
解析它,那麼您將在記憶體中擁有整個字串以及所有 10,000 個用戶PHP 結構。下表顯示了差異:
一次在記憶體中儲存字串項 | 一次在記憶體中解碼 PHP 項目 | 全部的 | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::from*() | 1 | 1 | 2 |
這意味著,JSON Machine 對於任何大小的已處理 JSON 都始終有效率。 100GB沒問題。
TL;DR:記憶體複雜度為O(n+1)
還有一個方法Items::fromString()
。如果你被迫解析一個大字串,且流不可用,JSON Machine 可能比json_decode
更好。原因是,與json_decode
不同,JSON Machine 仍然一次遍歷 JSON 字串一項,並且不會一次將所有產生的 PHP 結構載入到記憶體中。
讓我們繼續使用 10,000 個使用者的範例。這次它們都在記憶體中以字串形式存在。當使用json_decode
解碼該字串時,會在記憶體中建立 10,000 個陣列(物件),然後傳回結果。另一方面,JSON Machine 為字串中找到的每個項目建立單一結構並將其傳回給您。當您處理此項目並迭代到下一項時,將建立另一個單一結構。這與流/文件的行為相同。下表闡述了這個概念:
一次在記憶體中儲存字串項 | 一次在記憶體中解碼 PHP 項目 | 全部的 | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::fromString() | 10000 | 1 | 10001 |
現實情況甚至更好。 Items::fromString
消耗的記憶體比json_decode
少約 5 倍。原因是 PHP 結構比其對應的 JSON 表示佔用更多的記憶體。
原因之一可能是您想要迭代的項目位於某些子鍵(例如"results"
中,但您忘記指定 JSON 指標。請參閱解析子樹。
另一個原因可能是,您迭代的其中一項本身太大,無法立即解碼。例如,您迭代用戶,其中一個用戶中有數千個「朋友」物件。最有效的解決方案是使用遞歸迭代。
這可能意味著單一 JSON 標量字串本身太大,無法放入記憶體。例如非常大的 base64 編碼檔。在這種情況下,在 JSON Machine 支援將標量值產生為 PHP 流之前,您可能仍然會運氣不佳。
composer require halaxa/json-machine
複製或下載此儲存庫並將以下內容新增至您的引導檔案:
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
克隆這個儲存庫。該庫支援兩種開發方法:
在專案目錄中執行composer run -l
以查看可用的開發腳本。這樣您就可以運行建置流程的某些步驟,例如測試。
安裝 Docker 並在主機上的專案目錄中執行make
以查看可用的開發工具/命令。您可以單獨運行建置流程的所有步驟,也可以一次執行整個建置流程。 Make 基本上在後台容器內執行 Composer 開發腳本。
make build
:運行完整的建置。透過 GitHub Actions CI 執行相同的指令。
你喜歡這個圖書館嗎?給它加註星標、分享它、展示它:) 非常歡迎提出問題和拉取請求。
阿帕契2.0
齒輪元素:由 TutsPlus 從 www.flaticon.com 製作的圖示已獲得 CC 3.0 BY 許可
使用 markdown-toc 產生的目錄