it-swarm.com.de

Umgang mit Lazy JSON in Python - 'Name der Eigenschaft erwartet'

Mit Pythons (2.7) 'Json'-Modul werde ich verschiedene JSON-Feeds verarbeiten. Leider entsprechen einige dieser Feeds nicht den JSON-Standards. Insbesondere werden einige Schlüssel nicht in doppelte Sprachmarken (") eingeschlossen. Dies führt dazu, dass Python ausfällt.

Bevor ich ein hässliches Stück Code schrieb, um die eingehenden Daten zu analysieren und zu reparieren, dachte ich, würde ich fragen - gibt es eine Möglichkeit, Python zu erlauben, diese missgebildete JSON zu parsen oder die Daten zu "reparieren", so dass dies der Fall ist gültiger JSON?

Arbeitsbeispiel

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

Gebrochenes Beispiel

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)

Ich habe ein kleines REGEX geschrieben, um die JSON zu korrigieren, die von diesem bestimmten Anbieter kommt, aber ich sehe, dass dies in Zukunft ein Problem ist. Unten ist was ich mir ausgedacht habe.

>>> 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

Sie versuchen, einen JSON-Parser zu verwenden, um etwas zu analysieren, das nicht JSON ist. Am besten holen Sie sich den Ersteller der Feeds, um sie zu reparieren.

Ich verstehe, dass das nicht immer möglich ist. Möglicherweise können Sie die Daten mit Regex fixieren, je nachdem, wie beschädigt sie sind:

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

Eine andere Option ist die Verwendung des Moduls demjson , das Json im nicht strengen Modus parsen kann.

17
Joel

Die regulären Ausdrücke, auf die Ned und Cheeseinvert hinweisen, werden nicht berücksichtigt, wenn sich die Übereinstimmung in einer Zeichenfolge befindet.

Siehe folgendes Beispiel (Verwendung von Cheeseinverts Lösung):

>>> fixLazyJsonWithRegex ('{ key : "a { a : b }", }')
'{ "key" : "a { "a": b }" }'

Das Problem ist, dass die erwartete Ausgabe ist:

'{ "key" : "a { a : b }" }'

Da JSON-Token eine Teilmenge von Python-Token sind, können wir das tokenize-Modul von python verwenden.

Bitte korrigieren Sie mich, wenn ich falsch liege, aber der folgende Code korrigiert in allen Fällen eine Lazy-Json-Zeichenfolge:

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)

Um einen Json-String zu parsen, sollten Sie einen Aufruf von fixLazyJson einkapseln, wenn json.loads fehlschlägt (um Performance-Strafen für wohlgeformten Json zu vermeiden):

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)

Das einzige Problem, das ich beim Fixieren von Lazy Json sehe, ist, dass, wenn der Json fehlerhaft ist, der von den zweiten json.loads erzeugte Fehler nicht auf die Zeile und die Spalte der ursprünglichen Zeichenfolge verweist, sondern auf die geänderte.

Als letzte Anmerkung möchte ich nur darauf hinweisen, dass es unkompliziert wäre, alle Methoden zu aktualisieren, um ein Dateiobjekt anstelle eines Strings zu akzeptieren.

BONUS: Abgesehen davon möchten die Leute normalerweise C/C++ - Kommentare hinzufügen, wenn json für Konfigurationsdateien verwendet wird. In diesem Fall können Sie entweder Kommentare mit einem regulären Ausdruck entfernen oder die erweiterte Version und verwenden Fixiere den Json-String in einem Durchgang:

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

Auf Neds Vorschlag eingegangen, hat mir folgendes geholfen:

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

In einem ähnlichen Fall habe ich ast.literal_eval verwendet. AFAIK, das funktioniert nicht nur, wenn die Konstante null (entsprechend Python None) in der JSON erscheint.

Wenn Sie über die null/None-Situation Bescheid wissen, können Sie:

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

Neben dem Vorschlag von Neds und Cheeseinvert sollte durch Hinzufügen von (?!/) das erwähnte Problem mit URLs vermieden werden 

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