Extracción de registros de ancho fijo sin delimitador de una sola línea


8

Necesito extraer cadenas de texto de un solo archivo que contiene una línea de texto muy larga sin delimitadores. Usando la siguiente línea de muestra, estos son los siguientes hechos conocidos:

??????? 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.

Código Perl refactorizado para tener en cuenta sus actualizaciones. Por favor, vea si ayuda.
Joseph R.

Gracias Joseph No conozco a Perl, pero quería dejar en claro que el archivo contiene solo 1 línea de texto, es decir, no hay retornos de carro ni saltos de línea. Solo quería aclarar eso porque veo en sus comentarios que implica que el archivo tiene más de 1 líneas, a menos que, como dije, haya leído mal esto. Muchas gracias.
jags

Esto no debería hacer la diferencia. El código Perl funcionará igual si está todo en una línea o si hay varios, siempre que cada línea contenga un número entero de registros bien formados.
Joseph R.

Muchas gracias Joseph. Ha funcionado. Probado con si un marcador de registro está en el cuerpo del registro y esta referencia inversa supera eso. ¿Alguien puede ofrecer un equivalente de Unix por favor?
jags

Por favor, mire mi respuesta actualizada.
Joseph R.

Respuestas:


5

Qué tal si

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

Esto imprime cada registro de cada tipo de registro en una línea separada. Para redirigir grepla salida a 3 archivos con el nombre 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}'

Muchas gracias por esto. ¿Le importaría explicar estos diversos componentes de script y modificadores utilizados para que pueda probar y extender por favor? Además, ¿cómo agrego el patrón de 9 antes (que en realidad serán caracteres alfanuméricos de 7 caracteres de longitud). Muchas gracias.
jags

Hablé demasiado pronto ... También debería haber agregado 1 información vital que era que el pattern.recordmarker podría aparecer en el resto del registro, por lo que se nos aconseja que eliminemos un registro a la vez en un archivo y reinterroguemos el archivo que probablemente significa que no puedo usar grep.
jags

Además, tengo 2 posibles soluciones. - atravesar el archivo, etiquetar con un carácter oscuro para indicar el inicio de un registro válido. Mueva los caracteres X según el tipo de registro y use el mismo carácter oscuro para denotar el siguiente registro. Sin embargo, desconfíe de cualquier problema de búfer. Por lo tanto, se espera que la nueva salida interrogue con este aspecto "? \\ 9999999A1XXXXXXXXXX? \\ 9999999B1XXXX? \\ 9999999A1XXXXXXXXXX? \\ 9999999C1XXXXXXX" - use el sol actual pero luego busque dentro de cada archivo de salida si los otros patrones aparecen aparte del principio
jags

@jags, es posible que desee actualizar su pregunta original con datos de muestra verdaderamente representativos, todo se está
volviendo

Gracias 1_CR, he vuelto a enviar la pregunta. Gracias por toda tu ayuda. Más apreciado.
jags

4

Aquí hay una posible solución usando 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 una línea:

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

Tenga en cuenta que FPATrequiere Gawk versión 4. Ver: linuxjournaldigital.com/linuxjournal/201109#pg98
Håkon Hægland

4

En 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
}

Invocarlo como:

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

Código probado y funciona con su entrada dada.

Actualizar

En sus comentarios, solicitó un "equivalente de Unix" de lo anterior. Dudo mucho que exista tal cosa, ya que la expresión Perl utilizada para analizar su línea es una expresión muy irregular y dudo que las expresiones regulares vainilla puedan analizar su formato de datos dado: es demasiado similar a un tipo famoso de expresión que regex puede 't parse (coincide con cualquier número de a' s seguido por el mismo número de b's).

En cualquier caso, el enfoque "Unix" más cercano que puedo encontrar es la generalización de la respuesta de 1_CR . Debe tener en cuenta que este enfoque es específico para la implementación de GNU grepy, por lo tanto, no funcionará en la mayoría de los Unices. El enfoque de Perl, por el contrario, debería funcionar igual en cualquier plataforma en la que Perl trabaje. Aquí está mi grepenfoque sugerido de GNU :

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

Actualizar

Según las solicitudes del OP en los comentarios, en lugar de pasar el nombre del archivo como un argumento de línea de comando, se puede abrir dentro del script de la siguiente manera:

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

Esto supone que ha declarado que la variable $input_file_namecontiene, bueno, el nombre del archivo de entrada.

En cuanto a agregar una marca de tiempo al nombre del archivo de salida, puede usar la qx{}sintaxis: entre las llaves puede colocar cualquier comando de Unix que desee y se ejecutará y su salida estándar se leerá en lugar del qx{}operador:

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

El qxoperador no está restringido a llaves, use su personaje favorito como delimitador, solo asegúrese de que no esté en el comando que necesita ejecutar:

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

y así...

En algunos códigos de Perl, puede ver backticks ( ` `) utilizados para servir esta función, similar a lo que hace el shell. Solo piense en el qxoperador como la generalización de los backticks a cualquier delimitador.

Por cierto, esto le dará una marca de tiempo ligeramente diferente a cada archivo (si la diferencia de sus tiempos de creación es un número finito de segundos). Si no quieres esto, puedes hacerlo en dos pasos:

my $tstamp = qx{...};
open my $OUT,'>',"$file_$tstamp" or die...;

Hola de nuevo ... comenzando a amar realmente a Perl Solo tiene un par de trozos curiosos. 1 . Cómo leer en el archivo en lugar de pasar en el argumento de la línea de comando. Intentando pero sin poder utilizar la configuración de ejecución de Eclipse. 2 . Cómo agregar texto al archivo de salida $ file. Más apreciado.
jags

@jags Bienvenido al club :). Respuesta actualizada A ver si ayuda.
Joseph R.

Gracias Joseph Sin embargo, para la última solicitud, tenía la intención de agregar, por ejemplo, fecha / hora al nombre del archivo de salida. El código actual genera los archivos A1, B1 y C1. Muchas gracias de nuevo.
jags

@jags ya veo. Por favor, vea si la actualización ayuda.
Joseph R.

Gracias como siempre Joseph. Sin embargo, me refería a agregar al nombre de archivo de salida real que en este caso es actualmente A1, B1, C1, es decir, quiero agregar una fecha / marca de tiempo, A1_ <fecha de día>, B1_ <fecha de día>, C1_ <fecha de día>. Muchas gracias.
jags
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.