这是第一个 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 和/或问题时请应用一些常识,没有模板。