Ejemplo

Ejemplo

Me pregunto si hay una manera fácil, tal vez una sola línea, con herramientas Unix cli para dividir un archivo CSV con marcas de tiempo UTC ISO-8601 con precisión de milisegundos ( +%FT%T.%3NZ, por ejemplo 2021-05-27T13:59:33.641Z) a lo largo de un desplazamiento/interrupción/diferencia de tiempo definido, como por ejemplo dos horas.

Como siempre, hay ciertas formas diferentes de tenerlo y, aunque para otros usuarios con preguntas similares, otras opciones también pueden ser relevantes en una respuesta completa, yo...

  • ... usar/tener git 2.31.1 GNU Bash 4.4.23, GNU sed 4.8( GNU Awk 5.0.0y todas las demás herramientas que incluye),xsv 0.13.0y jq 1.6en Windows 7
  • ... preferiría usar esto en un script que en un shell interactivo
  • ... use un punto y coma ( ;) como delimitador, sin coma
  • ... hacernotener mis valores citados (por ejemplo, entre comillas simples ( ') o dobles ( "))
  • ... no tiene encabezado
  • ... ya tendría todo el CSV en una variable y también querría tener el resultado en variables (¿una matriz?) para poder analizarlas más a fondo
  • Mis columnas lo hacennotienen una longitud fija en realidad y pueden contener espacios y guiones además de caracteres alfanuméricos
  • La marca de tiempo es la quinta de ocho columnas en mis datos del mundo real.
  • Se puede suponer que el archivo tiene como máximo 250.000 líneas y 20 MiB.
  • Si bien sería preferible que el script/comando tomara menos de medio segundo en mi i5-4300U, un máximo de 5 a 10 segundos aún no sería un factor decisivo.

Ejemplo

Si tuviera 2 hoursun desplazamiento para usar en mi división (y no mezclé nada), este archivo:

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

se dividiría en las siguientes tres partes

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

Descargo de responsabilidad: no soy un hablante nativo, así que si cambiar la redacción hace que esta pregunta sea más comprensible, hágalo. La verbosidad re. por ejemplo, especificar también las opciones que no se aplican a mi caso de uso (coma, comillas) o usar tanto la palabra semicoloncomo el signo ;en el texto de esta pregunta es para fines de SEO.

Respuesta1

Dados sus datos CSV de muestra en la 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"

salidas

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

Eso se puede leer en una matriz de shell como:

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")

Y luego interactuar sobre ellos como:

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

Respuesta2

El siguiente script en Perl generará el archivo de entrada y agregará una línea en blanco cada vez que vea una línea que no esté dentro de las 2 horas del período de inicio anterior, dividiendo la entrada en lotes de una duración máxima de 2 horas.

El período de inicio se establece al leer la primera línea y solo se actualiza cuando se imprime una línea en blanco adicional; esto es para garantizar un nuevo lote al menos cada 2 horas; de lo contrario, la entrada de muestra se dividiría en solo dos lotes (6 líneas de 14:15 a 18:15, y 2 líneas a las 21:12 y 21:15), y una entrada de registro adicional en, digamos, 16:45 y otra a, digamos, 20:00 evitaría cualquier división de su entrada de muestra .

Obtiene la fecha y hora del tercer campo de la entrada; tenga en cuenta que las matrices de Perl comienzan desde cero en lugar de uno, al igual que $F[2]el tercer campo de la matriz @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";
}

Salida de muestra:

$ ./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

Si necesita el resultado en archivos separados, puede hacer algo como esto:

#!/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";
}

Si desea que cualquiera de las versiones del script sea un poco más flexible con los formatos de hora que puede manejar, utilice:

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

Esto permite que tanto la fracción decimal como la Z sean opcionales en la cadena de tiempo.

Respuesta3

Con GNU awk para gensub()y 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

Respuesta4

Si todas las fechas del archivo pertenecen al mismo día:

#!/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

Puede editar el ho(desplazamiento de hora) en el BEGINbloque del script para dividirlo en el csv para otro desplazamiento de hora.


#!/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

información relacionada