Extrahieren von Datensätzen mit fester Breite ohne Trennzeichen aus einer einzelnen Zeile

8

Ich muss Textzeichenfolgen aus einer einzelnen Datei extrahieren, die eine sehr lange Textzeile ohne Trennzeichen enthält. Anhand der folgenden Beispielzeile sind dies die folgenden bekannten Fakten:

??????? 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.
Zacken
quelle
Perl-Code überarbeitet, um Ihre Updates zu berücksichtigen. Bitte sehen Sie, ob es hilft.
Joseph R.
Danke Joseph. Ich kenne Perl nicht, wollte aber klarstellen, dass die Datei nur eine Textzeile enthält, dh keine Zeilenumbrüche oder Zeilenumbrüche. Ich wollte das nur klarstellen, weil ich in Ihren Kommentaren sehe, dass die Datei mehr als 1 Zeilen enthält, es sei denn, wie gesagt, ich habe dies falsch verstanden. Danke vielmals.
Zacken
Dies sollte keinen Unterschied machen. Der Perl-Code funktioniert gleich, wenn sich alles in einer Zeile befindet oder wenn mehrere vorhanden sind, solange jede Zeile eine ganzzahlige Anzahl wohlgeformter Datensätze enthält.
Joseph R.
Vielen Dank Joseph. Es hat funktioniert. Getestet mit, ob sich ein Datensatzmarker im Datensatzkörper befindet, und diese Rückreferenzierung überwindet dies. Kann jemand bitte ein Unix-Äquivalent anbieten?
Zacken
Bitte schauen Sie sich meine aktualisierte Antwort an.
Joseph R.

Antworten:

5

Wie wäre es mit

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

Dadurch wird jeder Datensatz jedes Datensatztyps in einer separaten Zeile gedruckt. Umleiten grepAusgabe auf 3 Dateien mit dem Namen A1, B1, C1bzw.,

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt| 
awk -v OFS= -v FS= '{f=$1$2; $1=$2=""; print>f}'
iruvar
quelle
Vielen Dank dafür. Haben Sie etwas dagegen, diese verschiedenen verwendeten Skriptkomponenten und Schalter zu erklären, damit ich sie bitte testen und erweitern kann? Wie füge ich auch das Muster von 9s davor hinzu (das in Wirklichkeit alphanumerische Zeichen mit einer Länge von 7 Zeichen sein wird)? Danke vielmals.
Zacken
Zu früh gesprochen ... Ich hätte auch 1 wichtige Information hinzufügen sollen, nämlich, dass der pattern.recordmarker möglicherweise im Rest des Datensatzes angezeigt wird. Es wurde daher empfohlen, jeweils einen Datensatz in eine Datei zu entfernen und die Datei erneut abzufragen, was wahrscheinlich der Fall ist bedeutet, ich kann grep nicht verwenden.
Zacken
Außerdem habe ich 2 mögliche Lösungen. - Durchlaufen Sie die Datei und beschriften Sie sie mit einem dunklen Zeichen, um den Beginn eines gültigen Datensatzes anzuzeigen. Verschieben Sie X Zeichen je nach Datensatztyp und verwenden Sie dasselbe dunkle Zeichen, um den nächsten Datensatz zu kennzeichnen. Allerdings vorsichtig bei Pufferproblemen. Daher erwarten neue Ausgabe zu verhören wie diese suchen „\\ 9999999A1XXXXXXXXXX \\ 9999999B1XXXX \\ 9999999A1XXXXXXXXXX \\ 9999999C1XXXXXXX???“ - Einsatz aktuelle Sol aber dann innerhalb jeder Ausgabedatei suchen , wenn die anderen Muster andere erscheinen als zu Beginn
jags
@jags, vielleicht möchten Sie Ihre ursprüngliche Frage mit wirklich repräsentativen Beispieldaten aktualisieren, es wird alles ein bisschen verwirrend
iruvar
Vielen Dank 1_CR, ich habe die Frage erneut eingereicht. Vielen Dank für Ihre Hilfe. Am meisten geschätzt.
Zacken
4

Hier ist eine mögliche Lösung mit gawks FPAT

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
}

Als Einzeiler:

gawk 'BEGIN{FPAT="A1.{10}|B1.{4}|C1.{7}"} {for(i=1;i<=NF;i++)print $i >> substr($i,0,2)}' < datafile
rzymek
quelle
Beachten Sie, FPATdass gawk Version 4 erforderlich ist. Siehe: 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
}

