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

8

Preciso extrair cadeias de texto de um único arquivo contendo uma linha muito longa 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 arent 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.
entalhes
fonte
Código Perl refatorado para levar em consideração suas atualizações. Por favor, veja se isso ajuda.
Joseph R.
Obrigado Joseph. Não conheço Perl, mas queria deixar claro que o arquivo contém apenas 1 linha de texto, ou seja, nenhum retorno de carro ou quebra de linha. Só queria deixar isso claro, porque vejo nos seus comentários que você implica que o arquivo tem mais de uma linha, a menos que, como eu disse, tenha interpretado errado. Muito Obrigado.
Entalhes
Isso não deve fazer diferença. O código Perl funcionará da mesma forma se estiver tudo em uma linha ou se houver várias, desde que cada linha contenha um número inteiro de registros bem formados.
Joseph R.
Muito obrigado, Joseph. Funcionou. Testado com se um marcador de registro está no corpo do registro e essa referência posterior supera isso. Alguém pode oferecer um equivalente Unix, por favor?
quer
Por favor, olhe minha resposta atualizada.
Joseph R.

Respostas:

5

E se

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

Isso imprime cada registro de cada tipo de registro em uma linha separada. Para redirecionar grepa saída para 3 arquivos nomeados A1, B1, C1respectivamente,

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt| 
awk -v OFS= -v FS= '{f=$1$2; $1=$2=""; print>f}'
iruvar
fonte
Muito obrigado por isso. Você se importa de explicar esses vários componentes de script e switches usados ​​para que eu possa testar e estender, por favor. Além disso, como adiciono o padrão de 9s antes dele (que na realidade serão caracteres alfanuméricos de 7 caracteres). Muito Obrigado.
Entalhes
Falei cedo demais ... Eu também deveria ter adicionado uma informação vital, que indicava que o pattern.recordmarker poderia aparecer no restante do registro, por isso é aconselhável remover um registro de cada vez em um arquivo e reinterrogar o arquivo que provavelmente significa que não posso usar grep.
Entalhes
Além disso, tenho 2 soluções possíveis. - percorrer o arquivo, rotular com um caracter obscuro para indicar o início do registro válido. Mova X caracteres dependendo do tipo de registro e use o mesmo caractere obscuro para indicar o próximo registro. No entanto, desconfie de quaisquer problemas no buffer. Portanto, esperando que uma nova saída seja interrogada com esta aparência "? \\ 9999999A1XXXXXXXXXX? \\ 9999999B1XXXX? \\ 9999999A1XXXXXXXXXX? \\ 9999999C1XXXXXXX" - use o sol atual, mas pesquise em cada arquivo de saída se os outros padrões aparecerem diferentes do início
jags
@jags, você pode querer atualizar sua pergunta original com dados de amostra verdadeiramente representativas, é tudo ficando um pouco confuso
Iruvar
Obrigado 1_CR, reenviei a pergunta. Obrigado a todos por sua ajuda. Mais apreciado.
Entalhes
4

Aqui está uma solução possível usando o FPAT de 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 uma linha:

gawk 'BEGIN{FPAT="A1.{10}|B1.{4}|C1.{7}"} {for(i=1;i<=NF;i++)print $i >> substr($i,0,2)}' < datafile
rzymek
fonte
Note que FPATrequer a versão
gawk
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-o como:

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

Código testado e funciona com a sua entrada.

Atualizar

Nos seus comentários, você solicitou um "equivalente Unix" acima. Eu duvido muito que exista isso, pois 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' s seguido pelo mesmo número de b's).

De qualquer forma, a abordagem "Unix" mais próxima que posso encontrar é a generalização da resposta do 1_CR . Você deve observar que essa abordagem é específica para a implementação do GNU grepe, portanto, não funcionará na maioria dos Unices. A abordagem Perl, pelo contrário, deve funcionar da mesma maneira em qualquer plataforma na qual o Perl trabalha. Aqui está minha grepabordagem GNU sugerida :

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 argumento da linha de comando, ele pode ser aberto no script da seguinte maneira:

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 que a variável $input_file_namecontém, bem, o nome do arquivo de entrada.

Quanto a acrescentar um carimbo de data / hora ao nome do arquivo de saída, você pode usar a qx{}sintaxe: entre chaves, você pode colocar qualquer comando Unix que desejar, e ele será executado e sua saída padrão será lida no lugar do qx{}operador:

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

O qxoperador não está restrito a chaves, use seu personagem favorito como delimitador, apenas verifique se ele não está no comando que você precisa executar:

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

e assim por diante...

Em algum código Perl, você pode ver os backticks ( ` `) usados ​​para servir essa função, semelhante ao que o shell faz. Pense no qxoperador como a generalização dos backticks para qualquer delimitador.

A propósito, isso fornecerá um registro de data e hora 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, poderá fazê-lo em duas etapas:

my $tstamp = qx{...};
open my $OUT,'>',"$file_$tstamp" or die...;
Joseph R.
fonte
Oi novamente .... começando a realmente amar perl. Apenas tenha alguns pedaços pequenos. 1 . Como ler o arquivo em vez de passar no argumento da linha de comando. Tentando, mas falhando, em usar a configuração de execução do Eclipse. 2 . Como anexar algum texto ao arquivo de saída $ file. Mais apreciado.
Entalhes
@jags Bem-vindo ao clube :). Resposta atualizada. Veja se isso ajuda.
Joseph R.
Obrigado Joseph. No entanto, para a última solicitação, pretendi acrescentar, por exemplo, data / carimbo de data / hora ao nome do arquivo de saída. O código atual gera os arquivos A1, B1 e C1. Muito obrigado novamente.
Entalhes
@ jags eu vejo. Por favor, veja se a atualização ajuda.
Joseph R.
Obrigado como sempre, Joseph. No entanto, eu quis acrescentar o nome do arquivo de saída real que, neste caso, é atualmente A1, B1, C1, ou seja, eu quero adicionar um carimbo de data / hora, A1_ <data_da_todia>, B1_ <data_todia>, C1_ <data_data>. Muito Obrigado.
Entalhes