Exemplo

Exemplo

Gostaria de saber se existe uma maneira fácil, talvez um liner, com ferramentas unix cli para dividir um arquivo CSV com carimbos de data e hora UTC ISO-8601 com precisão de milissegundos ( +%FT%T.%3NZpor exemplo 2021-05-27T13:59:33.641Z) ao longo de um deslocamento/intervalo/diferença de tempo definido, como por exemplo duas horas.

Como sempre, existem maneiras diferentes de fazer isso e, embora para outros usuários com perguntas semelhantes, outras opções também possam ser relevantes em uma resposta abrangente, eu ...

  • ... use/tenha git 2.31.1's GNU Bash 4.4.23, GNU sed 4.8, GNU Awk 5.0.0(e todas as outras ferramentas que ele inclui),xsv 0.13.0e jq 1.6no Windows 7
  • ... prefiro usar isso em um script que em um shell interativo
  • ... use ponto e vírgula ( ;) como delimitador, sem vírgula
  • ... fazernãoter meus valores entre aspas (por exemplo, entre aspas simples ( ') ou duplas ( "))
  • ... não tem cabeçalho
  • ... já teria todo o CSV em uma variável e também gostaria de ter o resultado em variáveis ​​(um array?) para poder analisá-las melhor
  • Minhas colunas fazemnãotêm comprimento fixo na realidade e podem conter espaços e hífens além de caracteres alfanuméricos
  • O carimbo de data/hora é a quinta de oito colunas nos meus dados do mundo real
  • Pode-se presumir que o arquivo tenha no máximo 250 mil linhas e 20 MiB
  • Embora fosse preferível que o script/comando demorasse menos de meio segundo no meu i5-4300U, 5 a 10 segundos no máximo ainda não seriam um problema

Exemplo

Se eu tivesse 2 hoursum deslocamento para usar na minha divisão (e não misturei nada), este arquivo:

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

seria dividido nas três partes seguintes

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

isenção de responsabilidade: não sou falante nativo, portanto, se a reformulação tornar esta pergunta mais compreensível, vá em frente. A verbosidade re. por exemplo, também especificar as opções que não se aplicam ao meu caso de uso (vírgula, aspas) ou usar a palavra semicolone o sinal ;no texto desta pergunta é para fins de SEO

Responder1

Dados seus dados CSV de amostra na variável $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"

saídas

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

Isso pode ser lido em uma 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")

E então interaja sobre eles 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

Responder2

O script perl a seguir gerará o arquivo de entrada, adicionando uma linha em branco sempre que encontrar uma linha que não esteja dentro de 2 horas do período inicial anterior - dividindo a entrada em lotes com duração máxima de 2 horas.

O período inicial é definido na leitura da primeira linha e atualizado somente quando uma linha extra em branco é impressa - isso é para garantir um novo lote pelo menos a cada 2 horas - caso contrário, sua entrada de amostra seria dividida em apenas dois lotes (6 linhas a partir de 14h15 às 18h15 e 2 linhas às 21h12 e 21h15), e uma entrada de log extra às 16h45 e outra às 20h evitaria qualquer divisão da sua entrada de amostra .

Ele obtém a data e hora do terceiro campo da entrada - observe que os arrays perl começam de zero em vez de um, assim como $F[2]o terceiro campo de array @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";
}

Exemplo de saída:

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

Se precisar da saída em arquivos separados, você pode fazer algo assim:

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

Se você quiser que qualquer versão do script seja um pouco mais flexível com os formatos de hora que ela pode suportar, use:

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

Isso permite que a fração decimal e o Z sejam opcionais na sequência de tempo.

Responder3

Com GNU awk para gensub()e 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

Responder4

Se todas as datas do arquivo pertencerem ao mesmo dia:

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

Você pode editar o ho(deslocamento de hora) no BEGINbloco do script para dividir no csv para outro deslocamento 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

informação relacionada