109 votes

Comment puis-je lire paresseusement plusieurs valeurs JSON à partir d'un fichier/stream en Python ?

J'aimerais lire plusieurs objets JSON d'un fichier/flux en Python, un par un. Malheureusement, json.load() juste .read() s jusqu'à la fin du fichier ; il ne semble pas y avoir de moyen de l'utiliser pour lire un seul objet ou pour itérer paresseusement sur les objets.

Y a-t-il un moyen de le faire ? L'idéal serait d'utiliser la bibliothèque standard, mais s'il existe une bibliothèque tierce, je l'utiliserais à la place.

Pour l'instant, je place chaque objet sur une ligne séparée et j'utilise la méthode suivante json.loads(f.readline()) mais je préférerais vraiment ne pas avoir à le faire.

Exemple d'utilisation

exemple.py

import my_json as json
import sys

for o in json.iterload(sys.stdin):
    print("Working on a", type(o))

dans.txt

{"foo": ["bar", "baz"]} 1 2 [] 4 5 6

exemple de séance

$ python3.2 example.py < in.txt
Working on a dict
Working on a int
Working on a int
Working on a list
Working on a int
Working on a int
Working on a int

0 votes

Pourriez-vous ajouter un exemple du comportement que vous souhaitez obtenir des objets imbriqués ?

0 votes

@TimMcNamara : Le comportement des objets imbriqués ne devrait pas changer. Cependant, une fois que nous avons atteint la fin du premier objet de niveau supérieur ( {"foo": ["bar", "baz"]} dans mon exemple), il devrait yield puis passer à la suivante ( 1 ).

0 votes

Personnellement, je ne vois pas trop de problème avec la ligne de lecture. Vous pouvez aussi utiliser un séparateur d'enregistrement différent tant que vous êtes sûr qu'il n'est pas contenu dans un json valide.

42voto

Thomas K Points 16753

JSON n'est généralement pas très bien adapté à ce type d'utilisation incrémentielle ; il n'existe pas de moyen standard de sérialiser plusieurs objets de manière à pouvoir les charger facilement un par un, sans avoir à analyser l'ensemble.

La solution de l'objet par ligne que vous utilisez est vue ailleurs aussi. Scrapy l'appelle "lignes JSON" :

Vous pouvez le faire de manière un peu plus pythonienne :

for jsonline in f:
    yield json.loads(jsonline)   # or do the processing in this loop

Je pense que c'est la meilleure façon de procéder - elle ne dépend d'aucune bibliothèque tierce et il est facile de comprendre ce qui se passe. Je l'ai également utilisé dans certains de mes propres codes.

4 votes

Re : "pas de méthode standard" : Je ne vois pas le problème, la syntaxe semble rendre les objets multiples consécutifs sans ambiguïté tant qu'on a un tampon d'un caractère. Merci de m'avoir signalé que d'autres personnes utilisent les "lignes JSON", je me sens moins mal à l'aise de l'utiliser pour l'instant.

27voto

Jeremy Roman Points 9211

Bien sûr, vous pouvez le faire. Vous devez juste prendre raw_decode directement. Cette implémentation charge le fichier entier en mémoire et opère sur cette chaîne (un peu comme json.load ) ; si vous avez de gros fichiers, vous pouvez le modifier pour qu'il ne lise que ce qui est nécessaire dans le fichier sans trop de difficultés.

import json
from json.decoder import WHITESPACE

def iterload(string_or_fp, cls=json.JSONDecoder, **kwargs):
    if isinstance(string_or_fp, file):
        string = string_or_fp.read()
    else:
        string = str(string_or_fp)

    decoder = cls(**kwargs)
    idx = WHITESPACE.match(string, 0).end()
    while idx < len(string):
        obj, end = decoder.raw_decode(string, idx)
        yield obj
        idx = WHITESPACE.match(string, end).end()

Utilisation : comme vous l'avez demandé, c'est un générateur.

2 votes

Il semble que la partie délicate serait de s'assurer que les lectures en continu apportent suffisamment du fichier pour que vous ayez un objet entier à décoder. Il s'agit donc d'une approche simple qui fonctionne si vous supposez, par exemple, que les objets ne contiennent jamais de nouvelles lignes. Mais à moins que vous n'imposiez ce genre de structure supplémentaire au fichier, ce que le PO essaie d'éviter, il semble que vous ayez besoin d'une solution comme celle de @Benedict.

25voto

Benedict Points 1228

C'est un problème assez désagréable en fait parce qu'il faut faire du streaming en lignes, mais aussi de la correspondance de motifs sur plusieurs lignes contre des accolades, mais aussi de la correspondance de motifs en json. C'est une sorte de json-preparse suivi d'un json parse. Json est, par rapport à d'autres formats, facile à analyser, il n'est donc pas toujours nécessaire de recourir à une bibliothèque d'analyse syntaxique, mais comment résoudre ces problèmes contradictoires ?

Les générateurs à la rescousse !

La beauté des générateurs pour un problème de ce type est que vous pouvez les empiler les uns sur les autres en éliminant progressivement la difficulté du problème tout en maintenant la paresse. J'ai également envisagé d'utiliser le mécanisme permettant de renvoyer des valeurs dans un générateur (send()), mais heureusement, je n'ai pas eu besoin de l'utiliser.

