Agregar e agrupar o arquivo de texto em perl ou bash

3

Eu tenho um grande arquivo de texto (com 5m linhas) neste formato (4 colunas, separadas por ; ):

string1; string2; string3; userId

As primeiras 3 strings (SHA1s) formam uma única ID, chamada appId (para que possa ser simulada assim: appId; userId ). A segunda coluna (string2, ou segunda parte de appId ) pode ser composta de algumas partes separadas por vírgula , . O arquivo está classificado.

Eu gostaria de ter a lista de usuários de cada aplicativo na frente dele, assim:

entrada arquivo:

app1, user1
app1, user2
app1, user3
app2, user1

arquivo de saída :

app1: user1, user2, user3
app2: user1

parte do arquivo "real" entrada :

44a934ca4052b34e70f9cb03f3399c6f065becd0;bf038823f9633d25034220b9f10b68dd8c16d867;309;8ead5b3e0af5b948a6b09916bd271f18fe2678aa
44a934ca4052b34e70f9cb03f3399c6f065becd0;bf038823f9633d25034220b9f10b68dd8c16d867;309;a21245497cd0520818f8b14d6e405040f2fa8bc0
5c3eb56d91a77d6ee5217009732ff421e378f298;200000000000000001000000200000,6fd299187a5c347fe7eaab516aca72295faac2ad,e25ba62bbd53a72beb39619f309a06386dd381d035de372c85d70176c339d6f4;16;337556fc485cd094684a72ed01536030bdfae5bb
5c3eb56d91a77d6ee5217009732ff421e378f298;200000000000000001000000200000,6fd299187a5c347fe7eaab516aca72295faac2ad,e25ba62bbd53a72beb39619f309a06386dd381d035de372c85d70176c339d6f4;16;382f3aaa9a0347d3af9b35642d09421f9221ef7d
5c3eb56d91a77d6ee5217009732ff421e378f298;200000000000000001000000200000,6fd299187a5c347fe7eaab516aca72295faac2ad,e25ba62bbd53a72beb39619f309a06386dd381d035de372c85d70176c339d6f4;16;396529e08c6f8a98a327ee28c38baaf5e7846d14

O arquivo "real" output deve ter esta aparência:

44a934ca4052b34e70f9cb03f3399c6f065becd0;bf038823f9633d25034220b9f10b68dd8c16d867;309:8ead5b3e0af5b948a6b09916bd271f18fe2678aa, a21245497cd0520818f8b14d6e405040f2fa8bc0
5c3eb56d91a77d6ee5217009732ff421e378f298;200000000000000001000000200000,6fd299187a5c347fe7eaab516aca72295faac2ad,e25ba62bbd53a72beb39619f309a06386dd381d035de372c85d70176c339d6f4;16:337556fc485cd094684a72ed01536030bdfae5bb, 382f3aaa9a0347d3af9b35642d09421f9221ef7d, 396529e08c6f8a98a327ee28c38baaf5e7846d14

Como posso fazer isso?

Editar: Além disso, pode haver milhares de usuários por aplicativo, então, por quanto tempo uma linha pode ser? Existe alguma limitação para o comprimento da linha?

    
por Javad Sadeqzadeh 22.07.2014 / 13:42

4 respostas

3

Em Perl

perl -F';' -lane 'push @{$h{join ";",@F[0..2]}},$F[3];
                  END{
                    for(sort keys %h){
                        print "$_: ". join ",",@{$h{$_}};
                    }
                  }' your_file

Você deve ser capaz de fazer algo semelhante em awk usando matrizes associativas, mas eu não sou muito versado em awk , então não posso contribuir com código real.

Explicação

Aqui está uma versão expandida do código acima que usa o mínimo de "mágica" possível:

open($FH,"<","your_file");
while($line=<$FH>){ # For each line in the file (accomplished by -n)
    chomp $line; # Remove the newline at the end (done by -l)
    # The ; is set by -F and storing the split in @F done by -a
    @F = split /;/,$line # Split the line into fields on ;
    $app_id = join ";",@F[0..2]; # AppID is the first 3 fields
    push @{$h{$app_id}},$F[3]; # The 4th field is added onto the hash
} # The whole file has been read at this point.
foreach $key (sort keys %h){ # Sort the hash by AppID
     print "$key: " . join ",",@{h{$key}}."\n"; # Print the array values
     # The newline ("\n") added at the end is also done by -l
}

