在Python中处理懒惰的JSON - '期望属性名'

时间:2010-10-27 13:16:21

标签: python json

使用Pythons(2.7)'json'模块我正在寻找处理各种JSON提要。不幸的是,其中一些feed不符合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 个答案:

答案 0 :(得分:33)

您正在尝试使用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)

答案 1 :(得分:17)

另一种选择是使用demjson模块,它可以在非严格模式下解析json。

答案 2 :(得分:11)

当匹配在字符串中时,Ned和cheeseinvert指出的正则表达式不会考虑。

请参阅以下示例(使用cheeseinvert的解决方案):

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

问题是预期的输出是:

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

由于JSON令牌是python令牌的子集,我们可以使用python的tokenize module

如果我错了,请纠正我,但以下代码将修复所有情况下的懒惰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字符串,您可能希望在json.loads失败时封装对fixLazyJson的调用(以避免对格式良好的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引发的错误将不会引用原始字符串中的行和列,而是修改后的行。 / p>

作为最后一点,我只想指出,更新任何接受文件对象而不是字符串的方法都是直截了当的。

BONUS:除此之外,人们通常喜欢在使用json时包含C / C ++注释 配置文件,在这种情况下,您可以remove comments using a regular expression,或使用扩展版本并在一次传递中修复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)

答案 3 :(得分:6)

根据Ned的建议,以下内容对我有所帮助:

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)

答案 4 :(得分:1)

在类似的情况下,我使用了ast.literal_eval。 AFAIK,只有当常量null(对应于Python None)出现在JSON中时才会起作用。

鉴于您了解null/None困境,您可以:

import ast
decoded_object= ast.literal_eval(json_encoded_text)

答案 5 :(得分:0)

除了Neds和cheeseinvert建议之外,添加(?!/)应该避免提到的网址问题

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)