Ini adalah implementasi pembaca/penulis format file parket pertama di PHP, berdasarkan sumber Thrift yang disediakan oleh Apache Foundation. Sebagian besar kode dan konsep telah dipindahkan dari parquet-dotnet (lihat https://github.com/elastacloud/parquet-dotnet dan https://github.com/aloneguid/parquet-dotnet). Oleh karena itu, terima kasih sampaikan kepada Ivan Gavryliuk (https://github.com/aloneguid).
Paket ini memungkinkan Anda membaca dan menulis file/aliran Parket tanpa menggunakan ekstensi eksternal eksotik (kecuali Anda ingin menggunakan metode kompresi eksotik). Ini memiliki (hampir?) 100% kompatibilitas pengujian dengan parket-dotnet, mengenai fungsionalitas inti, dilakukan melalui PHPUnit.
Repositori ini (dan paket terkait di Packagist) adalah kelanjutan proyek resmi jocoon/parquet
. Karena berbagai perbaikan dan perbaikan bug penting, di sini, di codename/parquet
, penggunaan paket lama sangat tidak disarankan.
Untuk beberapa bagian dari paket ini, beberapa pola baru harus ditemukan karena saya belum menemukan implementasi apa pun yang memenuhi persyaratan. Pada sebagian besar kasus, tidak ada implementasi yang tersedia sama sekali.
Beberapa hal penting:
Saya mulai mengembangkan perpustakaan ini karena belum ada implementasi untuk PHP.
Di perusahaan saya, kami memerlukan solusi cepat untuk mengarsipkan data dalam jumlah besar dari database dalam format yang masih dapat dikueri, dapat diperluas dari perspektif skema, dan toleran terhadap kesalahan. Kami mulai menguji 'migrasi' langsung melalui AWS DMS ke S3, yang akhirnya mengalami error pada sejumlah data tertentu, karena keterbatasan memori. Dan itu terlalu berorientasi pada db, selain faktanya mudah untuk secara tidak sengaja menghapus data dari pemuatan sebelumnya. Karena kami memiliki arsitektur yang sangat berorientasi pada SDS dan platform-agnostik, ini bukanlah cara pilihan saya untuk menyimpan data sebagai tiruan database 1:1, seperti dump. Sebaliknya, saya ingin memiliki kemampuan untuk menyimpan data, terstruktur secara dinamis, seperti yang saya inginkan, dengan cara yang sama seperti DMS mengekspor ke S3. Akhirnya proyek tersebut mati karena alasan yang disebutkan di atas.
Tapi saya tidak bisa menghilangkan format parket dari kepala saya..
Hasil pencarian TOP 1 (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) tampak menjanjikan sehingga tidak memerlukan banyak usaha untuk mendapatkannya implementasi PHP - namun nyatanya, dibutuhkan beberapa waktu (sekitar 2 minggu pekerjaan tidak berturut-turut). Bagi saya, sebagai pengembang PHP dan C#, parket-dotnet adalah titik awal yang sempurna - bukan hanya karena tolok ukurnya terlalu menarik. Tapi saya berharap implementasi PHP tidak memenuhi tingkat kinerja ini, karena ini adalah implementasi awal, menunjukkan prinsipnya. Dan selain itu, belum ada yang melakukannya sebelumnya.
Karena PHP mempunyai andil besar dalam proyek-proyek terkait web, hal ini HARUS DIMILIKI di saat meningkatnya kebutuhan akan aplikasi dan skenario data besar. Untuk motivasi pribadi saya, ini adalah cara untuk menunjukkan PHP telah (secara fisik, virtual?) melampaui reputasinya sebagai 'bahasa scripting'. Saya pikir - atau setidaknya saya berharap - ada orang di luar sana yang akan mendapat manfaat dari paket ini dan pesan yang disampaikannya. Bukan hanya benda Thrift saja. Pun dimaksudkan.
Anda memerlukan beberapa ekstensi untuk menggunakan perpustakaan ini sepenuhnya.
Pustaka ini pada awalnya dikembangkan untuk/menggunakan PHP 7.3, namun seharusnya dapat berfungsi pada PHP > 7 dan akan diuji pada versi 8, ketika dirilis. Saat ini, pengujian pada PHP 7.1 dan 7.2 akan gagal karena beberapa masalah DateTime. Saya akan melihatnya. Tes sepenuhnya lulus pada PHP 7.3 dan 7.4. Pada saat penulisan ini, 8.0.0 RC2 juga berkinerja baik.
Perpustakaan ini sangat bergantung pada
Pada v0.2, saya juga beralih ke pendekatan implementasi-agnostik dengan menggunakan pembaca dan penulis. Sekarang, kita berurusan dengan implementasi BinaryReader(Interface) dan BinaryWriter(Interface) yang mengabstraksi mekanisme yang mendasarinya. Saya perhatikan mdurrant/php-binary-reader terlalu lambat. Saya hanya tidak ingin memfaktorkan ulang semuanya hanya untuk mencoba kekuatan membaca Nelexa. Sebagai gantinya, saya telah membuat dua antarmuka yang disebutkan di atas untuk mengabstraksi berbagai paket yang memberikan pembacaan/penulisan biner. Hal ini pada akhirnya menghasilkan cara yang optimal untuk menguji/membandingkan implementasi yang berbeda - dan juga melakukan pencampuran, misalnya menggunakan paket wapmorgan untuk membaca sementara menggunakan paket Nelexa untuk menulis.
Pada v0.2.1 Saya telah melakukan sendiri implementasi pembaca/penulis biner, karena tidak ada implementasi yang memenuhi persyaratan kinerja. Khusus untuk menulis, implementasi ultra-ringan ini memberikan kinerja buffer Nelexa tiga kali lipat*.
*dimaksudkan, aku suka kata ini
Paket membaca/menulis biner pihak ketiga alternatif dalam cakupan:
Instal paket ini melalui komposer, misalnya
composer require codename/parquet
Dockerfile yang disertakan memberi Anda gambaran tentang persyaratan sistem yang diperlukan. Hal terpenting yang harus dilakukan adalah mengkloning dan menginstal php-ext-snappy . Pada saat tulisan ini dibuat, PECL belum dipublikasikan .
...
# 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
...
Harap dicatat: php-ext-snappy agak aneh untuk dikompilasi dan diinstal pada Windows, jadi ini hanyalah informasi singkat untuk instalasi dan penggunaan pada sistem berbasis Linux. Selama Anda tidak memerlukan kompresi cepat untuk membaca atau menulis, Anda dapat menggunakan php-parquet tanpa mengkompilasinya sendiri.
Saya menemukan ParquetViewer (https://github.com/mukunku/ParquetViewer) oleh Mukunku sebagai cara yang bagus untuk melihat data untuk dibaca atau memverifikasi beberapa hal di mesin desktop Windows. Setidaknya, hal ini membantu memahami mekanisme tertentu, karena kurang lebih membantu secara visual hanya dengan menampilkan data dalam bentuk tabel.
Pemakaiannya hampir sama dengan parket-dotnet. Harap dicatat, kami tidak using ( ... ) { }
, seperti di C#. Jadi, Anda harus memastikan untuk menutup/membuang sendiri sumber daya yang tidak terpakai atau membiarkan GC PHP menanganinya secara otomatis dengan algoritma penghitungan ulangnya. (Inilah alasan mengapa saya tidak menggunakan destruktor seperti yang dilakukan parquet-dotnet.)
Karena sistem tipe PHP sangat berbeda dengan C#, kita harus membuat beberapa tambahan tentang cara menangani tipe data tertentu. Misalnya, bilangan bulat PHP bisa dibatalkan. Sebuah int di C#, tidak. Ini adalah poin yang saya masih tidak yakin bagaimana cara menghadapinya. Untuk saat ini, saya telah menetapkan int (PHP integer ) menjadi nullable - parquet-dotnet melakukan ini sebagai not-nullable. Anda selalu dapat menyesuaikan perilaku ini dengan mengatur secara manual ->hasNulls = true;
di Bidang Data Anda. Selain itu, php-parquet menggunakan dua cara untuk menentukan jenis. Dalam PHP, primitif memiliki tipenya sendiri (integer, bool, float/double, dll.). Untuk instance kelas (terutama DateTime/DateTimeImmutable), tipe yang dikembalikan oleh get_type() selalu berupa objek. Inilah alasan properti kedua untuk DataTypeHandlers ada untuk mencocokkan, menentukan, dan memprosesnya: phpClass.
Pada saat penulisan, tidak semua Tipe Data yang didukung oleh parket-dotnet juga didukung di sini. Fe Saya telah melewatkan Int16, SignedByte dan banyak lagi, tetapi seharusnya tidak terlalu rumit untuk memperluas kompatibilitas biner penuh.
Saat ini, perpustakaan ini melayani fungsionalitas inti yang diperlukan untuk membaca dan menulis file/aliran parket. Itu tidak termasuk Tabel, Baris, Enumerator/pembantu parket-dotnet dari namespace C# Parquet.Data.Rows
.
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
Anda juga dapat menggunakan ParquetDataIterator
dan ParquetDataWriter
untuk bekerja bahkan dengan skema yang sangat kompleks (data bersarang). Meskipun bersifat eksperimental pada saat penulisan, pengujian unit dan integrasi menunjukkan bahwa kami memiliki kompatibilitas 100% dengan Spark, karena sebagian besar implementasi Parket lainnya tidak memiliki fitur tertentu atau kasus penyarangan yang sangat kompleks.
ParquetDataIterator
dan ParquetDataWriter
memanfaatkan 'kedinamisan' sistem tipe PHP dan array (asosiatif) - yang hanya terhenti ketika sepenuhnya menggunakan bilangan bulat 64-bit yang tidak ditandatangani - semuanya hanya dapat didukung sebagian karena sifat PHP.
ParquetDataIterator
secara otomatis melakukan iterasi pada semua grup baris dan halaman data, pada semua kolom file parket dengan cara yang paling hemat memori. Artinya, ini tidak memuat semua kumpulan data ke dalam memori, tetapi melakukannya berdasarkan per halaman data/per grup baris.
Di bawah tenda, ini memanfaatkan fungsionalitas DataColumnsToArrayConverter
yang pada akhirnya melakukan semua 'pekerjaan berat' terkait Definisi dan Tingkat Pengulangan .
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
}
Begitu pula sebaliknya, ParquetDataWriter
memungkinkan Anda berdua menulis file Parket (di dalam memori atau di disk) dengan meneruskan data array asosiatif PHP, baik satu per satu atau secara batch. Secara internal, ia menggunakan ArrayToDataColumnsConverter
untuk menghasilkan data, kamus, definisi, dan tingkat pengulangan.
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 mendukung kemampuan bersarang penuh dari format Parket. Anda mungkin memperhatikan, bergantung pada jenis bidang apa yang Anda susun, entah bagaimana Anda akan 'kehilangan' nama kunci. Ini berdasarkan desain:
Secara umum, berikut adalah padanan PHP dari Tipe Logis Format Parket:
Parket | PHP | JSON | Catatan |
---|---|---|---|
Bidang Data | primitif | primitif | string fe, bilangan bulat, dll. |
Bidang Daftar | susunan | susunan [] | tipe elemen bisa berupa primitif atau bahkan Daftar, Struktur atau Peta |
Bidang Struktur | susunan asosiatif | objek {} | Kunci asosiasi. array adalah nama field di dalam StructField |
Bidang Peta | susunan asosiatif | objek {} | Sederhana: array_keys($data['someField']) dan array_values($data['someField']) , tetapi untuk setiap baris |
Format ini kompatibel dengan data ekspor JSON yang dihasilkan oleh Spark yang dikonfigurasi dengan spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
. Secara default, Spark menghapus nilai null
sepenuhnya saat mengekspor ke JSON.
Harap dicatat: Semua jenis bidang tersebut dapat dibuat menjadi nullable atau non-nullable/wajib pada setiap level bersarang (mempengaruhi level definisi). Beberapa nullabilities digunakan fe untuk mewakili daftar kosong dan membedakannya dari nilai null
untuk sebuah daftar.
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.
Paket ini juga memberikan benchmark yang sama dengan parket-dotnet. Ini adalah hasil di mesin saya :
Parket.Net (.NET Core 2.1) | php-parket (bare metal 7.3) | php-parquet (di-docker* 7.3) | Parket cepat (python) | parket-mr (Jawa) | |
---|---|---|---|---|---|
Membaca | 255 md | 1'090ms | 1'244ms | 154 md** | belum dicoba |
Tulis (tidak terkompresi) | 209 md | 1'272ms | 1'392ms | 237 md** | belum dicoba |
Tulis (gzip) | 1'945ms | 3'314ms | 3'695ms | 1'737ms** | belum dicoba |
Secara umum, pengujian ini dilakukan dengan kompresi gzip level 6 untuk php-parquet. Ini kira-kira akan berkurang setengahnya pada 1 (kompresi minimum) dan hampir dua kali lipat pada 9 (kompresi maksimum). Catatan, yang terakhir mungkin tidak menghasilkan ukuran file terkecil, tetapi selalu memiliki waktu kompresi terlama.
Karena ini adalah sebagian port dari sebuah paket dari bahasa pemrograman yang sama sekali berbeda, gaya pemrogramannya benar-benar berantakan. Saya memutuskan untuk mempertahankan sebagian besar casing (misalnya $writer->CreateRowGroup() daripada ->createRowGroup()) untuk menjaga 'kompatibilitas visual' tertentu dengan parket-dotnet. Setidaknya, ini adalah keadaan yang diinginkan dari sudut pandang saya, karena membuat perbandingan dan perluasan menjadi lebih mudah selama tahap pengembangan awal.
Beberapa bagian kode dan konsep telah di-porting dari C#/.NET, lihat:
php-parquet dilisensikan di bawah lisensi MIT. Lihat file LISENSI.
Jangan ragu untuk melakukan PR, jika Anda mau. Karena ini adalah proyek OSS waktu luang, kontribusi akan membantu semua pengguna paket ini, termasuk Anda sendiri. Harap terapkan sedikit akal sehat saat membuat PR dan/atau terbitan, tidak ada templat.