هذا هو أول تطبيق لقارئ/كاتب تنسيق ملف باركيه في PHP، استنادًا إلى مصادر التوفير المقدمة من مؤسسة Apache. تم نقل أجزاء واسعة من التعليمات البرمجية والمفاهيم من parquet-dotnet (راجع https://github.com/elastacloud/parquet-dotnet وhttps://github.com/aloneguid/parquet-dotnet). لذلك، شكرًا لإيفان جافريليوك (https://github.com/aloneguid).
تمكنك هذه الحزمة من قراءة وكتابة ملفات/تدفقات الباركيه بدون استخدام الامتدادات الخارجية الغريبة (باستثناء أنك تريد استخدام طرق ضغط غريبة). لقد تم اختبار توافقه (تقريبًا؟) بنسبة 100٪ مع parquet-dotnet، فيما يتعلق بالوظائف الأساسية، ويتم ذلك عبر PHPUnit.
هذا المستودع (والحزمة المرتبطة به على Packagist) هو استمرار المشروع الرسمي لـ jocoon/parquet
. نظرًا للتحسينات المختلفة وإصلاحات الأخطاء الأساسية، هنا في codename/parquet
، لا يُنصح بشدة باستخدام الحزمة القديمة.
بالنسبة لبعض أجزاء هذه الحزمة، كان لا بد من اختراع بعض الأنماط الجديدة لأنني لم أجد أي تطبيق يلبي المتطلبات. في معظم الحالات، لم تكن هناك أي تطبيقات متاحة على الإطلاق.
بعض النقاط البارزة:
لقد بدأت في تطوير هذه المكتبة نظرًا لعدم وجود تطبيق لـ PHP.
في شركتي، كنا بحاجة إلى حل سريع لأرشفة كميات هائلة من البيانات من قاعدة بيانات بتنسيق لا يزال قابلاً للاستعلام عنه وقابلاً للتوسيع من منظور المخطط ومتسامحًا مع الأخطاء. لقد بدأنا في اختبار "عمليات الترحيل" المباشرة عبر AWS DMS إلى S3، والتي انتهت بتعطل كميات معينة من البيانات، بسبب قيود الذاكرة. وكان ببساطة موجهًا نحو قواعد البيانات، إلى جانب حقيقة أنه من السهل حذف البيانات عن طريق الخطأ من عمليات التحميل السابقة. نظرًا لأن لدينا بنية موجهة بشكل كبير لـ SDS ومستقلة عن النظام الأساسي، فهي ليست طريقتي المفضلة لتخزين البيانات كنسخة 1:1 من قاعدة البيانات، مثل التفريغ. بدلاً من ذلك، كنت أرغب في الحصول على القدرة على تخزين البيانات، بشكل منظم ديناميكيًا، كما أردت، بنفس الطريقة التي كان يتم بها تصدير DMS إلى S3. وأخيراً مات المشروع للأسباب المذكورة أعلاه.
لكن لم أستطع إخراج شكل الباركيه من رأسي..
بدت نتيجة البحث الأولى (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) واعدة، حيث أنها لن تتطلب الكثير من الجهد للحصول عليها تنفيذ PHP - ولكن في الواقع، استغرق الأمر بعض الوقت (حوالي أسبوعين من العمل غير المتتالي). بالنسبة لي، كمطور PHP وC#، كانت parquet-dotnet نقطة انطلاق مثالية - ليس فقط بسبب حقيقة أن المعايير مقنعة للغاية. لكنني توقعت ألا يلبي تطبيق PHP هذه المستويات من الأداء، لأن هذا تطبيق أولي يوضح المبدأ. وبالإضافة إلى ذلك، لم يفعل أحد ذلك من قبل.
نظرًا لأن PHP تتمتع بحصة كبيرة فيما يتعلق بالمشاريع المتعلقة بالويب، فهذا أمر لا بد منه في أوقات الحاجة المتزايدة لتطبيقات وسيناريوهات البيانات الضخمة. بالنسبة لحافزي الشخصي، فهذه طريقة لإظهار أن PHP قد تجاوزت سمعتها باعتبارها "لغة برمجة نصية" (فعليًا أو افتراضيًا؟). أعتقد - أو على الأقل آمل - أن هناك أشخاصًا سيستفيدون من هذه الحزمة والرسالة التي تنقلها. ليس فقط كائنات التوفير. يقصد التورية.
ستحتاج إلى عدة ملحقات لاستخدام هذه المكتبة إلى أقصى حد.
تم تطوير هذه المكتبة في الأصل إلى/باستخدام PHP 7.3، ولكن يجب أن تعمل على PHP> 7 وسيتم اختبارها على 8، عند إصدارها. في الوقت الحالي، ستفشل اختبارات PHP 7.1 و7.2 بسبب بعض مشكلات DateTime. سوف ألقي نظرة على ذلك. تجتاز الاختبارات PHP 7.3 و7.4 بشكل كامل. في وقت كتابة هذا التقرير، كان أداء الإصدار 8.0.0 RC2 جيدًا أيضًا.
تعتمد هذه المكتبة بشكل كبير على
اعتبارًا من الإصدار 0.2، قمت أيضًا بالتحول إلى أسلوب حيادي التنفيذ لاستخدام القراء والكتاب. الآن، نحن نتعامل مع تطبيقات BinaryReader(Interface) وBinaryWriter(Interface) التي تلخص الآلية الأساسية. لقد لاحظت أن mdurrant/php-binary-reader بطيء جدًا. أنا فقط لم أرغب في إعادة هيكلة كل شيء فقط لتجربة قدرات القراءة لدى Nelexa. بدلاً من ذلك، قمت بإنشاء هاتين الواجهتين المذكورتين أعلاه لتجريد الحزم المختلفة التي توفر القراءة/الكتابة الثنائية. يؤدي هذا أخيرًا إلى طريقة مثالية لاختبار/مقارنة التطبيقات المختلفة - وكذلك المزج، على سبيل المثال، استخدام حزمة waporgan للقراءة أثناء استخدام حزمة Nelexa للكتابة.
اعتبارًا من الإصدار 0.2.1، قمت بإجراء عمليات تنفيذ القارئ/الكاتب الثنائي بنفسي، حيث لم يلبي أي تطبيق متطلبات الأداء. خاصة بالنسبة للكتابة، يوفر هذا التطبيق خفيف الوزن للغاية ثلاث مرات* أداء المخزن المؤقت لـ Nelexa.
* مقصود أنا أحب هذه الكلمة
حزم القراءة/الكتابة الثنائية البديلة التابعة لجهات خارجية في النطاق:
قم بتثبيت هذه الحزمة عبر الملحن، على سبيل المثال
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 دون تجميعه بنفسك.
لقد وجدت أن ParquetViewer (https://github.com/mukunku/ParquetViewer) بواسطة Mukunku هو وسيلة رائعة للنظر في البيانات المراد قراءتها أو التحقق من بعض الأشياء على جهاز سطح مكتب يعمل بنظام Windows. على الأقل، يساعد هذا في فهم آليات معينة، لأنه يساعد بشكل أو بآخر بصريًا بمجرد عرض البيانات كجدول.
الاستخدام هو تقريبًا نفس استخدام الباركيه دوت نت. يرجى ملاحظة أننا لا using ( ... ) { }
، كما هو الحال في C#. لذلك عليك التأكد من إغلاق/التخلص من الموارد غير المستخدمة بنفسك أو السماح لـ PHP's GC بالتعامل معها تلقائيًا من خلال خوارزمية إعادة الحساب الخاصة بها. (هذا هو السبب وراء عدم استخدامي للمدمرات مثل parquet-dotnet.)
نظرًا لأن نظام كتابة PHP مختلف تمامًا عن نظام C#، يتعين علينا إجراء بعض الإضافات حول كيفية التعامل مع أنواع معينة من البيانات. على سبيل المثال، عدد PHP الصحيح لاغٍ، بطريقة أو بأخرى. إن int في C# ليس كذلك. هذه نقطة ما زلت غير متأكد من كيفية التعامل معها. في الوقت الحالي، قمت بتعيين int (PHP integer ) ليكون nullable - parquet-dotnet يقوم بذلك على أنه غير nullable. يمكنك دائمًا ضبط هذا السلوك عن طريق الإعداد يدويًا ->hasNulls = true;
على DataField الخاص بك. بالإضافة إلى ذلك، يستخدم php-parquet طريقة مزدوجة لتحديد النوع. في PHP، البدائي له نوعه الخاص (عدد صحيح، منطقي، عائم/مزدوج، وما إلى ذلك). بالنسبة لمثيلات الفئة (خاصة DateTime/DateTimeImmutable)، فإن النوع الذي يتم إرجاعه بواسطة get_type() يكون دائمًا كائنًا. هذا هو السبب وراء وجود خاصية ثانية لـ DataTypeHandlers لمطابقتها وتحديدها ومعالجتها: phpClass.
في وقت كتابة هذا التقرير، لا يتم دعم كل أنواع البيانات التي تدعمها parquet-dotnet هنا أيضًا. لقد تخطيت Int16 وSignedByte وغيرهما، لكن لا ينبغي أن يكون الأمر معقدًا للغاية بحيث لا يمتد إلى التوافق الثنائي الكامل.
في الوقت الحالي، تخدم هذه المكتبة الوظائف الأساسية اللازمة لقراءة وكتابة ملفات/تدفقات الباركيه. ولا يتضمن الجدول والصف والعدادات/المساعدين في parquet-dotnet من مساحة الاسم 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
يمكنك أيضًا استخدام ParquetDataIterator
و ParquetDataWriter
للعمل حتى مع المخططات شديدة التعقيد (البيانات المتداخلة). على الرغم من كونها تجريبية في وقت كتابة هذا التقرير، تشير اختبارات الوحدة والتكامل إلى أن لدينا توافقًا بنسبة 100% مع Spark، حيث تفتقر معظم تطبيقات Parquet الأخرى إلى ميزات معينة أو حالات تداخل فائقة التعقيد.
يستفيد ParquetDataIterator
و ParquetDataWriter
من "الديناميكية" لنظام نوع PHP والمصفوفات (الترابطية) - والتي تتوقف فقط عند الاستخدام الكامل لأعداد صحيحة 64 بت غير موقعة - ولا يمكن دعمها إلا جزئيًا نظرًا لطبيعة PHP.
يقوم ParquetDataIterator
بالتكرار تلقائيًا على جميع مجموعات الصفوف وصفحات البيانات، وعلى جميع أعمدة ملف الباركيه بأفضل طريقة ممكنة من حيث كفاءة الذاكرة. وهذا يعني أنه لا يقوم بتحميل جميع مجموعات البيانات في الذاكرة، ولكنه يقوم بذلك على أساس كل صفحة بيانات/كل مجموعة صف.
تحت الغطاء، فإنه يعزز وظائف 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
كتابة ملف Parquet (في الذاكرة أو على القرص) عن طريق تمرير بيانات مصفوفة PHP المرتبطة، إما واحدًا تلو الآخر أو على دفعات. داخليًا، يستخدم 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. قد تلاحظ، اعتمادًا على أنواع الحقول التي تقوم بتداخلها، أنك ستفقد أسماء المفاتيح بطريقة ما. وهذا حسب التصميم:
بشكل عام، فيما يلي مرادفات PHP للأنواع المنطقية لتنسيق Parquet:
باركيه | PHP | JSON | ملحوظة |
---|---|---|---|
DataField | بدائية | بدائية | سلسلة fe، عدد صحيح، الخ. |
ListField | صفيف | مصفوفة [] | يمكن أن يكون نوع العنصر بدائيًا أو حتى قائمة أو هيكلًا أو خريطة |
StructField | مجموعة النقابي | هدف {} | مفاتيح المساعد. array هي أسماء الحقول داخل StructField |
MapField | مجموعة النقابي | هدف {} | التبسيط: array_keys($data['someField']) و array_values($data['someField']) ولكن لكل صف |
يتوافق التنسيق مع بيانات تصدير JSON التي تم إنشاؤها بواسطة Spark التي تم تكوينها باستخدام spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
. افتراضيًا، يقوم Spark بإزالة القيم null
تمامًا عند التصدير إلى JSON.
يرجى ملاحظة: يمكن جعل جميع أنواع الحقول هذه فارغة أو غير قابلة للإلغاء/مطلوبة على كل مستوى تداخل (يؤثر على مستويات التعريف). يتم استخدام بعض القيم الخالية fe لتمثيل القوائم الفارغة وتمييزها عن القيمة 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. وهذه هي النتائج على جهازي :
باركيه.نت (.NET كور 2.1) | php-الباركيه (المعادن العارية 7.3) | php-parquet (مُجهز* 7.3) | فاست باركيه (بيثون) | الباركيه السيد (جافا) | |
---|---|---|---|---|---|
يقرأ | 255 مللي ثانية | 1'090 مللي ثانية | 1'244 مللي ثانية | 154 مللي ثانية** | لم تختبر |
الكتابة (غير مضغوطة) | 209 مللي ثانية | 1'272 مللي ثانية | 1'392 مللي ثانية | 237 مللي ثانية ** | لم تختبر |
اكتب (gzip) | 1'945 مللي ثانية | 3'314 مللي ثانية | 3'695 مللي ثانية | 1'737 مللي ثانية** | لم تختبر |
بشكل عام، تم إجراء هذه الاختبارات بمستوى ضغط gzip 6 لـ php-parquet. سينخفض إلى النصف تقريبًا عند 1 (الحد الأدنى للضغط) ويتضاعف تقريبًا عند 9 (الحد الأقصى للضغط). لاحظ أن هذا الأخير قد لا ينتج عنه أصغر حجم للملف، ولكنه دائمًا أطول وقت للضغط.
نظرًا لأن هذا منفذ جزئي لحزمة من لغة برمجة مختلفة تمامًا، فإن أسلوب البرمجة هو إلى حد كبير فوضى خالصة. قررت الاحتفاظ بمعظم الغلاف (على سبيل المثال $writer->CreateRowGroup() بدلاً من ->createRowGroup()) للحفاظ على "توافق مرئي" معين مع parquet-dotnet. على الأقل، هذه حالة مرغوبة من وجهة نظري، لأنها تجعل المقارنة والتوسيع أسهل بكثير خلال مراحل التطوير الأولية.
تم نقل بعض أجزاء ومفاهيم التعليمات البرمجية من C#/.NET، راجع:
php-parquet مرخص بموجب ترخيص MIT. انظر ترخيص الملف.
لا تتردد في القيام بالعلاقات العامة، إذا كنت تريد. نظرًا لأن هذا مشروع OSS في وقت الفراغ، فإن المساهمات ستساعد جميع مستخدمي هذه الحزمة، بما فيهم أنت. يرجى تطبيق القليل من المنطق السليم عند إنشاء العلاقات العامة و/أو المشكلات، فلا يوجد قالب.