Pour résoudre le premier de ces problèmes, vous avez besoin d'une sorte de streamingfinditer, comme une version streaming de re.finditer. La tentative que j'ai faite ci-dessous ajoute des lignes selon les besoins (décommentez l'instruction de débogage pour voir) tout en retournant les correspondances. Je l'ai ensuite légèrement modifié pour retourner les lignes non correspondantes ainsi que les correspondances (marquées comme 0 ou 1 dans la première partie du tuple retourné).

import re

def streamingfinditer(pat,stream):
  for s in stream:
#    print "Read next line: " + s
    while 1:
      m = re.search(pat,s)
      if not m:
        yield (0,s)
        break
      yield (1,m.group())
      s = re.split(pat,s,1)[1]

Avec cela, il est alors possible de faire correspondre jusqu'à des accolades, de tenir compte à chaque fois du fait que les accolades sont équilibrées, puis de renvoyer des objets simples ou composés selon le cas.

braces='{}[]'
whitespaceesc=' \t'
bracesesc='\\'+'\\'.join(braces)
balancemap=dict(zip(braces,[1,-1,1,-1]))
bracespat='['+bracesesc+']'
nobracespat='[^'+bracesesc+']*'
untilbracespat=nobracespat+bracespat

def simpleorcompoundobjects(stream):
  obj = ""
  unbalanced = 0
  for (c,m) in streamingfinditer(re.compile(untilbracespat),stream):
    if (c == 0): # remainder of line returned, nothing interesting
      if (unbalanced == 0):
        yield (0,m)
      else:
        obj += m
    if (c == 1): # match returned
      if (unbalanced == 0):
        yield (0,m[:-1])
        obj += m[-1]
      else:
        obj += m
      unbalanced += balancemap[m[-1]]
      if (unbalanced == 0):
        yield (1,obj)
        obj="" 

Cela renvoie des tuples comme suit :

(0,"String of simple non-braced objects easy to parse")
(1,"{ 'Compound' : 'objects' }")

En gros, c'est la partie la plus désagréable qui est faite. Il ne nous reste plus qu'à faire le dernier niveau de parsing comme bon nous semble. Par exemple, nous pouvons utiliser la fonction iterload de Jeremy Roman (Merci !) pour faire le parsing d'une seule ligne :

def streamingiterload(stream):
  for c,o in simpleorcompoundobjects(stream):
    for x in iterload(o):
      yield x 

Testez-le :

of = open("test.json","w") 
of.write("""[ "hello" ] { "goodbye" : 1 } 1 2 {
} 2
9 78
 4 5 { "animals" : [ "dog" , "lots of mice" ,
 "cat" ] }
""")
of.close()
// open & stream the json
f = open("test.json","r")
for o in streamingiterload(f.readlines()):
  print o
f.close()

J'obtiens les résultats suivants (et si vous activez la ligne de débogage, vous verrez qu'elle ajoute les lignes nécessaires) :

[u'hello']
{u'goodbye': 1}
1
2
{}
2
9
78
4
5
{u'animals': [u'dog', u'lots of mice', u'cat']}

Cela ne fonctionnera pas dans toutes les situations. En raison de l'implémentation de la méthode json bibliothèque, il est impossible pour fonctionner tout à fait correctement sans réimplémenter l'analyseur syntaxique vous-même.

8 votes

Si vous voulez faire cela correctement, vous devez également faire attention aux accolades et aux parenthèses dans les chaînes de caractères. Et il faut aussi faire attention aux guillemets échappés. Avant que vous ne le sachiez, le "préparateur" sera presque aussi compliqué qu'un analyseur JSON complet.

0 votes

Merci Jeremy. C'était un beau défi que de poser une question ! Oui Petr - vous avez tout à fait raison bien sûr :)

1 votes

Bien fait. Est-ce que cela se comportera correctement si des caractères comme "}" y "]" se produisent dans les chaînes JSON ? Je pense que c'est une limitation générale de l'analyse syntaxique avec regex.

4voto

wuliang Points 319

J'aimerais proposer une solution. La pensée clé est d'"essayer" de décoder : si cela échoue, donnez-lui plus d'alimentation, sinon utilisez les informations de décalage pour préparer le prochain décodage.

Cependant, le module json actuel ne tolère pas les espaces en tête de chaîne pour être décodé, et je dois donc les supprimer.

import sys
import json

def iterload(file):
    buffer = ""
    dec = json.JSONDecoder()
    for line in file:         
        buffer = buffer.strip(" \n\r\t") + line.strip(" \n\r\t")
        while(True):
            try:
                r = dec.raw_decode(buffer)
            except:
                break
            yield r[0]
            buffer = buffer[r[1]:].strip(" \n\r\t")

for o in iterload(sys.stdin):
    print("Working on a", type(o),  o)

\========================= J'ai testé pour plusieurs fichiers txt, et cela fonctionne bien. (in1.txt)

{"foo": ["bar", "baz"]
}
 1 2 [
  ]  4
{"foo1": ["bar1", {"foo2":{"A":1, "B":3}, "DDD":4}]
}
 5   6

(in2.txt)

{"foo"
: ["bar",
  "baz"]
  } 
1 2 [
] 4 5 6

(in.txt, votre initiale)

{"foo": ["bar", "baz"]} 1 2 [] 4 5 6

(sortie pour le testcase de Benedict)

python test.py < in.txt
('Working on a', <type 'list'>, [u'hello'])
('Working on a', <type 'dict'>, {u'goodbye': 1})
('Working on a', <type 'int'>, 1)
('Working on a', <type 'int'>, 2)
('Working on a', <type 'dict'>, {})
('Working on a', <type 'int'>, 2)
('Working on a', <type 'int'>, 9)
('Working on a', <type 'int'>, 78)
('Working on a', <type 'int'>, 4)
('Working on a', <type 'int'>, 5)
('Working on a', <type 'dict'>, {u'animals': [u'dog', u'lots of mice', u'cat']})

1voto

Jeremy Banks Points 32470

Vous ne pouvez pas faire cela avec la bibliothèque standard. J'ai parcouru les sources de la json et il est impossible de l'utiliser paresseusement sans en réimplémenter la majeure partie.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X