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.
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 devraityield
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.
0 votes
Liés : Comment analyser le premier objet JSON d'un flux en JS ?
1 votes
Pourquoi éviter les "lignes json" ? Il est toujours possible de sérialiser un objet en json de manière à ce qu'il n'ait aucune
'\n'
(un seul saut de ligne, pas deux caractères) dans sa représentation json car'\n'
doit être échappé à l'intérieur d'une chaîne json et donc'\n'
peut être utilisé pour le formatage uniquement, par exemple, je crois quejson.dumps()
n'introduit pas'\n'
par défaut. Attention, les nouvelles lignes Unicode telles que U+0085 peuvent être non encodées dans les chaînes json.0 votes
@J.F.Sebastian Les lignes JSON semblent très judicieuses. Je prévois de l'utiliser à l'avenir.
2 votes
El ijson pourrait être utile dans ce cas. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
0 votes
Si vous ne voulez pas utiliser de lignes json, vous pouvez utiliser un message préfixé par la longueur.
0 votes
@BorisChervenkov - ijson ne le fait pas. Il s'attend à ce que tout soit inclus dans un énorme objet ou une liste.
0 votes
@JeremyBanks, a posté une solution basée sur une machine d'état qui devrait être rapide je crois. Jetez-y un œil et faites-moi part de vos commentaires
1 votes
Le titre ne devrait-il pas être "Comment puis-je lire paresseusement plusieurs fichiers JSON ? valeurs à partir d'un fichier/flux en Python" ? Puisqu'un objet est aussi une valeur, tout comme un json, un int, une chaîne, etc., alors que l'inverse n'est pas nécessairement vrai ?
0 votes
@hetepeperfan Bon point. Changé.