web-dev-qa-db-ja.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]

実際には、私の入力配列には300万近くの要素があり、すべてUUIDです。

12
Echo Nolan

次のストリーム指向のwindow/3の定義は、CédricConnes(github:connesc)によるもので、_nwiseを一般化し、ストリームの終わりのマーカーを使用する必要性を回避する「ボクシングテクニック」を示しています。なので、ストリームに非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;

サイズ300万のアレイの場合、これは非常にパフォーマンスが高くなります。古いMacでは3.91秒、最大常駐サイズは162746368です。

このバージョン(末尾呼び出し最適化再帰を使用)は、このページの他の場所に示されているforeachを使用したnwise/2のバージョンよりも実際に高速であることに注意してください。

8
peak

配列が大きすぎてメモリに収まりきらない場合は、@ CharlesDuffyによって提案された戦略を採用します。つまり、次のようなnwiseのストリーム指向バージョンを使用して、配列要素をjqの2番目の呼び出しにストリーミングします。

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 

したがって、シェルパイプラインは次のようになります。

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

このアプローチは非常に効果的です。 nwise/2を使用して、300万のアイテムのストリームを3つのグループにグループ化するには、

/usr/bin/time -lp

jqの2回目の呼び出しでは、次のようになります。

user         5.63
sys          0.04
   1261568  maximum resident set size

警告:この定義では、nanをストリーム終了マーカーとして使用しています。 nanはJSON値ではないため、これはJSONストリームの処理には問題になりません。

1
peak

以下は確かにハッカーです-しかしmemory-efficientハッカー、たとえ任意の長いリストであっても:

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

配列がアトミック値で構成されていると仮定して、入力JSONファイルのパイプラインストリームの最初の部分は要素ごとに1行出力します(ここで[]と{}はアトミック値として含まれています)。ストリーミングモードで実行されるため、単一のドキュメントであるにもかかわらず、コンテンツ全体をメモリに保存する必要はありません。

パイプラインの2番目の部分は、最大3つのアイテムを繰り返し読み取り、それらをリストにまとめます。

これにより、一度にメモリに4つ以上のデータが必要になることを回避できます。

1
Charles Duffy