Extrair registros de largura fixa sem delimitador de uma única linha

8

Eu preciso extrair strings de texto de um único arquivo contendo uma longa linha de texto sem delimitadores. Usando a linha de amostra abaixo, estes são os seguintes fatos conhecidos:

??????? A1XXXXXXXXXX ??????? B1XXXX ??????? A1XXXXXXXXXX ??????? C1XXXXXXX

1.  It contains 38 fixed width record types 
2.  The record marker is a 7 alphanumeric character followed by, for example, ‘A1’.
3.  Each record type has varying widths, for example, A1 record type will have 10 characters following it, if B1 then 4, and if C1 then 7.
4.  The record types aren’t clumped together and can be in any order. As in the example, its A1,B1,A1,C1
5.  The example above has 4 records and each record type needs to go to separate files. In this case 38 of them.

??????? A1XXXXXXXXXX

??????? B1XXXX

??????? A1XXXXXXXXXX

??????? C1XXXXXXX

6.  The record identifier, e.g. ????????A1, can appear in the body of the record so cannot use grep. 
7.  With the last point in mind, I was proposing 3 solutions but not sure on how to script this and of course would greatly appreciate some help. 
a. Traverse through the file from the beginning and sequentially strip out the record to the appropriate output file. For example, strip out first record type A1 to A1file which I know is 10 characters long then re-interrogate the file which will then have B1 which I know is 4 chars long, strip this out to B1file etc.. <<< this seems painful >>
b. Traverse through the file and append some obscure character to each record marker within the same file. Much like above but not strip out. I understand it still will use the same logic but seems more elegant
c. I did think of simply using the proposed grep -oE solution but then re-interrogate the output files to see if any of the 38 record markers exist anywhere other than at the beginning. But this might not always work.
    
por jags 13.12.2013 / 22:31

3 respostas

5

Que tal

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt

Imprime cada registro de cada tipo de registro em uma linha separada. Para redirecionar a saída grep para 3 arquivos chamados A1 , B1 , C1 respectivamente,

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt| 
awk -v OFS= -v FS= '{f=$1$2; $1=$2=""; print>f}'
    
por 13.12.2013 / 23:03
4

Aqui está uma solução possível usando o FPAT do gawk

BEGIN { 
    FPAT="A1.{10}|B1.{4}|C1.{7}" #define field contents
} 
{
    for(i=1;i<=NF;i++) 
        print $i >> substr($i,0,2) #print the field to file A1,B1,etc
}

Como um verso:

gawk 'BEGIN{FPAT="A1.{10}|B1.{4}|C1.{7}"} {for(i=1;i<=NF;i++)print $i >> substr($i,0,2)}' < datafile
    
por 13.12.2013 / 22:47
4

Em Perl:

#!/usr/bin/env perl

use strict;
use warnings;
use re qw(eval);

my %field_widths = (
    A1 => 10,
    B1 =>  4,
    C1 =>  7,
    #...(fill this up with the widths of your 38 record types)
);

# Make a regex of record types; sort with longest first as appropriate for
# ... regex alternation:
my $record_type_regex = join '|', sort { length($b) <=> length($a) } keys %field_widths; 

my %records;
my $marker_length=7; #Assuming the marker is 7 characters long
while(<>){
    chomp;
    while( # Parse each line of input
      m!
        (.{$marker_length})          # Match the record marker (save in $1)
        ($record_type_regex)         # Match any record type (save in $2)
        (
         (??{'.'x$field_widths{$2})} # Match a field of correct width
        )                            # Save in $3
       !xg){
        $records{$2}.="$1$2$3\n";
      }
}
for my $file (sort keys %records){
    open my $OUT,'>',$file or die "Failed to open $file for writing: $!\n";
    print $OUT $records{$file};
    close $OUT
}

Invoque como:

[user@host]$ ./myscript.pl file_of_data

Código testado e funciona com sua entrada específica.

Atualizar

Nos seus comentários, você solicitou um "equivalente Unix" dos itens acima. Eu duvido que exista tal coisa, uma vez que a expressão Perl usada para analisar sua linha é uma expressão altamente irregular e duvido que expressões regulares de baunilha possam analisar seu formato de dados: é muito semelhante a um tipo famoso de expressão que regex pode 't analisar (corresponde a qualquer número de a seguido pelo mesmo número de b ).

Em qualquer caso, a abordagem "Unix" mais próxima que eu posso encontrar é a generalização da resposta do 1_CR . Você deve observar que essa abordagem é específica para a implementação GNU de grep e, portanto, não funcionará na maioria dos Unices. A abordagem do Perl, ao contrário, deve funcionar da mesma forma em qualquer plataforma em que o Perl trabalhe. Aqui está a minha abordagem sugerida do GNU grep :

cat <<EOF \
| while read -r record width;do
    grep -oE ".{7}$record.{$width}" input_file\ #replace 7 with marker length
     >> "$record"
done
A1 10
B1 4
# enter your 38 record types
EOF

Atualizar

Com base nas solicitações do OP nos comentários, em vez de passar o nome do arquivo como um argumento de linha de comando, ele pode ser aberto no script da seguinte forma:

open my $IN,'<',$input_file_name or die "Failed to open $input_file: $!\n";
while(<$IN>){ #instead of while(<>)
...

Isso pressupõe que você tenha declarado a variável $input_file_name para conter, bem, o nome do arquivo de entrada.

Como para acrescentar um timestamp ao nome do arquivo de saída, você pode usar a sintaxe qx{} : entre as chaves, você pode colocar qualquer comando Unix que desejar e ele será executado e sua saída padrão será lida no lugar do% operadorqx{}:

open my $OUT,'>',"$file_".qx{date +%Y-%m-%d--%I:%M:%S%P}

O operador qx não está restrito a chaves, use seu caractere favorito como delimitador, apenas certifique-se de que ele não esteja no comando que você precisa executar:

qx<...>
qx(...)    
qx!...!    
qx@...@

e assim por diante ...

Em algum código Perl você pode ver backticks ( ' ' ) usados para servir esta função, similar ao que o shell faz. Basta pensar no operador qx como a generalização de backticks para qualquer delimitador.

Aliás, isso dará um timestamp ligeiramente diferente para cada arquivo (se a diferença de seus tempos de criação for um número finito de segundos). Se você não quiser isso, você pode fazer isso em duas etapas:

my $tstamp = qx{...};
open my $OUT,'>',"$file_$tstamp" or die...;
    
por 13.12.2013 / 23:49