Обработка отложенного JSON в Python - «Ожидается имя свойства» - PullRequest
46 голосов
/ 27 октября 2010

Использование Python-модуля (2.7) 'json' Я хочу обработать различные потоки JSON.К сожалению, некоторые из этих каналов не соответствуют стандартам JSON - в частности, некоторые ключи не заключены в двойные речевые метки ("). Это вызывает ошибки в Python.

Перед тем, как написать некрасивый адЧасть кода для синтаксического анализа и исправления входящих данных, я подумал, я бы спросил - есть ли способ позволить Python анализировать этот искаженный JSON или «восстанавливать» данные так, чтобы он был действительным JSON?

Рабочий пример

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

Неправильный пример

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)

Я написал небольшой REGEX, чтобы исправить JSON, исходящий от этого конкретного провайдера, но я предполагаю, что это будет проблемой в будущемВот то, что я придумал.

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

Ответы [ 6 ]

33 голосов
/ 27 октября 2010

Вы пытаетесь использовать анализатор JSON для анализа чего-то, что не является JSON.Лучше всего, чтобы создатель каналов исправил их.

Я понимаю, что это не всегда возможно.Вы можете исправить данные с помощью регулярных выражений, в зависимости от того, насколько они повреждены:

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)
17 голосов
/ 15 мая 2012

Другой вариант - использовать модуль demjson , который может анализировать json в нестрогом режиме.

11 голосов
/ 08 февраля 2014

Регулярные выражения, указанные Недом и cheeseinvert, не учитывают, когда совпадение находится внутри строки.

См. Следующий пример (с использованием решения cheeseinvert):

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

Проблема в том, что ожидаемый результат:

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

Поскольку токены JSON являются подмножеством токенов Python, мы можем использовать модуль токена Python .

Пожалуйста, исправьте меня, если я ошибаюсь, но следующий код исправит ленивую строку json во всех случаях:

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)

Таким образом, для анализа строки json может потребоваться инкапсулировать вызов fixLazyJson после сбоя json.loads (во избежание снижения производительности для правильно сформированного json):

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)

Единственная проблема, которую я вижу при исправлении lazy json, заключается в том, что если json искажен, ошибка, вызванная вторым json.loads, будет ссылаться не на строку и столбец из исходной строки, а на измененную.

В качестве заключительного замечания я просто хочу отметить, что было бы просто обновить любой из методов, чтобы принимать файловый объект вместо строки.

БОНУС: Помимо этого, людям обычно нравится включать комментарии C / C ++, когда json используется для файлы конфигурации, в этом случае вы можете либо удалить комментарии, используя регулярное выражение , либо использовать расширенную версию и исправить строку json за один проход:

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)
6 голосов
/ 08 февраля 2012

Расширяя предложение Неда, мне помогло следующее:

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)
1 голос
/ 29 июня 2016

В аналогичном случае я использовал ast.literal_eval. AFAIK, это не будет работать только тогда, когда в JSON появится постоянная null (соответствующая Python None).

Учитывая, что вы знаете о затруднительном положении null/None, вы можете:

import ast
decoded_object= ast.literal_eval(json_encoded_text)
0 голосов
/ 25 ноября 2016

В дополнение к предложению Neds и cheeseinvert, добавление (?!/) должно избежать упомянутой проблемы с URL

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...