【问题标题】:Handling lazy JSON in Python - 'Expecting property name'在 Python 中处理惰性 JSON - '期望属性名称'
【发布时间】:2011-05-01 07:45:36
【问题描述】:

使用 Pythons (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}'

【问题讨论】:

    标签: python json


    【解决方案1】:

    除了 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)
    

    【讨论】:

      【解决方案2】:

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

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

      import ast
      decoded_object= ast.literal_eval(json_encoded_text)
      

      【讨论】:

        【解决方案3】:

        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)
        

        我在修复惰性 json 时看到的唯一问题是,如果 json 格式错误,则第二个 json.loads 引发的错误不会引用原始字符串中的行和列,而是引用修改后的字符串。

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

        BONUS:除此之外,人们通常喜欢在使用 json 时包含 C/C++ cmets 配置文件,在这种情况下,您可以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)
        

        【讨论】:

        • 确实,谢谢,虽然为了让它工作,我还必须添加 import StringIO 并使用 StringIO 将行更改为:StringIO.StringIO(in_text) from StringIO(in_text) 然后它在一个懒惰的 json 上工作谷歌财务用于延迟期权链报价。
        • 谢谢!我忘记将“从 StringIO 导入 StringIO”添加到我粘贴在这里的代码中。现在更新了:)
        • 老兄,这绝对是救命稻草。感谢您发布此内容。
        【解决方案4】:

        扩展 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)
        

        【讨论】:

        • 最后一行第一行 (\w) 必须是 (\w*),因为您要尝试匹配整个单词。
        • 谢谢克里斯,我更新到 \w+ 因为 0 字符匹配没有意义
        • 而且,对于我们这些不小心创建带有尾随逗号的“Pythonic”JSON 的人:j = re.sub(r",\s*]", "]", j) ... 我没有编辑答案,因为可能存在我没有想到的缺点。
        【解决方案5】:

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

        【讨论】:

        • 是的,demjson确实支持很好的解析非严格的json。
        • 我测试了demjson 的每个循环 5.05 毫秒和json 的每个循环 15.2 毫秒,json 更快!
        【解决方案6】:

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

        【讨论】:

        • 感谢您的意见 - 我非常怀疑提供商会回复,但我会尝试与他们联系。我也尝试了 REGEX。我已经编辑了我的问题以反映我使用 REGEX 的发现。
        • 我将把这个打开一段时间,看看是否有其他人有任何进一步的建议 - 否则我会接受你的回答。查看您添加的 REGEX 语句,它们的作用与我的几乎相同。
        • 请注意,虽然此正则表达式可能适用于某些非常特定的场景,但它不会适用于更复杂的东西,例如:{ location: 'http://www.google.com' },您最终会得到无效的 JSON: {"location": "http"://www.google.com"}
        猜你喜欢
        • 2012-02-29
        • 2017-01-22
        • 1970-01-01
        • 1970-01-01
        • 2019-08-06
        • 2021-10-15
        • 1970-01-01
        • 2014-10-31
        • 2021-11-13
        相关资源
        最近更新 更多