Esta é a primeira implementação de leitor/gravador de formato de arquivo parquet em PHP, baseada nas fontes Thrift fornecidas pela Apache Foundation. Extensas partes do código e conceitos foram portadas do parquet-dotnet (consulte https://github.com/elastacloud/parquet-dotnet e https://github.com/aloneguid/parquet-dotnet). Portanto, nossos agradecimentos vão para Ivan Gavryliuk (https://github.com/aloneguid).
Este pacote permite que você leia e grave arquivos/streams Parquet sem o uso de extensões externas exóticas (exceto se você quiser usar métodos de compactação exóticos). Possui (quase?) 100% de compatibilidade de teste com parquet-dotnet, no que diz respeito à funcionalidade principal, feito via PHPUnit.
Este repositório (e pacote associado no Packagist) é a continuação oficial do projeto jocoon/parquet
. Devido a diversas melhorias e correções de bugs essenciais, aqui em codename/parquet
, o uso do pacote legado é altamente desencorajado.
Para algumas partes deste pacote, alguns novos padrões tiveram que ser inventados, pois não encontrei nenhuma implementação que atendesse aos requisitos. Na maioria dos casos, não havia nenhuma implementação disponível.
Alguns destaques:
Comecei a desenvolver esta biblioteca porque simplesmente não havia implementação para PHP.
Na minha empresa, precisávamos de uma solução rápida para arquivar enormes quantidades de dados de um banco de dados em um formato que ainda pudesse ser consultado, extensível do ponto de vista do esquema e tolerante a falhas. Começamos a testar ‘migrações’ ao vivo via AWS DMS para S3, que acabaram travando em determinadas quantidades de dados, devido a limitações de memória. E simplesmente era muito orientado ao banco de dados, além do fato de que é fácil excluir acidentalmente dados de carregamentos anteriores. Como temos uma arquitetura fortemente orientada a SDS e independente de plataforma, não é minha maneira preferida de armazenar dados como um clone 1:1 de banco de dados, como um dump. Em vez disso, eu queria ter a capacidade de armazenar dados estruturados dinamicamente, como eu queria, da mesma forma que o DMS exportava para o S3. Por fim, o projeto morreu pelos motivos mencionados acima.
Mas não conseguia tirar o formato parquet da cabeça..
O resultado da pesquisa TOP 1 (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) parecia promissor, pois não seria necessário tanto esforço para ter uma implementação de PHP - mas na verdade demorou um pouco (cerca de 2 semanas de trabalho não consecutivo). Para mim, como desenvolvedor de PHP e C#, o parquet-dotnet foi um ponto de partida perfeito - não apenas porque os benchmarks são simplesmente muito atraentes. Mas eu esperava que a implementação do PHP não atendesse a esses níveis de desempenho, pois esta é uma implementação inicial, mostrando o princípio. E além disso, ninguém tinha feito isso antes.
Como o PHP tem uma grande participação em projetos relacionados à web, este é um item OBRIGATÓRIO em tempos de necessidade crescente de aplicações e cenários de big data. Para minha motivação pessoal, esta é uma forma de mostrar que o PHP superou (fisicamente, virtualmente?) sua reputação como uma 'linguagem de script'. Penso - ou pelo menos espero - que existam pessoas que irão beneficiar deste pacote e da mensagem que ele transmite. Não apenas objetos Thrift. Trocadilho intencional.
Você precisará de várias extensões para usar esta biblioteca em toda a sua extensão.
Esta biblioteca foi originalmente desenvolvida para PHP 7.3, mas deve funcionar em PHP > 7 e será testada em 8, quando lançada. No momento, os testes no PHP 7.1 e 7.2 irão falhar devido a alguns problemas de DateTime. Vou dar uma olhada nisso. Os testes são totalmente aprovados no PHP 7.3 e 7.4. No momento em que este artigo foi escrito, o 8.0.0 RC2 também estava apresentando um bom desempenho.
Esta biblioteca depende muito de
A partir da v0.2, também mudei para uma abordagem independente de implementação de uso de leitores e gravadores. Agora, estamos lidando com implementações BinaryReader(Interface) e BinaryWriter(Interface) que abstraem o mecanismo subjacente. Percebi que mdurrant/php-binary-reader é muito lento. Eu só não queria refatorar tudo apenas para testar os poderes de leitura do Nelexa. Em vez disso, criei essas duas interfaces mencionadas acima para abstrair vários pacotes que fornecem leitura/gravação binária. Isto finalmente leva a uma maneira ideal de testar/avaliar diferentes implementações - e também misturar, por exemplo, usando o pacote do wapmorgan para leitura enquanto usa o do Nelexa para escrever.
A partir da v0.2.1, eu mesmo fiz as implementações de leitor/gravador binário, pois nenhuma implementação atendeu aos requisitos de desempenho. Especialmente para escrita, esta implementação ultraleve oferece três vezes* o desempenho do buffer do Nelexa.
* pretendido, eu amo essa palavra
Pacotes alternativos de leitura/gravação binária de terceiros no escopo:
Instale este pacote via compositor, por exemplo
composer require codename/parquet
O Dockerfile incluído dá uma ideia dos requisitos de sistema necessários. A coisa mais importante a realizar é clonar e instalar php-ext-snappy . No momento em que este artigo foi escrito, ainda não havia sido publicado no 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
...
Observação: php-ext-snappy é um pouco peculiar para compilar e instalar no Windows, então esta é apenas uma breve informação para instalação e uso em sistemas baseados em Linux. Contanto que você não precise da compactação rápida para ler ou escrever, você pode usar o php-parquet sem compilá-lo sozinho.
Descobri que o ParquetViewer (https://github.com/mukunku/ParquetViewer) da Mukunku é uma ótima maneira de examinar os dados a serem lidos ou verificar algumas coisas em uma máquina desktop Windows. Pelo menos, isso ajuda a compreender certos mecanismos, pois auxilia mais ou menos visualmente, simplesmente exibindo os dados como uma tabela.
O uso é quase igual ao parquet-dotnet. Observe que não using ( ... ) { }
, como em C#. Portanto, você deve certificar-se de fechar/descartar os recursos não utilizados ou deixar o GC do PHP lidar com isso automaticamente por seu algoritmo de recontagem. (Esta é a razão pela qual não uso destruidores como o parquet-dotnet faz.)
Como o sistema de tipos do PHP é completamente diferente do C#, temos que fazer alguns acréscimos sobre como lidar com certos tipos de dados. Por exemplo, um inteiro PHP é anulável, de alguma forma. Um int em C#, não é. Este é um ponto que ainda não tenho certeza sobre como lidar com isso. Por enquanto, defini int (PHP integer ) como anulável - parquet-dotnet está fazendo isso como não anulável. Você sempre pode ajustar esse comportamento definindo manualmente ->hasNulls = true;
no seu DataField. Além disso, o php-parquet usa uma forma dupla de determinar um tipo. Em PHP, uma primitiva possui seu próprio tipo (inteiro, bool, float/duplo, etc.). Para instâncias de classe (especialmente DateTime/DateTimeImmutable), o tipo retornado por get_type() é sempre object. Esta é a razão pela qual existe uma segunda propriedade para DataTypeHandlers para combiná-lo, determiná-lo e processá-lo: phpClass.
No momento em que este artigo foi escrito, nem todos os DataType suportados pelo parquet-dotnet também são suportados aqui. Fe, pulei Int16, SignedByte e alguns mais, mas não deve ser muito complicado estender a compatibilidade binária total.
No momento, esta biblioteca atende à funcionalidade principal necessária para ler e gravar arquivos/fluxos parquet. Ele não inclui Tabela, Linha, Enumeradores/ajudantes do parquet-dotnet do namespace 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
Você também pode usar ParquetDataIterator
e ParquetDataWriter
para trabalhar mesmo com esquemas altamente complexos (como dados aninhados). Embora experimentais no momento em que este artigo foi escrito, os testes de unidade e integração indicam que temos 100% de compatibilidade com o Spark, já que a maioria das outras implementações do Parquet carecem de certos recursos ou casos de aninhamento supercomplexo.
ParquetDataIterator
e ParquetDataWriter
aproveitam a 'dinâmica' do sistema de tipo PHP e matrizes (associativas) - que só param quando usam inteiros não assinados de 64 bits - que só podem ser parcialmente suportados devido à natureza do PHP.
ParquetDataIterator
itera automaticamente em todos os grupos de linhas e páginas de dados, em todas as colunas do arquivo parquet da maneira mais eficiente possível em termos de memória. Isso significa que ele não carrega todos os conjuntos de dados na memória, mas faz isso por página de dados/por grupo de linhas.
Nos bastidores, ele aproveita a funcionalidade de DataColumnsToArrayConverter
que, em última análise, faz todo o 'trabalho pesado' em relação aos níveis de definição e repetição .
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
}
Vice-versa, ParquetDataWriter
permite que você grave um arquivo Parquet (na memória ou no disco) passando dados de matriz associativa PHP, um de cada vez ou em lotes. Internamente, utiliza ArrayToDataColumnsConverter
para produzir dados, dicionários, níveis de definição e repetição.
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 suporta todos os recursos de aninhamento do formato Parquet. Você pode notar que, dependendo dos tipos de campo que você está aninhando, você de alguma forma 'perderá' os nomes das chaves. Isso ocorre por design:
De modo geral, aqui estão os equivalentes em PHP dos tipos lógicos do formato Parquet:
Parquete | PHP | JSON | Observação |
---|---|---|---|
Campo de dados | primitivo | primitivo | fe string, inteiro, etc. |
CampoLista | variedade | variedade [] | o tipo de elemento pode ser um primitivo ou até mesmo uma lista, estrutura ou mapa |
CampoEstrutura | matriz associativa | objeto {} | Chaves da associação. array são os nomes dos campos dentro do StructField |
CampoMapa | matriz associativa | objeto {} | Simplificado: array_keys($data['someField']) e array_values($data['someField']) , mas para cada linha |
O formato é compatível com dados de exportação JSON gerados pelo Spark configurados com spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
. Por padrão, o Spark remove completamente os valores null
ao exportar para JSON.
Observação: todos esses tipos de campo podem ser anuláveis ou não anuláveis/obrigatórios em todos os níveis de aninhamento (afeta os níveis de definição). Algumas nulidades são usadas para representar listas vazias e distingui-las de um valor null
para uma 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 pacote também fornece o mesmo benchmark do parquet-dotnet. Estes são os resultados na minha máquina :
Parquet.Net (.NET Core 2.1) | php-parquet (bare metal 7.3) | php-parquet (dockerizado* 7.3) | Fastparquet (python) | parquet-mr (Java) | |
---|---|---|---|---|---|
Ler | 255ms | 1'090ms | 1'244ms | 154ms** | não testado |
Escrever (descompactado) | 209ms | 1'272ms | 1'392ms | 237ms** | não testado |
Escreva (gzip) | 1'945ms | 3'314ms | 3'695ms | 1.737ms** | não testado |
Em geral, esses testes foram realizados com compressão gzip nível 6 para php-parquet. Ele cairá aproximadamente pela metade com 1 (compressão mínima) e quase dobrará com 9 (compressão máxima). Observe que o último pode não produzir o menor tamanho de arquivo, mas sempre o maior tempo de compactação.
Como esta é uma porta parcial de um pacote de uma linguagem de programação completamente diferente, o estilo de programação é praticamente uma bagunça. Decidi manter a maior parte do invólucro (por exemplo, $writer->CreateRowGroup() em vez de ->createRowGroup()) para manter uma certa 'compatibilidade visual' com o parquet-dotnet. Pelo menos, este é um estado desejável do meu ponto de vista, pois torna a comparação e a extensão muito mais fáceis durante os estágios iniciais de desenvolvimento.
Algumas partes de código e conceitos foram portados do C#/.NET, consulte:
php-parquet está licenciado sob a licença MIT. Veja arquivo LICENÇA.
Sinta-se à vontade para fazer um PR, se quiser. Como este é um projeto OSS de tempo livre, as contribuições ajudarão todos os usuários deste pacote, inclusive você. Por favor, aplique uma pitada de bom senso ao criar PRs e/ou problemas, não existe um modelo.