在Python中处理懒惰的JSON – “期待属性名称”

使用pythons(2.7)'json'模块我正在寻找处理各种JSON提要。 不幸的是,这些提要中的一些不符合JSON标准 – 特定的某些键不包含在双重语音标记(“)中,这导致Python出错。

在编写一段丑陋的代码来parsing和修复传入的数据之前,我想我会问 – 是否有任何方法可以让Pythonparsing这个畸形的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}' 

您正试图使用​​JSONparsing器来parsing不是JSON的东西。 最好的办法是让饲料的创build者来修复它们。

我明白这并不总是可能的。 您可能可以使用正则expression式修复数据,具体取决于它是如何被破坏的:

 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) 

另一个select是使用可以非严格模式parsingjson的demjson模块。

Ned和cheeseinvert指出的正则expression式并没有考虑什么时候比赛是在一个string内。

看下面的例子(使用cheeseinvert的解决scheme):

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

问题是预期的输出是:

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

由于JSON令牌是Python令牌的一个子集,我们可以使用python的令牌化模块 。

请纠正我,如果我错了,但下面的代码将修复在所有情况下一个懒惰的JSONstring:

 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) 

所以为了parsing一个jsonstring,你可能想要封装一个调用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引发的错误将不会引用原始string中的行和列,而是修改后的行。

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

BONUS:除此之外,人们通常喜欢在使用json作为configuration文件时包含C / C ++注释,在这种情况下,您可以使用正则expression式来删除注释 ,或者使用扩展版本并在一次传递中修复jsonstring:

 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) 

内德的build议扩大,以下对我有帮助:

 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) 

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

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

 import ast decoded_object= ast.literal_eval(json_encoded_text) 

除了Neds和cheeseinvert的build议,添加(?!/)应避免与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)