it-roy-ru.com

Как разбить массив на куски с помощью jq?

У меня есть очень большой файл JSON, содержащий массив. Можно ли использовать jq, чтобы разбить этот массив на несколько меньших массивов фиксированного размера? Предположим, мой ввод был таким: [1,2,3,4,5,6,7,8,9,10], и я хотел разделить его на 3 элемента по длине. Желаемый вывод из jq будет:

[1,2,3]
[4,5,6]
[7,8,9]
[10]

В действительности мой входной массив содержит почти три миллиона элементов, все UUID.

9
Echo Nolan

Следующее потоково-ориентированное определение window/3, обусловленное Седриком Коннесом (Github: connesc), обобщает _nwise, И иллюстрирует «Технику бокса», которая обходит необходимость использования End- маркера of-stream, и поэтому его можно использовать , если поток содержит не-JSON-значение nan. Определение _nwise/1 в терминах window/3 также включено.

Первый аргумент window/3 интерпретируется как поток. $ size - это размер окна, а $ step указывает количество пропускаемых значений. Например,

window(1,2,3; 2; 1)

Результаты:

[1,2]
[2,3]

window/3 и _nsize/1

def window(values; $size; $step):
  def checkparam(name; value): if (value | isnormal) and value > 0 and (value | floor) == value then . else error("window \(name) must be a positive integer") end;
  checkparam("size"; $size)
| checkparam("step"; $step)
  # We need to detect the end of the loop in order to produce the terminal partial group (if any).
  # For that purpose, we introduce an artificial null sentinel, and wrap the input values into singleton arrays in order to distinguish them.
| foreach ((values | [.]), null) as $item (
    {index: -1, items: [], ready: false};
    (.index + 1) as $index
    # Extract items that must be reused from the previous iteration
    | if (.ready | not) then .items
      Elif $step >= $size or $item == null then []
      else .items[-($size - $step):]
      end
    # Append the current item unless it must be skipped
    | if ($index % $step) < $size then . + $item
      else .
      end
    | {$index, items: ., ready: (length == $size or ($item == null and length > 0))};
    if .ready then .items else empty end
  );

def _nwise($n): window(.[]; $n; $n);

Источник:

https://Gist.github.com/connesc/d6b87cbacae13d4fd58763724049da58

2
peak

Существует (недокументированное) встроенное _nwise, которое отвечает функциональным требованиям:

$ jq -nc '[1,2,3,4,5,6,7,8,9,10] | _nwise(3)'

[1,2,3]
[4,5,6]
[7,8,9]
[10]

Также:

$ jq -nc '_nwise([1,2,3,4,5,6,7,8,9,10];3)' 
[1,2,3]
[4,5,6]
[7,8,9]
[10]

Кстати, _nwise можно использовать как для массивов, так и для строк.

(Я считаю, что это недокументировано, потому что были некоторые сомнения по поводу подходящего имени.)

TCO-версия

К сожалению, встроенная версия небрежно определена и не будет хорошо работать для больших массивов. Вот оптимизированная версия (она должна быть примерно такой же эффективной, как нерекурсивная версия):

def nwise($n):
 def _nwise:
   if length <= $n then . else .[0:$n] , (.[$n:]|_nwise) end;
 _nwise;

Для массива размером 3 миллиона это вполне производительно: 3,91 на старом Mac, максимальный размер резидента 162746368.

Обратите внимание, что эта версия (с использованием рекурсии, оптимизированной для хвостового вызова) на самом деле быстрее, чем версия nwise/2 с использованием foreach, показанная в другом месте на этой странице.

7
peak

Если массив слишком велик, чтобы удобно помещаться в памяти, я бы принял стратегию, предложенную @CharlesDuffy, то есть направил элементы массива во второй вызов jq, используя ориентированную на поток версию nwise, такую ​​как:

def nwise(stream; $n):
  foreach (stream, nan) as $x ([];
    if length == $n then [$x] else . + [$x] end;
    if (.[-1] | isnan) and length>1 then .[:-1]
    Elif length == $n then .
    else empty
    end);

«Драйвер» для вышеупомянутого будет:

nwise(inputs; 3)

Но, пожалуйста, не забудьте использовать параметр командной строки -n.

Чтобы создать поток из произвольного массива:

$ jq -cn --stream '
    fromstream( inputs | (.[0] |= .[1:])
                | select(. != [[]]) )' huge.json 

Таким образом, конвейер Shell может выглядеть так:

$ jq -cn --stream '
    fromstream( inputs | (.[0] |= .[1:])
                | select(. != [[]]) )' huge.json |
  jq -n -f nwise.jq

Этот подход довольно производительный. Для группировки потока из 3 миллионов элементов в группы по 3 с помощью nwise/2,

/usr/bin/time -lp

для второго вызова JQ дает:

user         5.63
sys          0.04
   1261568  maximum resident set size

Предостережение: это определение использует nan в качестве маркера конца потока. Поскольку nan не является значением JSON, это не может быть проблемой для обработки потоков JSON.

1
peak

Ниже приведено описание хакерских атак, но эффективное использование памяти хакерских атак, даже с произвольно длинным списком:

jq -c --stream 'select(length==2)|.[1]' <huge.json \
| jq -nc 'foreach inputs as $i (null; null; [$i,try input,try input])'

Первый фрагмент конвейерного потока во входном файле JSON, испускающий одну строку на элемент, предполагая, что массив состоит из атомарных значений (где [] и {} здесь включены как атомарные значения). Поскольку он работает в потоковом режиме, ему не нужно хранить весь контент в памяти, несмотря на то, что он является одним документом.

Второй фрагмент конвейера многократно считывает до трех элементов и собирает их в список.

Это должно избегать необходимости более трех частей данных в памяти одновременно.

1
Charles Duffy