Como dividir um arquivo CSV por coluna inicial (com cabeçalhos)?

2

Esta é uma combinação de duas outras perguntas ( como dividir um arquivo por cada prefixo de linha e how para dividir um arquivo de acordo com uma coluna, incluindo o cabeçalho ). Eu quero ir deste conteúdo em input.csv :

id,first,second,third
1,a,b,c
333,b,b,b
1,d,e,f
2,d,e,f
1,c,d,e
333,a,a,a
[more lines in the same format]

para este conteúdo em 1.csv :

id,first,second,third
1,a,b,c
1,d,e,f
1,c,d,e

, este conteúdo em 2.csv :

id,first,second,third
2,d,e,f

e este conteúdo em 333.csv :

id,first,second,third
333,b,b,b
333,a,a,a

, isto é:

  1. Coloque todas as linhas com o ID N em N.csv .
  2. Mantenha a sequência de linhas como no original.
  3. Inclua o cabeçalho do arquivo original em todos os arquivos de saída.

Isso também deve ser muito rápido, então um while read loop não irá cortá-lo.

    
por l0b0 09.02.2017 / 13:31

3 respostas

5

Este comando GNU awk faz o truque:

awk -F ',' 'NR==1{h=$0; next};!seen[$1]++{f=$1".csv"; print h > f};{f=$1".csv"; print >> f; close(f)}' input.csv

Advertência: Isso não funcionará se houver vírgulas com escape no primeiro campo. Vírgulas em outros campos devem funcionar bem.

Explicação:

  • -F ',' (separador de campo) garante que $1 etc. se refiram às colunas CSV, em vez de valores separados por espaços.
  • NR==1{h=$0; next} trata a primeira linha especialmente ( NR==1 ), armazenando a linha de cabeçalho completa em uma variável h ( h=$0 ) e pulando a linha ( next ).
  • !seen[$1]++{f=$1".csv"; print h > f} trata a primeira ocorrência de qualquer $1 especialmente ( !seen[$1] ) armazenando $1 seguido de .csv em uma variável de nome de arquivo f e salvando o cabeçalho nesse arquivo ( print h > f ).
  • {f=$1".csv"; print >> f; close(f)} adiciona a linha atual ao arquivo ( print >> f ) e fecha o descritor de arquivo ( close(f) ) para evitar mantê-lo assim que o processamento de todas as linhas com um ID específico for concluído.

Bônus: se você substituir $1 por outro campo, ele deverá fazer o que você espera: criar um arquivo por valor único nessa coluna com as linhas que contêm esse valor na coluna especificada.

    
por 09.02.2017 / 13:34
3

(Desculpe por enviar spam a todos com outra resposta) Para muitas situações, as elegantes versões do awk apresentadas são perfeitas. Mas há vida fora dos one-liners - geralmente precisamos de mais:

  • adicione código extra para lidar com arquivos csv complexos;
  • adicione etapas extras para normalização, reformatação e processamento.

No esqueleto a seguir, usamos um analisador de arquivos CSV. Desta vez estamos evitando ligners e até declaramos as variáveis!

#!/usr/bin/perl

use strict;
use Parse::CSV;
my %dict=();

my $c = Parse::CSV->new(file => 'a1.csv');

while ( my $row = $c->fetch ) {                    ## for all records
   $dict{$row->[0]} .=   join(" :: ",@$row)."\n";  ## process and save
}

for my $k (keys %dict){                            ## create the cvs files
   open(F,">","$k.cvs") or die;
   print F $dict{$k};
   close F;
}
  • A principal vantagem é que podemos lidar com arquivos csv mais complexos; desta vez a entrada csv pode ter strings com ";", pode incluir campos de múltiplas linhas (a especificação csv é complexa!):
 1111,2,3
 "3,3,3",a,"b, c, and d"
 "a more, complex
        multiline record",3,4
  • para exemplificar uma etapa de processamento, o separador de campo foi alterado para "::"
  • para exemplificar etapas extras, adicionamos alguma otimização: como usamos um cache de dict, esse script é executado 100 vezes mais rápido do que minha outra solução.
por 10.02.2017 / 01:15
1

Esta não é uma resposta, mas apenas uma variante de evitar a rolagem da excelente resposta do IObO ...

awk -F, 'NR==1{h=$0; next} {print seen[$1]++ ? $0 : h "\n" $0 >$1 ".csv"}'
    
por 09.02.2017 / 16:34