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

47 投票
6 回答
69872 浏览
提问于 2025-04-16 06:10

我正在使用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)

我写了一个小的正则表达式来修复来自这个特定提供者的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 个回答

11

Ned和cheeseinvert提到的正则表达式没有考虑到匹配内容在字符串内部的情况。

看看下面这个例子(使用cheeseinvert的解决方案):

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

问题是,期望的输出是:

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

由于JSON的标记是Python标记的一个子集,我们可以使用Python的tokenize模块

如果我说错了请纠正我,但以下代码可以在所有情况下修复懒惰的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)

我在修复懒惰的JSON时看到的唯一问题是,如果JSON格式不正确,第二次调用json.loads时抛出的错误不会指向原始字符串的行和列,而是指向修改后的字符串。

最后我想指出的是,更新任何方法以接受文件对象而不是字符串是很简单的。

额外提示:除此之外,人们通常喜欢在使用JSON作为配置文件时包含C/C++的注释,在这种情况下,你可以使用正则表达式去除注释,或者使用扩展版本一次性修复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)
17

另一个选择是使用 demjson 这个模块,它可以在不严格的模式下解析json数据。

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)

撰写回答