jocoon/parquet

此包已被废弃,不再维护。作者建议使用codename/parquet包代替。

基于Thrift的PHP实现,用于使用Apache Parquet格式

v0.5.1 2021-11-10 15:55 UTC

This package is auto-updated.

Last update: 2021-11-10 15:57:25 UTC


README

Build Status (Github Actions)

GitHub Workflow Status (event) GitHub Workflow Status (event)

Packagist Version Packagist PHP Version Support Packagist Downloads

这是PHP中第一个基于Apache Foundation提供的Thrift源代码的Parquet文件格式读写实现。代码的大部分部分和概念已从parquet-dotnet迁移而来(参见https://github.com/elastacloud/parquet-dotnethttps://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\parquetcodename\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(解)压缩)
  • snappyhttps://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** 未测试
* 在Windows 10机器上使用绑定挂载进行dockerized,这会减慢大多数高IOPS进程。
** 看起来fastparquet或Python进行了一些内部缓存 - 首次打开文件时的原始结果要差得多(约2'700ms)

总的来说,这些测试是在gzip压缩级别6下对php-parquet进行的。在1(最小压缩)时大约减半,在9(最大压缩)时几乎翻倍。注意,后者可能不会产生最小的文件大小,但压缩时间总是最长的。

编码风格

由于这是一个从完全不同的编程语言中转换过来的包的一部分,编程风格相当混乱。我决定保留大部分的 casing(例如,$writer->CreateRowGroup() 而不是 ->createRowGroup()),以保持与 parquet-dotnet 一定的“视觉兼容性”。至少,这是我希望的状态,因为它在初始开发阶段使比较和扩展变得更加容易。

致谢

一些代码部分和概念是从C#/.NET转换过来的,请参阅

许可证

php-parquet采用MIT许可证。请参阅文件LICENSE。

贡献

如果您愿意,可以进行PR。有关如何贡献的信息即将推出。