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.%3NZ
z. 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.0
undjq 1.6
unter 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 hours
fü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 semicolon
als 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
ho
Sie 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