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.
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]
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
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.)
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
.
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.
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.