Beispiel

Beispiel

Ich frage mich, ob es eine einfache Möglichkeit, vielleicht eine Einzeiler-Methode, mit Unix-CLI-Tools gibt, um eine CSV-Datei mit ISO-8601-UTC-Zeitstempeln in Millisekundenpräzision ( +%FT%T.%3NZz. B. 2021-05-27T13:59:33.641Z) entlang eines definierten Zeitversatzes/einer definierten Unterbrechung/Differenz, wie beispielsweise zwei Stunden, aufzuteilen.

Wie immer gibt es bestimmte verschiedene Möglichkeiten, dies zu erreichen, und während für andere Benutzer mit ähnlichen Fragen auch andere Optionen in einer umfassenden Antwort relevant sein könnten, ...

  • ... benutze/habe Git 2.31.1 GNU Bash 4.4.23, GNU sed 4.8, GNU Awk 5.0.0(und alle anderen darin enthaltenen Tools),xsv 0.13.0und jq 1.6unter Windows 7
  • ... würde dies lieber in einem Skript als in einer interaktiven Shell verwenden
  • ... verwenden Sie als Trennzeichen ein Semikolon ( ;), kein Komma
  • ... Tunnichtmeine Werte in Anführungszeichen setzen (z. B. in einfache ( ') oder doppelte Anführungszeichen ( "))
  • ... haben keinen Header
  • ...hätte die komplette CSV bereits in einer Variable und möchte das Ergebnis zusätzlich in Variablen (einem Array?) haben, um diese weiter analysieren zu können
  • Meine Spaltennichthaben in Wirklichkeit eine feste Länge und können neben alphanumerischen Zeichen auch Leerzeichen und Bindestriche enthalten
  • Der Zeitstempel ist die fünfte von acht Spalten in meinen Realweltdaten
  • Die Datei ist voraussichtlich maximal 250.000 Zeilen und 20 MiB groß
  • Obwohl es wünschenswert wäre, wenn das Skript/der Befehl auf meinem i5-4300U weniger als eine halbe Sekunde dauern würde, wären 5 bis 10 Sekunden maximal immer noch kein Dealbreaker

Beispiel

Wenn ich 2 hoursfür meine Teilung den Offset verwenden müsste (und nichts verwechselt hätte), wäre es diese Datei:

abc;square;2021-05-27T14:15:39.315Z
def;circle;2021-05-27T14:17:03.416Z
ghi;triang;2021-05-27T14:45:13.520Z
abc;circle;2021-05-27T15:25:47.624Z
ghi;square;2021-05-27T17:59:33.641Z
def;triang;2021-05-27T18:15:33.315Z
abc;circle;2021-05-27T21:12:13.350Z
ghi;triang;2021-05-27T21:15:31.135Z

würde in folgende drei Teile aufgeteilt werden

abc;square;2021-05-27T14:15:39.315Z
def;circle;2021-05-27T14:17:03.416Z
ghi;triang;2021-05-27T14:45:13.520Z
abc;circle;2021-05-27T15:25:47.624Z
ghi;square;2021-05-27T17:59:33.641Z
def;triang;2021-05-27T18:15:33.315Z
abc;circle;2021-05-27T21:12:13.350Z
ghi;triang;2021-05-27T21:15:31.135Z

Haftungsausschluss: Ich bin kein Muttersprachler. Wenn also eine Umformulierung diese Frage verständlicher macht, dann bitte. Die Ausführlichkeit, z. B. auch die Optionen anzugeben, die für meinen Anwendungsfall nicht gelten (Komma, Anführungszeichen) oder sowohl das Wort semicolonals auch das Zeichen ;im Text dieser Frage zu verwenden, dient SEO-Zwecken .

Antwort1

Angenommen, Ihre Beispiel-CSV-Daten in der Variable $csv:

gawk '
    function timestamp2epoch(ts,       m) {
        if(match(ts, /([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\..*/, m)) 
            return mktime(m[1] " " m[2] " " m[3] " " m[4] " " m[5] " " m[6])
        else
            return -1
    }

    BEGIN {
        FS = ";"
        interval = 2 * 3600     # 2 hours
    }

    { t = timestamp2epoch($3) }
    t > start + interval { start = t; n++ }
    { batch[n] = batch[n] (batch[n] == "" ? "" : "/") $0 }

    END {
        PROCINFO["sorted_in"] = "@ind_num_asc"
        for (i in batch)
            print batch[i]
    }
' <<<"$csv"

Ausgänge

abc;square;2021-05-27T14:15:39.315Z/def;circle;2021-05-27T14:17:03.416Z/ghi;triang;2021-05-27T14:45:13.520Z/abc;circle;2021-05-27T15:25:47.624Z
ghi;square;2021-05-27T17:59:33.641Z/def;triang;2021-05-27T18:15:33.315Z
abc;circle;2021-05-27T21:12:13.350Z/ghi;triang;2021-05-27T21:15:31.135Z

Dies kann in ein Shell-Array wie folgt eingelesen werden:

mapfile -t batches < <(gawk '...' <<<"$csv")
declare -p batches
declare -a batches=([0]="abc;square;2021-05-27T14:15:39.315Z/def;circle;2021-05-27T14:17:03.416Z/ghi;triang;2021-05-27T14:45:13.520Z/abc;circle;2021-05-27T15:25:47.624Z" [1]="ghi;square;2021-05-27T17:59:33.641Z/def;triang;2021-05-27T18:15:33.315Z" [2]="abc;circle;2021-05-27T21:12:13.350Z/ghi;triang;2021-05-27T21:15:31.135Z")

Und dann interagieren Sie damit etwa so:

for ((i = 0; i < "${#batches[@]}"; i++)); do
    IFS="/" read -ra records <<<"${batches[i]}"
    echo "batch $i"
    for record in "${records[@]}"; do echo "  $record"; done
    echo
done
batch 0
  abc;square;2021-05-27T14:15:39.315Z
  def;circle;2021-05-27T14:17:03.416Z
  ghi;triang;2021-05-27T14:45:13.520Z
  abc;circle;2021-05-27T15:25:47.624Z

batch 1
  ghi;square;2021-05-27T17:59:33.641Z
  def;triang;2021-05-27T18:15:33.315Z

batch 2
  abc;circle;2021-05-27T21:12:13.350Z
  ghi;triang;2021-05-27T21:15:31.135Z

Antwort2

Das folgende Perl-Skript gibt die Eingabedatei aus und fügt jedes Mal eine leere Zeile hinzu, wenn es eine Zeile findet, die nicht innerhalb von 2 Stunden nach dem vorherigen Startzeitraum liegt. Dadurch wird die Eingabe in Stapel mit einer maximalen Dauer von 2 Stunden aufgeteilt.

Der Startzeitraum wird beim Lesen der ersten Zeile festgelegt und nur aktualisiert, wenn eine zusätzliche leere Zeile gedruckt wird. Dadurch wird sichergestellt, dass mindestens alle 2 Stunden ein neuer Stapel vorliegt. Andernfalls würde Ihre Probeneingabe nur in zwei Stapel aufgeteilt (6 Zeilen von 14:15 bis 18:15 und 2 Zeilen um 21:12 und 21:15) und ein zusätzlicher Protokolleintrag beispielsweise um 16:45 und ein weiterer um beispielsweise 20:00 würden jede Aufteilung Ihrer Probeneingabe verhindern.

Es ruft Datum und Uhrzeit aus dem dritten Feld der Eingabe ab. Beachten Sie, dass Perl-Arrays bei Null und nicht bei Eins beginnen, und dies $F[2]gilt auch für das dritte Feld des Arrays @F.

#!/usr/bin/perl

use strict;
use Date::Parse;

my $start;

while(<>) {
  chomp;
  my $approx;
  my @F = split /;/;

  # approximate date/time to start of hour
  ($approx = $F[2]) =~ s/:\d\d:\d\d\.\d+Z$/:00:00/;

  my $now = str2time($approx);
  $start = $now if ($. == 1);

  if (($now - $start) > 7200) {
    $start = $now;
    print "\n";
  };
  print "$_\n";
}

Beispielausgabe:

$ ./split.pl input.csv 
abc;square;2021-05-27T14:15:39.315Z
def;circle;2021-05-27T14:17:03.416Z
ghi;triang;2021-05-27T14:45:13.520Z
abc;circle;2021-05-27T15:25:47.624Z

ghi;square;2021-05-27T17:59:33.641Z
def;triang;2021-05-27T18:15:33.315Z

abc;circle;2021-05-27T21:12:13.350Z
ghi;triang;2021-05-27T21:15:31.135Z

Wenn Sie die Ausgabe in separaten Dateien benötigen, können Sie stattdessen Folgendes tun:

#!/usr/bin/perl

use strict;
use Date::Parse;

my $start;

# output-file counter
my $fc = 1;
my $outfile = "file.$fc.csv";

open (my $fh, ">", $outfile) || die "couldn't open $outfile for write: $!\n";

while(<>) {
  chomp;
  my $approx;
  my @F = split /;/;

  # approximate date/time to start of hour
  ($approx = $F[2]) =~ s/:\d\d:\d\d\.\d+Z$/:00:00/;

  my $now = str2time($approx);
  $start = $now if ($. == 1);

  if (($now - $start) > 7200) {
    $start = $now;
    close($fh);
    $fc++;
    $outfile = "file.$fc.csv";
    open ($fh, ">", $outfile) || die "couldn't open $outfile for write: $!\n";
  };
  print $fh "$_\n";
}

Wenn Sie möchten, dass eine der Versionen des Skripts hinsichtlich der verarbeitbaren Zeitformate flexibler ist, verwenden Sie:

  ($approx = $F[2]) =~ s/:\d\d:\d\d(?:\.\d+)?Z?$/:00:00/;

Dadurch können sowohl die Dezimalstelle als auch das Z in der Zeitzeichenfolge optional sein.

Antwort3

Mit GNU awk für gensub()und mktime():

$ cat tst.awk
BEGIN {
    FS = ";"
    maxSecs = 2 * 60 * 60
    prevTime = -(maxSecs + 1)
}
{
    split($3,dt,/[.]/)
    dateHMS   = gensub(/[-T:]/," ","g",dt[1])
    currSecs  = mktime(dateHMS,1) "." dt[2]
    secsDelta = currTime - prevTime
    prevTime  = currTime
}
secsDelta > maxSecs {
    close(out)
    out = "out" (++numOut)
}
{ print > out }

$ awk -f tst.awk file

$ head out?
==> out1 <==
abc;square;2021-05-27T14:15:39.315Z
def;circle;2021-05-27T14:17:03.416Z
ghi;triang;2021-05-27T14:45:13.520Z
abc;circle;2021-05-27T15:25:47.624Z

==> out2 <==
ghi;square;2021-05-27T17:59:33.641Z
def;triang;2021-05-27T18:15:33.315Z

==> out3 <==
abc;circle;2021-05-27T21:12:13.350Z
ghi;triang;2021-05-27T21:15:31.135Z

Antwort4

Wenn alle Daten in der Datei zum selben Tag gehören:

#!/usr/bin/awk -f
BEGIN {
    FS=OFS=";"
    ho = 1
}

{
    # Split the last field in date and times
    split($NF, a, "T")

    # Get the hour from time
    h = a[2]
    sub(/:.*$/, "", h)
    
    if (lh == 0) lh = h+ho

    if (h > lh) {
        lh = h+ho
        print "\n"
    }
}1

hoSie können den (Stundenversatz) im Block des Skripts bearbeiten BEGIN, um die CSV-Datei für andere Stundenversätze aufzuteilen.


#!/usr/bin/awk -f
BEGIN {
    FS=OFS=";"

    # Set here the hour offset
    hour_offset = 1

    # Get the hour values in seconds
    ho = 60 * 60 * hour_offset
}

{
    sub(/Z$/, "", $NF)

    # Call /bin/date and translate the 'visual date' to
    # epoch timestamp.
    cmd="/bin/date -d " $NF " +%s"
    epoch=((cmd | getline line) > 0 ? line : -1)
    close(cmd)

    if (epoch == -1) {
        print "Date throw an error at : " NR;
        exit 1; 
    }

    # If the lh (last hour) is not set, set it
    # to the current value for the epoch time plus 
    # the chosen offset
    if (!lh) lh = epoch + ho

    # if the current offset less the the old hour processed is
    # greater then the offset you choose: update the offset and 
    # print the separator
    if (epoch - lh > ho) {
        lh = epoch + ho
        print ""
    }
}1

verwandte Informationen