Пример

Пример

Интересно, есть ли простой способ, может быть, в одну строку, с помощью инструментов командной строки Unix, чтобы разделить CSV-файл с временными метками UTC ISO-8601 с точностью до миллисекунд ( +%FT%T.%3NZнапример, 2021-05-27T13:59:33.641Z) по определенному смещению / разрыву / разнице во времени, например, два часа.

Как всегда, существуют различные способы получить ответ, и хотя для других пользователей с похожими вопросами другие варианты также могут оказаться полезными для комплексного ответа, я ...

  • ... использовать/иметь git 2.31.1 GNU Bash 4.4.23, GNU sed 4.8, GNU Awk 5.0.0(и все остальные инструменты, которые он включает),xsv 0.13.0и jq 1.6на Windows 7
  • ... предпочел бы использовать это в скрипте, а не в интерактивной оболочке
  • ... используйте точку с запятой ( ;) в качестве разделителя, без запятой
  • ... делатьнетзаключайте мои значения в кавычки (например, в одинарные ( ') или двойные ( "))
  • ... не имеют заголовка
  • ... уже имеет весь CSV в переменной и также хочет иметь результат в переменных (массиве?), чтобы иметь возможность дальнейшего анализа
  • Мои колонки делаютнетв действительности имеют фиксированную длину и могут содержать пробелы и дефисы помимо буквенно-цифровых символов
  • Временная метка — пятый из восьми столбцов в моих реальных данных.
  • Можно предположить, что файл содержит не более 250 тыс. строк и 20 МБ.
  • Хотя было бы предпочтительнее, чтобы выполнение скрипта/команды на моем i5-4300U занимало менее половины секунды, 5–10 секунд максимум все равно не будут решающим фактором.

Пример

Если бы у меня был 2 hoursсмещение для использования при моем разделении (и я ничего не перепутал), этот файл:

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

будет разделен на следующие три части

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

отказ от ответственности: я не носитель языка, поэтому если перефразирование делает этот вопрос более понятным, пожалуйста, сделайте это. Многословие, например, указание опций, которые не применимы к моему варианту использования (запятая, кавычки) или использование как слова, так и semicolonзнака ;в тексте этого вопроса, предназначено для целей SEO

решение1

Учитывая ваш пример данных CSV в переменной $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"

выходы

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

Это можно прочитать в массив оболочки следующим образом:

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

А затем взаимодействуйте с ними следующим образом:

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

решение2

Следующий скрипт Perl выведет входной файл, добавляя пустую строку каждый раз, когда он видит строку, которая не находится в пределах 2 часов от предыдущего начального периода, — разделяя входные данные на пакеты максимальной продолжительностью 2 часа.

Начальный период устанавливается при считывании первой строки и обновляется только при печати дополнительной пустой строки — это необходимо для того, чтобы обеспечить подачу новой партии не реже одного раза в 2 часа. В противном случае ваш образец входных данных будет разделен только на две партии (6 строк с 14:15 до 18:15 и 2 строки в 21:12 и 21:15), а дополнительная запись в журнале, скажем, в 16:45 и еще одна, скажем, в 20:00, предотвратит разделение вашего образца входных данных.

Он получает дату и время из третьего поля входных данных — обратите внимание, что массивы Perl начинаются с нуля, а не с единицы, как и $F[2]третье поле массива @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";
}

Пример вывода:

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

Если вам нужны выходные данные в отдельных файлах, вы можете сделать что-то вроде этого:

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

Если вы хотите, чтобы какая-либо из версий скрипта была немного более гибкой в ​​отношении поддерживаемых форматов времени, используйте:

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

Это позволяет сделать десятичную дробь и Z необязательными в строке времени.

решение3

С помощью GNU awk для gensub()и 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

решение4

Если все даты в файле относятся к одному и тому же дню:

#!/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(смещение часов) в BEGINблоке скрипта, чтобы разделить CSV-файл на другие смещения часов.


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

Связанный контент