Esta es la primera implementación de lector/escritor de formato de archivo parquet en PHP, basada en las fuentes Thrift proporcionadas por la Fundación Apache. Gran parte del código y los conceptos se han trasladado desde parquet-dotnet (consulte https://github.com/elastacloud/parquet-dotnet y https://github.com/aloneguid/parquet-dotnet). Por lo tanto, agradecemos a Ivan Gavryliuk (https://github.com/aloneguid).
Este paquete le permite leer y escribir archivos/transmisiones de Parquet sin el uso de extensiones externas exóticas (excepto que desee utilizar métodos de compresión exóticos). Tiene (¿casi?) 100% de compatibilidad de prueba con parquet-dotnet, con respecto a la funcionalidad principal, realizada a través de PHPUnit.
Este repositorio (y el paquete asociado en Packagist) es la continuación oficial del proyecto de jocoon/parquet
. Debido a varias mejoras y correcciones de errores esenciales, aquí en codename/parquet
, se desaconseja el uso del paquete heredado.
Para algunas partes de este paquete, se tuvieron que inventar algunos patrones nuevos ya que no encontré ninguna implementación que cumpliera con los requisitos. En la mayoría de los casos, no había ninguna implementación disponible.
Algunos aspectos destacados:
Comencé a desarrollar esta biblioteca debido al hecho de que simplemente no había ninguna implementación para PHP.
En mi empresa, necesitábamos una solución rápida para archivar grandes cantidades de datos de una base de datos en un formato que aún fuera consultable, extensible desde una perspectiva de esquema y tolerante a fallas. Comenzamos a probar 'migraciones' en vivo a través de AWS DMS a S3, que terminaron fallando en ciertas cantidades de datos debido a limitaciones de memoria. Y simplemente estaba demasiado orientado a la base de datos, además del hecho de que es fácil eliminar accidentalmente datos de cargas anteriores. Como tenemos una arquitectura muy orientada a SDS e independiente de la plataforma, no es mi forma preferida de almacenar datos como un clon 1:1 de una base de datos, como un volcado. En cambio, quería tener la capacidad de almacenar datos, estructurados dinámicamente, como quería, de la misma manera que DMS exportaba a S3. Finalmente, el proyecto murió por las razones mencionadas anteriormente.
Pero no podía quitarme de la cabeza el formato parquet..
El resultado de búsqueda TOP 1 (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) parecía prometedor y no requeriría tanto esfuerzo tener una implementación de PHP, pero de hecho, tomó algo (alrededor de 2 semanas de trabajo no consecutivo). Para mí, como desarrollador de PHP y C#, parquet-dotnet fue un punto de partida perfecto, no sólo porque los puntos de referencia sean demasiado convincentes. Pero esperaba que la implementación de PHP no alcanzara estos niveles de rendimiento, ya que se trata de una implementación inicial que muestra el principio. Y además nadie lo había hecho antes.
Como PHP tiene una gran participación en los proyectos relacionados con la web, esto es IMPRESCINDIBLE en tiempos de creciente necesidad de aplicaciones y escenarios de big data. Para mi motivación personal, esta es una manera de mostrar que PHP ha superado (¿físicamente o virtualmente?) su reputación como "lenguaje de scripting". Creo -o al menos eso espero- que hay personas que se beneficiarán de este paquete y del mensaje que transmite. No sólo objetos de segunda mano. Juego de palabras intencionado.
Necesitará varias extensiones para utilizar esta biblioteca en toda su extensión.
Esta biblioteca se desarrolló originalmente para PHP 7.3, pero debería funcionar en PHP > 7 y se probará en 8, cuando se lance. Por el momento, las pruebas en PHP 7.1 y 7.2 fallarán debido a algunos problemas con DateTime. Le echaré un vistazo. Las pruebas pasan completamente en PHP 7.3 y 7.4. Al momento de escribir este artículo, 8.0.0 RC2 también está funcionando bien.
Esta biblioteca depende en gran medida de
A partir de la versión 0.2, también cambié a un enfoque independiente de la implementación de uso de lectores y escritores. Ahora, estamos tratando con implementaciones de BinaryReader(Interface) y BinaryWriter(Interface) que abstraen el mecanismo subyacente. He notado que mdurrant/php-binary-reader es demasiado lento. Simplemente no quería refactorizar todo sólo para probar los poderes de lectura de Nelexa. En cambio, he creado esas dos interfaces mencionadas anteriormente para abstraer varios paquetes que ofrecen lectura/escritura binaria. Esto finalmente conduce a una forma óptima de probar/evaluar diferentes implementaciones, y también de mezclarlas, por ejemplo, usando el paquete de wapmorgan para leer mientras usa el de Nelexa para escribir.
A partir de la versión 0.2.1, yo mismo realicé las implementaciones del lector/escritor binario, ya que ninguna implementación cumplió con los requisitos de rendimiento. Especialmente para escritura, esta implementación ultraligera ofrece tres veces* el rendimiento del buffer de Nelexa.
*intencionado, me encanta esta palabra
Paquetes alternativos de lectura/escritura binaria de terceros dentro del alcance:
Instale este paquete a través del compositor, por ejemplo
composer require codename/parquet
El Dockerfile incluido le da una idea de los requisitos necesarios del sistema. Lo más importante a realizar es clonar e instalar php-ext-snappy . En el momento de escribir este artículo, aún no se ha publicado en 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
...
Tenga en cuenta: php-ext-snappy es un poco peculiar de compilar e instalar en Windows, por lo que esta es solo una breve información para la instalación y el uso en sistemas basados en Linux. Siempre que no necesite la compresión rápida para leer o escribir, puede usar php-parquet sin compilarlo usted mismo.
Descubrí que ParquetViewer (https://github.com/mukunku/ParquetViewer) de Mukunku es una excelente manera de analizar los datos que se van a leer o verificar algunas cosas en una máquina de escritorio con Windows. Al menos, esto ayuda a comprender ciertos mecanismos, ya que ayuda más o menos visualmente simplemente mostrando los datos como una tabla.
El uso es casi el mismo que el de parquet-dotnet. Tenga en cuenta que no using ( ... ) { }
, como en C#. Por lo tanto, debe asegurarse de cerrar/eliminar los recursos no utilizados usted mismo o dejar que el GC de PHP los maneje automáticamente mediante su algoritmo de recuento. (Esta es la razón por la que no uso destructores como lo hace parquet-dotnet).
Como el sistema de tipos de PHP es completamente diferente al de C#, tenemos que hacer algunas adiciones sobre cómo manejar ciertos tipos de datos. Por ejemplo, un entero PHP es anulable, de alguna manera. Un int en C# no lo es. Este es un punto del que todavía no estoy seguro de cómo abordarlo. Por ahora, configuré que int (PHP integer ) sea anulable; parquet-dotnet está haciendo esto como no anulable. Siempre puedes ajustar este comportamiento configurando manualmente ->hasNulls = true;
en su campo de datos. Además, php-parquet utiliza una forma dual de determinar un tipo. En PHP, una primitiva tiene su propio tipo (entero, bool, flotante/doble, etc.). Para instancias de clase (especialmente DateTime/DateTimeImmutable), el tipo devuelto por get_type() es siempre objeto. Esta es la razón por la que existe una segunda propiedad para DataTypeHandlers para compararlo, determinarlo y procesarlo: phpClass.
Al momento de escribir este artículo, no todos los tipos de datos admitidos por parquet-dotnet también lo son aquí. Fe, me he saltado Int16, SignedByte y algunos más, pero no debería ser demasiado complicado extenderlo a una compatibilidad binaria total.
Por el momento, esta biblioteca ofrece la funcionalidad principal necesaria para leer y escribir archivos/transmisiones de parquet. No incluye la tabla, fila, enumeradores/ayudantes de parquet-dotnet del espacio de nombres 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
También puede utilizar ParquetDataIterator
y ParquetDataWriter
para trabajar incluso con esquemas muy complejos (por ejemplo, datos anidados). Aunque experimentales al momento de escribir este artículo, las pruebas unitarias y de integración indican que tenemos una compatibilidad del 100% con Spark, ya que la mayoría de las otras implementaciones de Parquet carecen de ciertas características o casos de anidamiento supercomplejo.
ParquetDataIterator
y ParquetDataWriter
aprovechan la "dinámica" del sistema de tipos PHP y las matrices (asociativas), que solo se detiene cuando se utilizan enteros de 64 bits sin signo; estos solo pueden admitirse parcialmente debido a la naturaleza de PHP.
ParquetDataIterator
itera automáticamente sobre todos los grupos de filas y páginas de datos, sobre todas las columnas del archivo parquet de la manera más eficiente posible en cuanto a memoria. Esto significa que no carga todos los conjuntos de datos en la memoria, sino que lo hace por página de datos/por grupo de filas.
Debajo del capó, aprovecha la funcionalidad de DataColumnsToArrayConverter
que, en última instancia, hace todo el "trabajo pesado" relacionado con los niveles de definición y repetición .
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
}
Viceversa, ParquetDataWriter
les permite escribir un archivo Parquet (en memoria o en disco) pasando datos de matriz asociativa PHP, ya sea uno a la vez o en lotes. Internamente, utiliza ArrayToDataColumnsConverter
para producir datos, diccionarios, niveles de definición y repetición.
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 admite todas las capacidades de anidamiento del formato Parquet. Puede notar que, dependiendo de los tipos de campos que esté anidando, de alguna manera "perderá" los nombres de las claves. Esto es por diseño:
En términos generales, aquí están los equivalentes PHP de los tipos lógicos del formato Parquet:
Parquet | PHP | JSON | Nota |
---|---|---|---|
campo de datos | primitivo | primitivo | fe cadena, número entero, etc. |
campo de lista | formación | matriz [] | El tipo de elemento puede ser primitivo o incluso Lista, Estructura o Mapa. |
EstructuraCampo | matriz asociativa | objeto {} | Claves de la asoc. matriz son los nombres de los campos dentro de StructField |
Campo de mapa | matriz asociativa | objeto {} | Simplificado: array_keys($data['someField']) y array_values($data['someField']) , pero para cada fila |
El formato es compatible con los datos de exportación JSON generados por Spark configurado con spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
. De forma predeterminada, Spark elimina por completo los valores null
al exportar a JSON.
Tenga en cuenta: todos esos tipos de campos se pueden hacer anulables o no anulables/requeridos en cada nivel de anidamiento (afecta los niveles de definición). Algunas nulabilidades se utilizan para representar listas vacías y distinguirlas de un valor null
para una lista.
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.
Este paquete también proporciona el mismo punto de referencia que parquet-dotnet. Estos son los resultados en mi máquina :
Parquet.Net (.NET Core 2.1) | php-parquet (metal desnudo 7.3) | php-parquet (acoplado* 7.3) | Fastparquet (pitón) | parquet-mr (Java) | |
---|---|---|---|---|---|
Leer | 255 ms | 1'090ms | 1'244ms | 154 ms** | no probado |
Escribir (sin comprimir) | 209ms | 1'272ms | 1'392ms | 237 ms** | no probado |
Escribir (gzip) | 1'945ms | 3'314ms | 3'695ms | 1'737ms** | no probado |
En general, estas pruebas se realizaron con el nivel de compresión gzip 6 para php-parquet. Se reducirá aproximadamente a la mitad con 1 (compresión mínima) y casi se duplicará con 9 (compresión máxima). Tenga en cuenta que es posible que este último no produzca el tamaño de archivo más pequeño, pero siempre el tiempo de compresión más largo.
Como se trata de una adaptación parcial de un paquete de un lenguaje de programación completamente diferente, el estilo de programación es prácticamente un desastre. Decidí conservar la mayor parte de la carcasa (por ejemplo, $writer->CreateRowGroup() en lugar de ->createRowGroup()) para mantener cierta "compatibilidad visual" con parquet-dotnet. Al menos, este es un estado deseable desde mi perspectiva, ya que facilita mucho la comparación y la ampliación durante las etapas iniciales de desarrollo.
Algunas partes del código y conceptos se han trasladado desde C#/.NET; consulte:
php-parquet tiene la licencia MIT. Ver ficha LICENCIA.
Siéntete libre de hacer un PR, si quieres. Como se trata de un proyecto OSS de tiempo libre, las contribuciones ayudarán a todos los usuarios de este paquete, incluido usted mismo. Aplique una pizca de sentido común al crear relaciones públicas y/o problemas; no existe una plantilla.