it-swarm.com.de

Wie kann ich ein Array mit jq in Blöcke aufteilen?

Ich habe eine sehr große JSON-Datei, die ein Array enthält. Ist es möglich, jq zu verwenden, um dieses Array in mehrere kleinere Arrays fester Größe aufzuteilen? Angenommen, meine Eingabe war Folgendes: [1,2,3,4,5,6,7,8,9,10], und ich wollte es in drei lange Elemente teilen. Die gewünschte Ausgabe von jq wäre:

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

In Wirklichkeit hat mein Eingabearray fast drei Millionen Elemente, alles UUIDs.

9
Echo Nolan

Die folgende strömungsorientierte Definition von window/3 aufgrund von Cédric Connes .__ (github: connesc) verallgemeinert _nwise, Und veranschaulicht eine of-stream marker und kann daher verwendet werden , wenn der Stream den Nicht-JSON-Wert nan enthält. Eine Definition .__ von _nwise/1 in Bezug auf window/3 ist ebenfalls enthalten.

Das erste Argument von window/3 wird als Stream interpretiert. $ size ist die Fenstergröße und $ step gibt die Anzahl der zu überspringenden Werte an. Zum Beispiel,

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

ergibt:

[1,2]
[2,3]

fenster/3 und _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);

Quelle:

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

2
peak

Es gibt ein (undokumentiertes) _nwise, das die funktionalen Anforderungen erfüllt:

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

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

Ebenfalls:

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

Übrigens kann _nwise sowohl für Arrays als auch für Strings verwendet werden.

(Ich glaube, es ist undokumentiert, weil es Zweifel an einem passenden Namen gab.)

TCO-Version

Leider ist die eingebaute Version leichtsinnig definiert und wird bei großen Arrays nicht gut funktionieren. Hier ist eine optimierte Version (sie sollte ungefähr so ​​effizient sein wie eine nicht rekursive Version):

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

Für ein Array mit einer Größe von 3 Millionen ist dies ziemlich performant: 3.91s auf einem alten Mac, 162746368 max resident size.

Beachten Sie, dass diese Version (unter Verwendung der für den Endaufruf optimierten Rekursion) tatsächlich schneller ist als die an anderer Stelle auf dieser Seite angegebene Version von nwise/2 mit foreach.

7
peak

Wenn das Array zu groß ist, um bequem in den Speicher zu passen, würde ich die von @CharlesDuffy vorgeschlagene Strategie übernehmen. Das heißt, die Array-Elemente werden in einen zweiten Aufruf von jq gestreamt, wobei eine strömungsorientierte Version von nwise verwendet wird, beispielsweise:

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

Der "Treiber" für das Obige wäre:

nwise(inputs; 3)

Denken Sie jedoch daran, die Befehlszeilenoption -n zu verwenden.

So erstellen Sie den Stream aus einem beliebigen Array:

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

Die Shell-Pipeline könnte also so aussehen:

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

Dieser Ansatz ist sehr performant. Zur Gruppierung eines Streams von 3 Millionen Elementen in Gruppen von 3 mit nwise/2

/usr/bin/time -lp

für den zweiten Aufruf von jq ergibt sich:

user         5.63
sys          0.04
   1261568  maximum resident set size

Vorbehalt: Diese Definition verwendet nan als End-of-Stream-Marker. Da nan kein JSON-Wert ist, kann dies für die Verarbeitung von JSON-Streams kein Problem sein.

1
peak

Das Folgende ist Hacker, um sicherzugehen - aber speichereffizient Hacker, selbst mit einer willkürlich langen Liste:

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

Der erste Teil der Pipeline-Streams in Ihrer JSON-Eingabedatei sendet eine Zeile pro Element aus, sofern das Array aus atomaren Werten besteht (wobei [] und {} hier als atomare Werte enthalten sind). Da es im Streaming-Modus ausgeführt wird, muss nicht der gesamte Inhalt im Arbeitsspeicher gespeichert werden, obwohl es sich um ein einziges Dokument handelt.

Der zweite Teil der Pipeline liest wiederholt bis zu drei Elemente und fügt sie zu einer Liste zusammen.

Dadurch sollten nicht mehr als drei Daten gleichzeitig im Speicher benötigt werden.

1
Charles Duffy