Les expressions régulières signalées par Ned et cheeseinvert ne tiennent pas compte du fait que la correspondance se trouve à l'intérieur d'une chaîne.
Voir l'exemple suivant (utilisant la solution de cheeseinvert) :
>>> fixLazyJsonWithRegex ('{ key : "a { a : b }", }')
'{ "key" : "a { "a": b }" }'
Le problème est que le résultat attendu est :
'{ "key" : "a { a : b }" }'
Les jetons JSON étant un sous-ensemble des jetons python, nous pouvons utiliser la fonction module tokenize .
Corrigez-moi si je me trompe, mais le code suivant corrigera une chaîne json paresseuse dans tous les cas :
import tokenize
import token
from StringIO import StringIO
def fixLazyJson (in_text):
tokengen = tokenize.generate_tokens(StringIO(in_text).readline)
result = []
for tokid, tokval, _, _, _ in tokengen:
# fix unquoted strings
if (tokid == token.NAME):
if tokval not in ['true', 'false', 'null', '-Infinity', 'Infinity', 'NaN']:
tokid = token.STRING
tokval = u'"%s"' % tokval
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
# remove invalid commas
elif (tokid == token.OP) and ((tokval == '}') or (tokval == ']')):
if (len(result) > 0) and (result[-1][1] == ','):
result.pop()
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
result.append((tokid, tokval))
return tokenize.untokenize(result)
Ainsi, pour analyser une chaîne json, vous pourriez vouloir encapsuler un appel à fixLazyJson une fois que json.loads a échoué (pour éviter les pénalités de performance pour un json bien formé) :
import json
def json_decode (json_string, *args, **kwargs):
try:
json.loads (json_string, *args, **kwargs)
except:
json_string = fixLazyJson (json_string)
json.loads (json_string, *args, **kwargs)
Le seul problème que je vois lors de la correction de json paresseux, est que si le json est malformé, l'erreur soulevée par le second json.loads ne fera pas référence à la ligne et à la colonne de la chaîne originale, mais à celle modifiée.
Pour terminer, je tiens à souligner qu'il serait facile de mettre à jour n'importe laquelle de ces méthodes pour qu'elle accepte un objet fichier au lieu d'une chaîne de caractères.
BONUS : En dehors de cela, les gens aiment généralement inclure des commentaires C/C++ lorsque json est utilisé pour les fichiers de configuration. fichiers de configuration, dans ce cas, vous pouvez soit supprimer les commentaires en utilisant une expression régulière ou utilisez la version étendue et corrigez la chaîne json en un seul passage :
import tokenize
import token
from StringIO import StringIO
def fixLazyJsonWithComments (in_text):
""" Same as fixLazyJson but removing comments as well
"""
result = []
tokengen = tokenize.generate_tokens(StringIO(in_text).readline)
sline_comment = False
mline_comment = False
last_token = ''
for tokid, tokval, _, _, _ in tokengen:
# ignore single line and multi line comments
if sline_comment:
if (tokid == token.NEWLINE) or (tokid == tokenize.NL):
sline_comment = False
continue
# ignore multi line comments
if mline_comment:
if (last_token == '*') and (tokval == '/'):
mline_comment = False
last_token = tokval
continue
# fix unquoted strings
if (tokid == token.NAME):
if tokval not in ['true', 'false', 'null', '-Infinity', 'Infinity', 'NaN']:
tokid = token.STRING
tokval = u'"%s"' % tokval
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
# remove invalid commas
elif (tokid == token.OP) and ((tokval == '}') or (tokval == ']')):
if (len(result) > 0) and (result[-1][1] == ','):
result.pop()
# detect single-line comments
elif tokval == "//":
sline_comment = True
continue
# detect multiline comments
elif (last_token == '/') and (tokval == '*'):
result.pop() # remove previous token
mline_comment = True
continue
result.append((tokid, tokval))
last_token = tokval
return tokenize.untokenize(result)