Estrazione di record a larghezza fissa senza delimitatore da una singola riga


8

Devo estrarre stringhe di testo da un singolo file contenente una riga di testo molto lunga senza delimitatori. Utilizzando la riga di esempio riportata di seguito, questi sono i seguenti fatti noti:

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

Codice Perl refactored per tenere conto degli aggiornamenti. Si prega di vedere se aiuta.
Joseph R.

Grazie Giuseppe. Non conosco Perl, ma volevo chiarire che il file contiene solo 1 riga di testo, ovvero nessun ritorno a capo o interruzioni di riga. Volevo solo chiarirlo perché vedo nei tuoi commenti che sottintendi che il file ha più di 1 righe a meno che, come ho detto, non abbia letto male. Grazie molto.
jags

Questo non dovrebbe fare la differenza. Il codice Perl funzionerà allo stesso modo se è tutto su una riga o se ce ne sono diversi, purché ogni riga contenga un numero intero di record ben formati.
Joseph R.

Grazie mille Joseph. Ha funzionato. Testato se un marker è nel corpo del record e questo riferimento indietro lo supera. Qualcuno può offrire un equivalente Unix per favore?
jags

Si prega di guardare la mia risposta aggiornata.
Joseph R.,

Risposte:


5

Che ne dite di

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

Ciò stampa ogni record di ciascun tipo di record su una riga separata. Per reindirizzare grepl'uscita a 3 file denominati A1, B1, C1rispettivamente

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

Grazie mille per questo. Ti dispiace spiegare questi vari componenti di script e switch utilizzati in modo che io possa testare ed estendere per favore. Inoltre, come faccio ad aggiungere il modello di 9 secondi prima (che in realtà saranno caratteri alfanumerici di 7 caratteri). Grazie molto.
jags

Ho parlato troppo presto ... Avrei dovuto anche aggiungere 1 informazione vitale che era che il pattern.recordmarker potrebbe apparire nel resto del record, quindi è stato consigliato di eliminare un record alla volta in un file e reinterrogare il file che probabilmente significa che non posso usare grep.
jags

Inoltre, ho 2 possibili soluzioni. - attraversa il file, etichetta con un carattere oscuro per indicare l'inizio di un record valido. Sposta i caratteri X in base al tipo di record e usa lo stesso carattere oscuro per indicare il record successivo. Tuttavia diffidare di eventuali problemi di buffer. Pertanto, si aspetta che il nuovo output venga interrogato in questo modo "? \\ 9999999A1XXXXXXXXXX? \\ 9999999B1XXXX? \\ 9999999A1XXXXXXXXXX? \\ 9999999C1XXXXXXX" - usa il sol corrente ma poi cerca all'interno di ogni file di output se gli altri motivi appaiono diversi all'inizio
jags

@jags, potresti voler aggiornare la tua domanda originale con dati di esempio veramente rappresentativi, tutto diventa un po 'confuso
iruvar

Grazie 1_CR, ho inviato nuovamente la domanda. Grazie a tutti per il vostro aiuto. Più apprezzato
jags

4

Ecco una soluzione possibile utilizzando FPAT di 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
}

Come una linea:

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

Nota che FPATrichiede gawk versione 4. Vedi: linuxjournaldigital.com/linuxjournal/201109#pg98
Håkon Hægland

4

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

Invocalo come:

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

Codice testato e funziona con i dati forniti.

Aggiornare

Nei tuoi commenti, hai richiesto un "equivalente Unix" di quanto sopra. Dubito fortemente che esista una cosa del genere, poiché l'espressione Perl utilizzata per analizzare la tua linea è un'espressione altamente irregolare e dubito che le espressioni regolari vaniglia possano analizzare il tuo dato formato di dati: è troppo simile a un famoso tipo di espressione che regex può 'analizza (corrisponde a qualsiasi numero di a' seguito dallo stesso numero di b').

In ogni caso, l'approccio "Unix" più vicino che posso trovare è la generalizzazione della risposta di 1_CR . Si noti che questo approccio è specifico per l'implementazione GNU di grepe quindi non funzionerà sulla maggior parte degli Unices. L'approccio Perl, al contrario, dovrebbe funzionare allo stesso modo su qualsiasi piattaforma su cui Perl lavora. Ecco il mio grepapproccio GNU suggerito :

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

Aggiornare

In base alle richieste dell'OP nei commenti, invece di passare il nome file come argomento della riga di comando, può essere aperto all'interno dello script in questo modo:

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

Ciò presuppone che tu abbia dichiarato che la variabile $input_file_namecontiene, beh, il nome del file di input.

Per quanto riguarda l'aggiunta di un timestamp al nome del file di output, è possibile utilizzare la qx{}sintassi: tra le parentesi graffe è possibile inserire qualsiasi comando Unix desiderato e verrà eseguito e l'output standard verrà riletto al posto qx{}dell'operatore:

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

L' qxoperatore non è limitato alle parentesi graffe, usa il tuo personaggio preferito come delimitatore, assicurati solo che non sia nel comando che devi eseguire:

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

e così via...

In alcuni codici Perl potresti vedere backticks ( ` `) usato per servire questa funzione, simile a quello che fa la shell. Basti pensare qxall'operatore come alla generalizzazione dei backtick per qualsiasi delimitatore.

A proposito, questo darà un timestamp leggermente diverso a ciascun file (se la differenza dei loro tempi di creazione è un numero finito di secondi). Se non lo desideri, puoi farlo in due passaggi:

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

Ciao di nuovo .... comincio ad amare davvero il perl. Basta avere un paio di pezzetti. 1 . Come leggere il file invece di passare l'argomento della riga di comando. Tentativo di utilizzo della configurazione di esecuzione di Eclipse non riuscito. 2 . Come aggiungere del testo al file $ nome file di output. Più apprezzato
jags

@jags Benvenuti nel club :). Risposta aggiornata Vedi se aiuta.
Joseph R.

Grazie Giuseppe Tuttavia, per l'ultima richiesta, intendevo aggiungere effettivamente, ad esempio, date / timestamp al nome del file di output. Il codice corrente genera i file A1, B1 e C1. Mille grazie ancora.
jags

@jags che vedo. Si prega di vedere se l'aggiornamento aiuta.
Joseph R.,

Grazie come sempre Giuseppe. Tuttavia intendevo aggiungere il nome del file di output effettivo che in questo caso è attualmente A1, B1, C1, ovvero voglio aggiungere una data / data / ora, A1_ <data_di_data>, B1_ <data_di_data>, C1_ <data_di_data>. Grazie molto.
jags
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.