Agora só resta a declaração push para explicar em mais detalhes:

  • push é normalmente usado para adicionar elementos a uma variável de matriz. Por exemplo:

    push @a,$x
    

    acrescenta o conteúdo da variável $x ao array @a .

  • O loop que lê o arquivo linha por linha está preenchendo uma tabela de hash ( %h ). As chaves para o hash são os AppIDs e o valor que corresponde a cada chave é um array contendo todos os IDs de usuário associados a esse AppID. Este é um array anônimo (não tem nome); em Perl isso é implementado como uma referência de matriz (um pouco semelhante aos ponteiros C). E como o valor de %h que corresponde ao AppID $app_id é denotado por $h{$app_id} , o acréscimo na matriz Perl sigial ( @ ) trata o valor de hash como uma matriz (desrefere a referência da matriz) e envia o ID do usuário atual para ele.

  • Uma alternativa que pode parecer menos "Perlish" para você seria concatenar o quarto campo com o valor atual:

    while(...) { ... $h{$app_id} = $h{$app_id} . ",$F[3]" }
    foreach $key (sort keys %h) { print "$_: $h{$_}" }
    

    em que . em Perl é o operador de concatenação de strings.

Observe que, no código de explicação, omiti o wrapper perl -e '...' para que o realce da sintaxe possa chegar ao código e torná-lo mais legível.

    
por 22.07.2014 / 13:53
2

Como você declara que o arquivo está classificado, não deve ser possível usar um loop simples com memória apenas para a string anterior appId ? Mais ou menos como a abordagem sed de Qeole, mas evitando a sobrecarga de expressões regulares usando a função read delimitada por shell e a comparação de strings:

#!/bin/bash

appId=""

while IFS=\; read -r s1 s2 s3 userId; do
  if [ "$s1;$s2;$s3" == "$appId" ]; then
    printf ', %s' "$userId"
  else
    appId="$s1;$s2;$s3"
    printf '\n%s:%s' "$appId" "$userId"
  fi
done < yourfile
printf '\n'

NOTA: isso imprime uma nova linha adicional no início da saída, mas isso pode ser evitado com um mínimo de complexidade adicional. Bash deve ser razoavelmente rápido para esse tipo de coisa, mas se não, você pode reimplementar em praticamente qualquer linguagem de script similar.

    
por 22.07.2014 / 22:18
2

com sed :

sed 's/;/:\t/3;H;1h;x                                                                                        
s/^\(\([^:]*\):.*\)\n//                                                                                      
/\n/P;//g;h;$!d' <input |
tr : \n

Isso imprime:

44a934ca4052b34e70f9cb03f3399c6f065becd0;bf038823f9633d25034220b9f10b68dd8c16d867;309
        8ead5b3e0af5b948a6b09916bd271f18fe2678aa
        a21245497cd0520818f8b14d6e405040f2fa8bc0
5c3eb56d91a77d6ee5217009732ff421e378f298;200000000000000001000000200000,6fd299187a5c347fe7eaab516aca72295faac2ad,e25ba62bbd53a72beb39619f309a06386dd381d035de372c85d70176c339d6f4;16
        337556fc485cd094684a72ed01536030bdfae5bb
        382f3aaa9a0347d3af9b35642d09421f9221ef7d
        396529e08c6f8a98a327ee28c38baaf5e7846d14

Você pode descartar tr para manter os grupos na mesma linha. Os ids serão : delimitados por cólon nesse caso. Você também pode precisar substituir o \t escape na primeira linha com um literal <tab> character - ou você pode se sentir livre para remover o \t abs completamente - eles servem apenas para tornar a saída mais legível ( na minha opinião) e não são vitais para a função da regex de qualquer forma.

    
por 11.01.2015 / 06:49
1

A sed answer:

sed ': l;N;s/^\([^;]\+;[^;]\+;[^;:]\+\)[;:] *\(.*\)\n;\(.*\)/: , /;tl;P;D' input_file.txt

O arquivo é lido apenas uma vez, por isso o desempenho não deve ser tão ruim, mas não posso dizer mais do que isso.

Com detalhes:

sed ': l;        # Label l

     N;          # Add next line of input to pattern space

     s/^\([^;]\+;[^;]\+;[^;:]\+\)[;:] *\(.*\)\n;\(.*\)/: , /;
                 # If two lines in pattern space start with same AppID, then
                 # take user ID and append it to first line, then delete second line

         tl;     # If previous substitution succeeded, i.e. we scanned two lines with 
                 # same AppID, then loop to label l. Else go on…

     P;          # Print first line from pattern space (here there should be two lines
                 # in pattern space, starting with a different AppID)

     D;          # Delete first line of pattern space; start script again with
                 # remaining text in pattern space, or next input line if pattern
                 # space is empty
    ' input_file.txt

(Mas eu não tenho idéia sobre possíveis limitações para o tamanho da linha, desculpe.)

    
por 22.07.2014 / 14:12

Tags