47 votes

Manipulation de JSON paresseux en Python - 'Expecting property name' (attente du nom de la propriété)

En utilisant le module 'json' de Python (2.7), je cherche à traiter différents flux JSON. Malheureusement, certains de ces flux ne sont pas conformes aux standards JSON - en particulier, certaines clés ne sont pas entourées de doubles guillemets ("). Cela provoque un bug de Python.

Avant d'écrire un horrible code pour analyser et réparer les données entrantes, j'ai pensé poser la question suivante : existe-t-il un moyen de permettre à Python d'analyser ce JSON malformé ou de "réparer" les données pour qu'elles soient valides ?

Exemple de travail

import json
>>> json.loads('{"key1":1,"key2":2,"key3":3}')
{'key3': 3, 'key2': 2, 'key1': 1}

Exemple brisé

import json
>>> json.loads('{key1:1,key2:2,key3:3}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python27\lib\json\__init__.py", line 310, in loads
    return _default_decoder.decode(s)
  File "C:\Python27\lib\json\decoder.py", line 346, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "C:\Python27\lib\json\decoder.py", line 362, in raw_decode
    obj, end = self.scan_once(s, idx)
ValueError: Expecting property name: line 1 column 1 (char 1)

J'ai écrit un petit REGEX pour corriger le JSON provenant de ce fournisseur particulier, mais je prévois que cela sera un problème à l'avenir. Voici ce que j'ai trouvé.

>>> import re
>>> s = '{key1:1,key2:2,key3:3}'
>>> s = re.sub('([{,])([^{:\s"]*):', lambda m: '%s"%s":'%(m.group(1),m.group(2)),s)
>>> s
'{"key1":1,"key2":2,"key3":3}'

33voto

Ned Batchelder Points 128913

Vous essayez d'utiliser un analyseur JSON pour analyser quelque chose qui n'est pas JSON. Votre meilleure chance est de demander au créateur des flux de les corriger.

Je comprends que ce n'est pas toujours possible. Vous pourriez être en mesure de réparer les données en utilisant des regex, en fonction de leur degré de dégradation :

j = re.sub(r"{\s*(\w)", r'{"\1', j)
j = re.sub(r",\s*(\w)", r',"\1', j)
j = re.sub(r"(\w):", r'\1":', j)

17voto

Joel Points 501

Une autre option consiste à utiliser le demjson qui peut analyser les données json en mode non strict.

11voto

psanchez Points 21

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)

6voto

cheeseinvert Points 73

Dans le prolongement de la suggestion de Ned, ce qui suit m'a été utile :

j = re.sub(r"{\s*'?(\w)", r'{"\1', j)
j = re.sub(r",\s*'?(\w)", r',"\1', j)
j = re.sub(r"(\w)'?\s*:", r'\1":', j)
j = re.sub(r":\s*'(\w+)'\s*([,}])", r':"\1"\2', j)

1voto

tzot Points 32224

Dans un cas similaire, j'ai utilisé ast.literal_eval . AFAIK, cela ne fonctionnera que lorsque la constante null (correspondant à Python None ) apparaît dans le JSON.

Étant donné que vous connaissez le null/None situation difficile, vous le pouvez :

import ast
decoded_object= ast.literal_eval(json_encoded_text)

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