web-dev-qa-db-fra.com

Comment diviser un tableau en morceaux avec jq?

J'ai un très gros fichier JSON contenant un tableau. Est-il possible d'utiliser jq pour diviser ce tableau en plusieurs tableaux plus petits d'une taille fixe? Supposons que ma saisie soit la suivante: [1,2,3,4,5,6,7,8,9,10] et que je veuille la scinder en 3 gros morceaux. La sortie souhaitée de jq serait:

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

En réalité, mon tableau d’entrée contient près de trois millions d’éléments, tous UUID.

9
Echo Nolan

La définition suivante de window/3 orientée sur les flux, due à Cédric Connes .__ (github: connesc), généralise _nwise, Et illustre une "technique de boxe" of-stream marker, et peut donc être utilisé si le flux contient la valeur non-JSON nan. Une définition De _nwise/1 en termes de window/3 est également incluse.

Le premier argument de window/3 est interprété comme un flux. $ size est la taille de la fenêtre et $ step spécifie le nombre de valeurs à ignorer. Par exemple,

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

rendements:

[1,2]
[2,3]

fenêtre/3 et _size/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);

La source:

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

2
peak

Il existe une structure intégrée (non documentée), _nwise, qui répond aux exigences fonctionnelles:

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

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

Également:

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

Par ailleurs, _nwise peut être utilisé à la fois pour les tableaux et les chaînes.

(Je pense que c'est sans papiers parce qu'il y avait un doute sur un nom approprié.)

TCO-version

Malheureusement, la version intégrée est définie avec négligence et ne fonctionnera pas bien pour les tableaux de grande taille. Voici une version optimisée (elle devrait être à peu près aussi efficace qu'une version non récursive):

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

Pour un tableau de taille 3 millions, cela est assez performant: 3,91s sur un ancien Mac, 162746368 taille maximale des résidents.

Notez que cette version (utilisant la récursivité optimisée pour les appels en aval) est en réalité plus rapide que la version de nwise/2 utilisant foreach indiquée ailleurs sur cette page.

7
peak

Si le tableau est trop volumineux pour tenir confortablement en mémoire, j'adopterais la stratégie suggérée par @CharlesDuffy - c'est-à-dire, diffusez les éléments du tableau dans un deuxième appel de jq à l'aide d'une version de nwise orientée sur le flux, telle que:

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

Le "pilote" pour ce qui précède serait:

nwise(inputs; 3)

Mais n'oubliez pas d'utiliser l'option de ligne de commande -n.

Pour créer le flux à partir d'un tableau arbitraire:

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

Ainsi, le pipeline Shell pourrait ressembler à ceci:

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

Cette approche est assez performante. Pour regrouper un flux de 3 millions d'éléments en groupes de 3 à l'aide de nwise/2,

/usr/bin/time -lp

pour la seconde invocation de jq donne:

user         5.63
sys          0.04
   1261568  maximum resident set size

Avertissement: cette définition utilise nan comme marqueur de fin de flux. nan n'étant pas une valeur JSON, cela ne peut pas poser de problème pour la gestion des flux JSON.

1
peak

Ce qui suit est du piratage, bien sûr, mais du efficace pour la mémoire, même avec une liste arbitrairement longue:

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

La première partie des flux de pipeline dans votre fichier JSON d'entrée émet une ligne par élément, en supposant que le tableau se compose de valeurs atomiques (où [] et {} sont inclus ici en tant que valeurs atomiques). Comme il fonctionne en mode de diffusion en continu, il n’est pas nécessaire de stocker tout le contenu en mémoire, bien qu’il s’agisse d’un document unique.

La deuxième partie du pipeline lit à plusieurs reprises jusqu'à trois éléments et les assemble dans une liste.

Cela devrait éviter d'avoir besoin de plus de trois données en mémoire à la fois.

1
Charles Duffy