web-dev-qa-db-fra.com

Gestion des fichiers JSON paresseux en Python - 'Nom de propriété en attente'

Utilisation du module 'json' de Pythons (2.7) Je cherche à traiter divers flux JSON. Malheureusement, certains de ces flux ne sont pas conformes aux normes JSON. En particulier, certaines clés ne sont pas entourées de doubles marques de langage ("). Cela provoque la correction de Python.

Avant d'écrire un morceau de code hideux pour analyser et réparer les données entrantes, je me demandais s'il existait un moyen d'autoriser Python à analyser ce JSON mal formé ou à "réparer" les données de sorte qu'elles soient JSON valide?

Exemple de travail

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

Exemple cassé

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 réparer le JSON provenant de ce fournisseur, mais je prévois que cela posera un problème à l'avenir. Ci-dessous est ce que je suis venu avec.

>>> 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}'
46
Seidr

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

Je comprends que ce n'est pas toujours possible. Vous pourrez peut-être réparer les données en utilisant des expressions rationnelles, selon leur degré de rupture:

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)
33
Ned Batchelder

Une autre option consiste à utiliser le module demjson qui permet d’analyser json en mode non strict.

17
Joel

Les expressions régulières soulignées par Ned et cheeseinvert ne tiennent pas compte de la correspondance dans une chaîne.

Voir l'exemple suivant (en 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 de jetons python, nous pouvons utiliser le module tokenize de python .

S'il vous plaît, corrigez-moi si je me trompe, mais le code suivant résoudra 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)

Donc, afin d'analyser une chaîne JSON, vous pouvez 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 la JSON paresseux est que si le JSON est mal formé, l'erreur générée par le second json.loads ne fera pas référence à la ligne et à la colonne de la chaîne d'origine, mais à celle modifiée.

Pour terminer, je tiens à souligner qu’il serait simple de mettre à jour l’une quelconque des méthodes pour accepter un objet fichier au lieu d’une chaîne.

BONUS: En dehors de cela, les gens préfèrent généralement inclure des commentaires C/C++ lorsque json est utilisé pour les fichiers de configuration Dans ce cas, vous pouvez supprimer des commentaires à l'aide d'une expression régulière , ou utiliser la version étendue 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)
11
psanchez

En développant 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)
6
cheeseinvert

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

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

import ast
decoded_object= ast.literal_eval(json_encoded_text)
1
tzot

En plus de la suggestion de Neds et cheeseinvert, ajouter (?!/) devrait éviter le problème mentionné avec les urls 

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) 
j = re.sub(r",\s*]", "]", j)
0
Stan