這是第一個 PHP 中的 parquet 檔案格式讀取器/寫入器實現,基於 Apache 基金會提供的 Thrift 原始碼。程式碼和概念的大量部分已從 parquet-dotnet 移植(請參閱 https://github.com/elastacloud/parquet-dotnet 和 https://github.com/aloneguid/parquet-dotnet)。因此,感謝 Ivan Gavryliuk (https://github.com/aloneguid)。
該套件使您能夠讀取和寫入 Parquet 檔案/串流,而無需使用奇異的外部擴充功能(除非您想使用奇異的壓縮方法)。關於核心功能,它與 parquet-dotnet 具有(幾乎?) 100% 的測試相容性,透過 PHPUnit 完成。
該儲存庫(以及 Packagist 上的相關套件)是jocoon/parquet
的官方專案延續。由於各種改進和重要的錯誤修復,在codename/parquet
中,強烈建議不要使用舊套件。
對於這個套件的某些部分,必須發明一些新的模式,因為我還沒有找到任何滿足要求的實作。對於大多數情況,根本沒有任何可用的實作。
一些亮點:
我開始開發這個函式庫是因為 PHP 根本沒有實作。
在我的公司,我們需要一個快速的解決方案,以一種仍然可查詢、從架構角度可擴展且具有容錯能力的格式來歸檔資料庫中的大量資料。我們開始測試透過 AWS DMS 到 S3 的即時“遷移”,由於記憶體限制,最終在某些資料量上崩潰。而且它太面向資料庫了,而且很容易意外刪除先前載入的資料。由於我們擁有高度面向 SDS 且與平台無關的架構,因此將資料儲存為資料庫的 1:1 克隆(如轉儲)並不是我的首選方式。相反,我希望能夠像我想要的那樣動態存儲數據,就像 DMS 導出到 S3 一樣。最終,該項目因上述原因而夭折。
但我無法將鑲木地板格式從我的腦海中抹去。
TOP 1 搜尋結果(https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) 看起來很有希望,不需要那麼多努力就可以得到一個 PHP 實作 - 但事實上,它確實需要一些時間(大約 2 週的非連續工作)。對我來說,作為一名 PHP 和 C# 開發人員,parquet-dotnet 是一個完美的起點 - 不僅僅是因為基準測試太引人注目了。但我預計 PHP 實作不會達到這些效能水平,因為這是一個初步實現,展示了原理。此外,以前沒有人這樣做過。
由於PHP在Web相關專案中佔有巨大份額,因此在大數據應用和場景需求不斷增長的情況下,這是必須的。出於我個人的動機,這是一種表明 PHP 已經(物理上、虛擬上?)超越其「腳本語言」聲譽的方式。我認為——或者至少我希望——有人會從這個包及其傳遞的訊息中受益。不僅僅是 Thrift 物件。雙關語的意思。
您需要幾個擴充功能才能充分使用該庫。
該庫最初是針對/使用 PHP 7.3 開發的,但它應該在 PHP > 7 上運行,並將在發布後在 8 上進行測試。目前,由於某些 DateTime 問題,PHP 7.1 和 7.2 上的測試將會失敗。我會看一下。 PHP 7.3 和 7.4 的測試完全通過。在撰寫本文時,8.0.0 RC2 也表現良好。
這個庫高度依賴
從 v0.2 開始,我還轉而使用與實作無關的讀取器和寫入器方法。現在,我們正在處理抽象底層機制的 BinaryReader(Interface) 和 BinaryWriter(Interface) 實作。我注意到mdurrant/php-binary-reader太慢了。我只是不想重構一切只是為了嘗試 Nelexa 的閱讀能力。相反,我製作了上面提到的兩個介面來抽象提供二進位讀/寫的各種套件。這最終導致了測試/基準測試不同實現的最佳方式 - 以及混合,例如使用 wapmorgan 的套件進行讀取,同時使用 Nelexa 的套件進行寫入。
從 v0.2.1 開始,我自己完成了二進制讀取器/寫入器實現,因為沒有實現滿足性能要求。特別是對於寫入而言,這種超輕量級實作可提供 Nelexa 緩衝區效能的三倍*。
*有意,我喜歡這個詞
範圍內的替代第三方二進位讀/寫包:
透過composer安裝這個包,例如
composer require codename/parquet
隨附的Dockerfile讓您了解所需的系統需求。最重要的事情是克隆並安裝php-ext-snappy 。在撰寫本文時, PECL 尚未發布。
...
# NOTE: this is a dockerfile snippet. Bare metal machines will be a little bit different
RUN git clone --recursive --depth=1 https://github.com/kjdev/php-ext-snappy.git
&& cd php-ext-snappy
&& phpize
&& ./configure
&& make
&& make install
&& docker-php-ext-enable snappy
&& ls -lna
...
請注意:php-ext-snappy 在 Windows 上編譯和安裝有點奇怪,因此這只是在基於 Linux 的系統上安裝和使用的簡短資訊。只要您不需要快速壓縮來讀取或寫入,您就可以使用 php-parquet,而無需自行編譯。
我發現 Mukunku 的 ParquetViewer (https://github.com/mukunku/ParquetViewer) 是在 Windows 桌上型電腦上查看要讀取的資料或驗證某些內容的好方法。至少,這有助於理解某些機制,因為它通過簡單地將數據顯示為表格或多或少地在視覺上有所幫助。
用法與 parquet-dotnet 幾乎相同。注意,我們沒有像 C# 那樣using ( ... ) { }
。因此,您必須確保自己關閉/處置未使用的資源,或讓 PHP 的 GC 透過其引用計數演算法自動處理它。 (這就是為什麼我不像 parquet-dotnet 那樣使用析構函數的原因。)
由於 PHP 的型別系統與 C# 完全不同,因此我們必須對如何處理某些資料型別進行一些補充。例如,PHP 整數在某種程度上可以為空。 C# 中的int 則不然。這是我仍然不確定如何處理的一點。現在,我已將 int (PHP integer )設為可為空 - parquet-dotnet 正在將其設為不可為空。您始終可以透過手動設定->hasNulls = true;
來調整此行為在你的資料欄位上。此外,php-parquet 使用雙重方式來確定類型。在 PHP 中,基元有自己的類型(整數、布林值、浮點/雙精度等)。對於類別實例(尤其是 DateTime/DateTimeImmutable), get_type() 傳回的類型總是 object。這就是 DataTypeHandlers 存在第二個屬性來匹配、確定和處理它的原因:phpClass。
在撰寫本文時,並非 parquet-dotnet 支援的所有資料類型都在這裡支援。 Fe 我跳過了 Int16、SignedByte 等,但擴展至完全二進位相容性應該不會太複雜。
目前,該庫提供讀取和寫入鑲木地板檔案/流所需的核心功能。它不包括來自 C# 命名空間Parquet.Data.Rows
的 parquet-dotnet 的 Table、Row、Enumerators/helpers。
use codename parquet ParquetReader ;
// open file stream (in this example for reading only)
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' r ' );
// open parquet file reader
$ parquetReader = new ParquetReader ( $ fileStream );
// Print custom metadata or do other stuff with it
print_r ( $ parquetReader -> getCustomMetadata ());
// get file schema (available straight after opening parquet reader)
// however, get only data fields as only they contain data values
$ dataFields = $ parquetReader -> schema -> GetDataFields ();
// enumerate through row groups in this file
for ( $ i = 0 ; $ i < $ parquetReader -> getRowGroupCount (); $ i ++)
{
// create row group reader
$ groupReader = $ parquetReader -> OpenRowGroupReader ( $ i );
// read all columns inside each row group (you have an option to read only
// required columns if you need to.
$ columns = [];
foreach ( $ dataFields as $ field ) {
$ columns [] = $ groupReader -> ReadColumn ( $ field );
}
// get first column, for instance
$ firstColumn = $ columns [ 0 ];
// $data member, accessible through ->getData() contains an array of column data
$ data = $ firstColumn -> getData ();
// Print data or do other stuff with it
print_r ( $ data );
}
use codename parquet ParquetWriter ;
use codename parquet data Schema ;
use codename parquet data DataField ;
use codename parquet data DataColumn ;
//create data columns with schema metadata and the data you need
$ idColumn = new DataColumn (
DataField:: createFromType ( ' id ' , ' integer ' ), // NOTE: this is a little bit different to C# due to the type system of PHP
[ 1 , 2 ]
);
$ cityColumn = new DataColumn (
DataField:: createFromType ( ' city ' , ' string ' ),
[ " London " , " Derby " ]
);
// create file schema
$ schema = new Schema ([ $ idColumn -> getField (), $ cityColumn -> getField ()]);
// create file handle with w+ flag, to create a new file - if it doesn't exist yet - or truncate, if it exists
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' w+ ' );
$ parquetWriter = new ParquetWriter ( $ schema , $ fileStream );
// optional, write custom metadata
$ metadata = [ ' author ' => ' santa ' , ' date ' => ' 2020-01-01 ' ];
$ parquetWriter -> setCustomMetadata ( $ metadata );
// create a new row group in the file
$ groupWriter = $ parquetWriter -> CreateRowGroup ();
$ groupWriter -> WriteColumn ( $ idColumn );
$ groupWriter -> WriteColumn ( $ cityColumn );
// As we have no 'using' in PHP, I implemented finish() methods
// for ParquetWriter and ParquetRowGroupWriter
$ groupWriter -> finish (); // finish inner writer(s)
$ parquetWriter -> finish (); // finish the parquet writer last
您也可以使用ParquetDataIterator
和ParquetDataWriter
來處理高度複雜的架構(例如巢狀資料)。儘管在撰寫本文時處於實驗階段,但單元測試和整合測試表明我們與 Spark 具有 100% 的兼容性,因為大多數其他 Parquet 實現缺乏某些功能或超級複雜嵌套的情況。
ParquetDataIterator
和ParquetDataWriter
利用 PHP 類型系統和(關聯)數組的「動態性」——只有在完全使用無符號 64 位元整數時才會停止——由於 PHP 的性質,這些只能部分支援。
ParquetDataIterator
以盡可能節省記憶體的方式自動迭代所有行組和資料頁以及 parquet 檔案的所有列。這意味著,它不會將所有資料集載入到記憶體中,而是在每個資料頁/每個行組的基礎上載入。
在底層,它利用DataColumnsToArrayConverter
的功能,最終完成有關定義和重複層級的所有「繁重工作」。
use codename parquet helper ParquetDataIterator ;
$ iterateMe = ParquetDataIterator:: fromFile ( ' your-parquet-file.parquet ' );
foreach ( $ iterateMe as $ dataset ) {
// $dataset is an associative array
// and already combines data of all columns
// back to a row-like structure
}
反之亦然, ParquetDataWriter
允許您透過傳遞 PHP 關聯數組資料(一次一個或批次)來兩次寫入 Parquet 檔案(在記憶體中或在磁碟上)。在內部,它使用ArrayToDataColumnsConverter
來產生資料、字典、定義和重複層級。
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
DataField:: createFromType ( ' name ' , ' string ' ),
]);
$ handle = fopen ( ' sample.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
// add two records at once
$ dataToWrite = [
[ ' id ' => 1 , ' name ' => ' abc ' ],
[ ' id ' => 2 , ' name ' => ' def ' ],
];
$ dataWriter -> putBatch ( $ dataToWrite );
// we add a third, single one
$ dataWriter -> put ([ ' id ' => 3 , ' name ' => ' ghi ' ]);
$ dataWriter -> finish (); // Don't forget to finish at some point.
fclose ( $ handle ); // You may close the handle, if you have to.
php-parquet支援 Parquet 格式的完整巢狀功能。您可能會注意到,根據您嵌套的欄位類型,您會以某種方式「遺失」鍵名稱。這是設計使然:
一般來說,以下是 Parquet 格式邏輯類型的 PHP 等效項:
實木複合地板 | PHP | JSON | 筆記 |
---|---|---|---|
資料欄位 | 原始 | 原始 | fe 字串、整數等 |
列表字段 | 大批 | 大批[] | 元素類型可以是原始類型,甚至可以是 List、Struct 或 Map |
結構字段 | 關聯數組 | 目的{} | 關聯的密鑰。 array 是 StructField 內的欄位名稱 |
地圖字段 | 關聯數組 | 目的{} | 簡化: array_keys($data['someField']) 和array_values($data['someField']) ,但對於每一行 |
此格式與使用spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
配置的 Spark 產生的 JSON 匯出資料相容。預設情況下,Spark 在匯出到 JSON 時會完全移除null
值。
請注意:所有這些欄位類型都可以在每個巢狀層級上設定為可為空或不可為空/必要(影響定義層級)。某些可空性用於表示空列表並將其與列表的null
值區分開來。
use codename parquet helper ParquetDataIterator ;
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
new MapField (
' aMapField ' ,
DataField:: createFromType ( ' someKey ' , ' string ' ),
StructField:: createWithFieldArray (
' aStructField '
[
DataField:: createFromType ( ' anInteger ' , ' integer ' ),
DataField:: createFromType ( ' aString ' , ' string ' ),
]
)
),
StructField:: createWithFieldArray (
' rootLevelStructField '
[
DataField:: createFromType ( ' anotherInteger ' , ' integer ' ),
DataField:: createFromType ( ' anotherString ' , ' string ' ),
]
),
new ListField (
' aListField ' ,
DataField:: createFromType ( ' someInteger ' , ' integer ' ),
)
]);
$ handle = fopen ( ' complex.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
$ dataToWrite = [
// This is a single dataset:
[
' id ' => 1 ,
' aMapField ' => [
' key1 ' => [ ' anInteger ' => 123 , ' aString ' => ' abc ' ],
' key2 ' => [ ' anInteger ' => 456 , ' aString ' => ' def ' ],
],
' rootLevelStructField ' => [
' anotherInteger ' => 7 ,
' anotherString ' => ' in paradise '
],
' aListField ' => [ 1 , 2 , 3 ]
],
// ... add more datasets as you wish.
];
$ dataWriter -> putBatch ( $ dataToWrite );
$ dataWriter -> finish ();
$ iterateMe = ParquetDataIterator:: fromFile ( ' complex.parquet ' );
// f.e. write back into a full-blown php array:
$ readData = [];
foreach ( $ iterateMe as $ dataset ) {
$ readData [] = $ dataset ;
}
// and now compare this to the original data supplied.
// manually, by print_r, var_dump, assertions, comparisons or whatever you like.
該軟體包還提供與 parquet-dotnet 相同的基準測試。這些是我的機器上的結果:
Parquet.Net(.NET Core 2.1) | php-parquet(裸機 7.3) | php-parquet(docker 化* 7.3) | 快速鑲木地板(Python) | 鑲木地板先生 (Java) | |
---|---|---|---|---|---|
讀 | 255毫秒 | 1'090 毫秒 | 1'244 毫秒 | 154毫秒** | 未經測試的 |
寫入(未壓縮) | 209毫秒 | 1'272 毫秒 | 1'392 毫秒 | 237毫秒** | 未經測試的 |
寫入(gzip) | 1'945 毫秒 | 3'314 毫秒 | 3'695 毫秒 | 1'737 毫秒** | 未經測試的 |
一般來說,這些測試是使用 php-parquet 的 gzip 壓縮等級 6 執行的。當為 1(最小壓縮)時,它大約減半,當為 9(最大壓縮)時,幾乎加倍。請注意,後者可能不會產生最小的檔案大小,但始終會產生最長的壓縮時間。
由於這是來自完全不同的程式語言的套件的部分移植,因此程式設計風格幾乎是一團糟。我決定保留大部分大小寫(例如 $writer->CreateRowGroup() 而不是 ->createRowGroup()),以保持與 parquet-dotnet 的一定「視覺相容性」。至少,從我的角度來看,這是一個理想的狀態,因為它使得在初始開發階段的比較和擴展變得更加容易。
一些程式碼部分和概念已從 C#/.NET 移植,請參閱:
php-parquet 根據 MIT 許可證獲得許可。請參閱文件許可證。
如果您願意,請隨意做 PR。由於這是一個業餘 OSS 項目,貢獻將幫助該軟體包的所有用戶,包括您自己。創建 PR 和/或問題時請應用一些常識,沒有模板。