Rufen Sie es auf als:

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

Code getestet und funktioniert mit Ihrer angegebenen Eingabe.

Aktualisieren

In Ihren Kommentaren haben Sie ein "Unix-Äquivalent" der oben genannten angefordert. Ich bezweifle sehr, dass es so etwas gibt, da der Perl-Ausdruck, der zum Parsen Ihrer Zeile verwendet wird, ein sehr unregelmäßiger Ausdruck ist, und ich bezweifle, dass reguläre Vanille-Ausdrücke Ihr gegebenes Datenformat analysieren können: Er ist einem berühmten Ausdruckstyp zu ähnlich, den Regex kann 't parse (entspricht einer beliebigen Anzahl von a' s, gefolgt von der gleichen Anzahl von b's).

In jedem Fall ist der nächste "Unix" -Ansatz, den ich finden kann, die Verallgemeinerung der Antwort von 1_CR . Sie sollten beachten, dass dieser Ansatz spezifisch für die GNU-Implementierung von ist grepund daher auf den meisten Unices nicht funktioniert. Im Gegensatz dazu sollte der Perl-Ansatz auf jeder Plattform, auf der Perl arbeitet, gleich funktionieren. Hier ist mein vorgeschlagener GNU- grepAnsatz:

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

Aktualisieren

Basierend auf den Anforderungen des OP in den Kommentaren kann der Dateiname nicht als Befehlszeilenargument übergeben werden, sondern im Skript wie folgt geöffnet werden:

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

Dies setzt voraus, dass Sie die Variable so deklariert haben $input_file_name, dass sie den Namen der Eingabedatei enthält.

Zum Anhängen eines Zeitstempels an den Namen der Ausgabedatei können Sie die folgende qx{}Syntax verwenden: Zwischen den geschweiften Klammern können Sie einen beliebigen Unix-Befehl einfügen, der ausgeführt wird, und die Standardausgabe wird anstelle des qx{}Operators zurückgelesen:

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

Der qxOperator ist nicht auf geschweifte Klammern beschränkt. Verwenden Sie Ihr Lieblingszeichen als Trennzeichen. Stellen Sie nur sicher, dass es nicht in dem Befehl enthalten ist, den Sie ausführen müssen:

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

und so weiter...

In einigen Perl-Codes sehen Sie möglicherweise backticks ( ` `), die stattdessen für diese Funktion verwendet werden, ähnlich wie die Shell. qxStellen Sie sich den Operator einfach als Verallgemeinerung von Backticks auf ein Trennzeichen vor.

Übrigens gibt dies jeder Datei einen etwas anderen Zeitstempel (wenn die Differenz ihrer Erstellungszeiten zufällig eine endliche Anzahl von Sekunden ist). Wenn Sie dies nicht möchten, können Sie dies in zwei Schritten tun:

my $tstamp = qx{...};
open my $OUT,'>',"$file_$tstamp" or die...;
Joseph R.
quelle
Hallo nochmal .... fange an Perl wirklich zu lieben. Haben Sie nur ein paar Kleinigkeiten. 1 . Lesen der Datei im Gegensatz zum Übergeben des Befehlszeilenarguments. Eclipse-Ausführungskonfiguration wird versucht, aber nicht verwendet. 2 . So hängen Sie Text an den Ausgabedateinamen $ file an. Am meisten geschätzt.
Zacken
@jags Willkommen im Club :). Antwort aktualisiert. Sehen Sie, ob es hilft.
Joseph R.
Danke Joseph. Für die letzte Anfrage wollte ich jedoch tatsächlich Datum / Zeitstempel an den Ausgabedateinamen anhängen. Der aktuelle Code gibt die Dateien A1, B1 und C1 aus. Vielen Dank nochmal.
Zacken
@jags Ich verstehe. Bitte sehen Sie, ob das Update hilft.
Joseph R.
Danke wie immer Joseph. Ich wollte jedoch an den tatsächlichen Ausgabedateinamen anhängen, der in diesem Fall derzeit A1, B1, C1 ist, dh ich möchte einen Datums- / Zeitstempel hinzufügen, A1_ <Today_Datum>, B1_ <Today_Datum>, C1_ <Today_Datum>. Danke vielmals.
Zacken