jocoon / parquet
Requires
- php: >=7.3 <=8.0.99
- ext-bcmath: *
- ext-gmp: *
- ext-zlib: *
- nelexa/buffer: ^1.3
- packaged/thrift: ^0.13.0
- pear/math_biginteger: ^1.0
Requires (Dev)
- phpunit/php-code-coverage: ^9.2
- phpunit/phpunit: ^9.0
Suggests
- ext-snappy: Install/compile snappy extension to get support for snappy compression reading/writing
README
这是PHP中第一个基于Apache Foundation提供的Thrift源代码的Parquet文件格式读写实现。代码的大部分部分和概念已从parquet-dotnet迁移而来(参见https://github.com/elastacloud/parquet-dotnet和https://github.com/aloneguid/parquet-dotnet)。因此,感谢Ivan Gavryliuk(https://github.com/aloneguid)。
此包允许您读取和写入Parquet文件/流。它与parquet-dotnet的核心功能(通过PHPUnit完成)具有近100%的测试兼容性。
迁移通知
自2021年11月10日(及v0.5.1版)起,由于组织结构变更,该项目将迁移到新的仓库和包。
⭐ 开发持续进行 在 github.com/codename-hub/php-parquet 下的新composer包名 codename/parquet。⭐
请注意,这还涉及到从jocoon\parquet到codename\parquet的命名空间更改,但没有其他破坏性更改(您可以在代码中简单地使用搜索和替换方法)。项目将继续遵循Semver约定。现有包将继续工作,但不会有错误修复或新功能。
如果有的话,请使用新包并在此处提交问题/PR。新包正处于提供Parquet格式的更多基本特性的边缘,包括新的格式规范,同时改进整体实现,特别是在嵌套和重复字段方面。
前言
对于此包的一些部分,不得不发明一些新模式,因为我没有找到任何满足要求的实现。在大多数情况下,根本没有任何实现可用。
一些亮点
- GZIP流包装器(也写入头和校验和),用于与fopen()和类似函数一起使用
- Snappy流包装器(Snappy压缩算法),用于与fopen()和类似函数一起使用
- 指定/打开/包装资源ID的流包装器,而不是(或与)文件路径或URI
- TStreamTransport作为纯流Thrift数据的TTransport实现
背景
我开始开发这个库是因为没有PHP的实现。
在我公司,我们需要一个快速解决方案来从数据库中归档大量数据,以一个仍然可查询、从架构角度可扩展且容错的数据格式。我们开始通过AWS DMS进行实时“迁移”到S3的测试,但由于内存限制,在某些数据量上最终崩溃。而且它过于依赖数据库,再加上很容易不小心删除之前加载数据的事实。鉴于我们的系统高度面向SDS且平台无关,我不希望以数据库1:1克隆的方式存储数据,就像备份一样。相反,我想像DMS导出到S3那样,以动态结构化方式存储数据。最终,由于上述原因,项目失败了。
但我无法将Parquet格式从脑海中抹去...
第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对象。这不是故意的。
要求
您需要几个扩展才能充分利用这个库。
- bcmath(现在,这应该是一个必备的)
- gmp(用于处理任意大的整数——间接地,巨大的小数!)
- zlib(用于GZIP(解)压缩)
- snappy(https://github.com/kjdev/php-ext-snappy ——遗憾的是,尚未发布到PECL——您需要自己编译它——请参阅安装指南)
这个库最初是在PHP 7.3下开发/使用的,但它应该可以在PHP > 7上运行,并将在发布时进行测试。目前,由于DateTime问题,PHP 7.1和7.2的测试将失败。我会看看这个问题。PHP 7.3和7.4上的测试完全通过。撰写本文时,8.0.0 RC2的表现也良好。
这个库高度依赖于
- apache/thrift 以处理与Thrift相关的对象和数据
- nelexa/buffer 用于读取和写入二进制数据(我决定不做C# BinaryWriter的克隆。(更新2020-11-04:我刚刚自己克隆了一个,见下文。)
- pear/Math_BigInteger 用于处理以二进制存储的任意精度小数(矛盾,我知道)
截至v0.2版本,我已切换到不依赖于具体实现的读取器和写入器方法。现在,我们处理的是BinaryReader(接口)和BinaryWriter(接口)的实现,它们抽象了底层机制。我发现mdurrant/php-binary-reader的速度太慢。我只是不想为了尝试Nelexa的读取能力而重构所有内容。相反,我创建了上述两个接口来抽象各种提供二进制读取/写入的包。这最终导致了测试/基准测试不同实现的最优方式——也可以混合使用,例如使用wapmorgan的包进行读取,同时使用Nelexa的包进行写入。
截至v0.2.1版本,我已经自行实现了二进制读取器/写入器,因为没有实现满足性能要求。特别是在写入方面,这个超轻量级的实现提供了Nelexa缓冲区的三倍性能。
* 期望如此,我喜欢这个词
范围内的第三方二进制读取/写入包
- nelexa/buffer
- mdurrant/php-binary-reader(仅读取)
- wapmorgan/binary-stream
安装
通过composer安装此包,例如。
composer require jocoon/parquet
请注意:截至2021-11-10,我鼓励切换到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的系统上安装和使用的简要信息。只要您不需要snappy压缩进行读取或写入,就可以使用php-parquet而不需要自己编译。
帮助工具以简化生活
我发现Mukunku的ParquetViewer(https://github.com/mukunku/ParquetViewer)是一个查看要读取的数据或验证Windows桌面机器上的某些内容的好方法。至少,这有助于理解某些机制,因为它通过简单地以表格形式显示数据来提供视觉上的辅助。
API
使用方法几乎与parquet-dotnet相同。请注意,我们没有C#中的using ( ... ) { },所以您必须确保自己关闭/释放未使用的资源,或者让PHP的GC通过其refcounting算法自动处理。(这也是为什么我不像parquet-dotnet那样使用析构器的原因。)
一般说明
由于PHP的类型系统与C#完全不同,我们必须在处理某些数据类型方面做一些补充。例如,PHP整数是可空的,而C#中的int不是。这是我仍不确定如何处理的问题。目前,我已将int(PHP integer)设置为可空的——parquet-dotnet这样做是不可以空的。您始终可以通过在DataField上手动设置->hasNulls = true;来调整此行为。此外,php-parquet使用一种双重方式来确定类型。在PHP中,原始类型有自己的类型(整数、布尔值、浮点/双精度浮点等)。对于类实例(尤其是DateTime/DateTimeImmutable),get_type()返回的类型始终是对象。这就是为什么存在第二个属性DataTypeHandlers来匹配、确定和处理它的原因:phpClass。
在撰写本文时,并非parquet-dotnet支持的所有数据类型在这里都受支持。例如,我跳过了Int16、SignedByte等,但扩展到完整的二进制兼容性应该不会太复杂。
目前,这个库提供了读取和写入Parquet文件/流所需的核心功能。它不包括来自C#命名空间 Parquet.Data.Rows 的parquet-dotnet的Table、Row、Enumerators/helpers。
读取文件
use jocoon\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); // 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 jocoon\parquet\ParquetWriter; use jocoon\parquet\data\Schema; use jocoon\parquet\data\DataField; use jocoon\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); // 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
性能
此包还提供了与parquet-dotnet相同的基准测试。以下是在我的机器上的结果。
| Parquet.Net (.NET Core 2.1) | php-parquet (裸机 7.3) | php-parquet (dockerized* 7.3) | Fastparquet (python) | parquet-mr (Java) | |
|---|---|---|---|---|---|
| 读取 | 255ms | 1'090ms | 1'244ms | 154ms** | 未测试 |
| 写入(未压缩) | 209ms | 1'272ms | 1'392ms | 237ms** | 未测试 |
| 写入(gzip压缩) | 1'945ms | 3'314ms | 3'695ms | 1'737ms** | 未测试 |
** 看起来fastparquet或Python进行了一些内部缓存 - 首次打开文件时的原始结果要差得多(约2'700ms)
总的来说,这些测试是在gzip压缩级别6下对php-parquet进行的。在1(最小压缩)时大约减半,在9(最大压缩)时几乎翻倍。注意,后者可能不会产生最小的文件大小,但压缩时间总是最长的。
编码风格
由于这是一个从完全不同的编程语言中转换过来的包的一部分,编程风格相当混乱。我决定保留大部分的 casing(例如,$writer->CreateRowGroup() 而不是 ->createRowGroup()),以保持与 parquet-dotnet 一定的“视觉兼容性”。至少,这是我希望的状态,因为它在初始开发阶段使比较和扩展变得更加容易。
致谢
一些代码部分和概念是从C#/.NET转换过来的,请参阅
许可证
php-parquet采用MIT许可证。请参阅文件LICENSE。
贡献
如果您愿意,可以进行PR。有关如何贡献的信息即将推出。