Python中JSON加载中的单引号与双引号的区别

13

我注意到单引号会导致simplejsonloads函数出错:

>>> import simplejson as json
>>> json.loads("\"foo\"")
'foo'
>>> json.loads("\'foo\'")
Traceback (most recent call last):
...
ValueError: No JSON object could be decoded
我正在将文本文件中的像这样的内容解析成Python列表:foo = ["a", "b", "c"],但我也想接受foo = ['a', 'b', 'c']。使用simplejson可以很方便地将foo自动转换为列表。
如何让loads接受单引号,或者自动将单引号替换为双引号而不破坏输入呢?谢谢。

6
根据 JSON 规范,如果使用单引号,则不属于 JSON 格式。 - larsks
2
据我所知,JSON字符串中的所有字符串都必须使用双引号。["a"]是有效的JSON,而['a']则不是。 - Sudipta Chatterjee
@user248237 str.replace() - 文档就在那里,兄弟。 - Gareth Latty
4
你可以修复产生错误输出的问题吗?你确定它应该是JSON格式吗? - Martijn Pieters
2
@user248237 ast.literal_eval()。如果您想解析Python字面量,请使用专门为此任务编写的解析器。 - Gareth Latty
显示剩余7条评论
3个回答

44

使用适当的工具完成工作,你不是在解析JSON而是在解析Python,因此请使用ast.literal_eval()

>>> import ast
>>> ast.literal_eval('["a", "b", "c"]')
['a', 'b', 'c']
>>> ast.literal_eval("['a', 'b', 'c']")
['a', 'b', 'c']
>>> ast.literal_eval('["mixed", \'quoting\', """styles"""]')
['mixed', 'quoting', 'styles']
  • JSON文档中的字符串总是使用双引号,对于\uhhhh 十六进制转义语法要使用UTF-16,使用{...} 对象表示键值对,其中键始终为字符串,序列始终为[...] 列表,并使用nulltruefalse 值;请注意小写布尔值。数字有整数和浮点数形式。

  • 在Python中,字符串表示可以使用单引号和双引号,Unicode 转义使用\uhhhh\Uhhhhhhhh 形式(没有 UTF-16代理对),用{...} 显示语法表示的字典可以具有许多不同类型的键,而不仅仅是字符串,序列可以是列表([...]),但也可以使用元组 ((...)),或者您可以使用其他容器类型。Python 有 NoneTrueFalse (标题化!) ,数字有整数、浮点数和复数形式。

将一个与另一个混淆可能会导致解析错误,或者当解码成功但数据被错误解释时会产生微妙的问题,例如转义的非BMP代码点如表情符号。确保使用正确的方法对其进行解码!在大多数情况下,当您拥有Python语法数据时,实际上某人错误地使用了编码方法,并且只是意外地生成了Python表示形式。在这种情况下,请查看源代码是否需要修正;通常输出是通过使用str(object)生成的,而应该改用json.dumps(obj)


在读了那种答案10遍之后,现在显而易见。实际上,你说得比其他人更简洁明了。谢谢! - jheld
ast.literal_eval返回的是单引号,我该如何将其转换为双引号呢?从字符串表示形式转换为双引号的JSON。 - Lidor Eliyahu Shelef
ast.literal_eval返回单引号,如何将其转换为双引号以便将字符串表示转换为双引号JSON? - Lidor Eliyahu Shelef
@LidorEliyahuShelef 不要使用它来生成JSON输出!请使用json模块来生成JSON。这个答案是关于将字符串解析为Python对象的。 - Martijn Pieters
我无法仅使用json模块从一个JSON字符串表示中处理多个嵌套的JSON。 - Lidor Eliyahu Shelef
@LidorEliyahuShelef 抱歉,没有细节我无法提供帮助,评论也不是合适的媒介。 - Martijn Pieters

0

我在寻找解决方法时,从StackOverflow的这个问题开始。

使用 ast.literal_eval() 的解决方案并不能适用于所有情况,因为文本中有时也会出现布尔常量 true/false ,它们不被识别为Python标记(Python标记大写)。

为了解决这个问题,我编写了一个 自定义JSONDecoder,它可以插入到标准的json Python包中。

pip install git+https://github.com/jpz/tolerantjsondecoder.git

也许对下一个人有用。

此外,需要注意的是,完成后,我后来发现 demjson 库似乎是一个更完整的解决方案,不过我还没有评估过它。


-1

Demjson可以完成你的工作,但是它非常慢,我是说与simplejson相比非常慢。 我不建议在生产环境中使用。

ast.literal_eval()和yaml也不能处理所有的JSON数据,因此您需要一个更稳定的解决方案,如simplejson。

如果您稍微调整一下simplejson,它就可以完成您的工作。 我自己已经做过了,并分享了这段代码。

我用两点来说明:

1)从github下载simplejson并将其添加到项目中。 2)现在simplejson有decoder.py python文件。 用这个代码替换那个文件的代码。

"""Implementation of JSONDecoder
"""
from __future__ import absolute_import
import re
import sys
import struct
from .compat import u, text_type, binary_type, PY3, unichr
from .scanner import make_scanner, JSONDecodeError

def _import_c_scanstring():
    try:
        from ._speedups import scanstring
        return scanstring
    except ImportError:
        return None
c_scanstring = _import_c_scanstring()

# NOTE (3.1.0): JSONDecodeError may still be imported from this module for
# compatibility, but it was never in the __all__
__all__ = ['JSONDecoder']

FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL

def _floatconstants():
    if sys.version_info < (2, 6):
        _BYTES = '7FF80000000000007FF0000000000000'.decode('hex')
        nan, inf = struct.unpack('>dd', _BYTES)
    else:
        nan = float('nan')
        inf = float('inf')
    return nan, inf, -inf

NaN, PosInf, NegInf = _floatconstants()

_CONSTANTS = {
    '-Infinity': NegInf,
    'Infinity': PosInf,
    'NaN': NaN,
}

STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)

# Changed Code Here. Added These Two Lines

STRINGCHUNKUNQUOTED = re.compile(r'(.*?)([:\\\x00-\x1f])', FLAGS)
STRINGCHUNKSINGLEQUOTED = re.compile(r"(.*?)(['\\\x00-\x1f])", FLAGS)

BACKSLASH = {
    '"': u('"'), '\\': u('\u005c'), '/': u('/'),
    'b': u('\b'), 'f': u('\f'), 'n': u('\n'), 'r': u('\r'), 't': u('\t'),
}
# Changed Code Here.
SINGLE_QUOTE_BACKSLASH = {
    "'": u("'"), '\\': u('\u005c'), '/': u('/'),
    'b': u('\b'), 'f': u('\f'), 'n': u('\n'), 'r': u('\r'), 't': u('\t'),
}

DEFAULT_ENCODING = "utf-8"

# Changed Code Here.
def parse_single_quoted_string(s, end, encoding=None, strict=True):
    return py_scanstring(s, end, encoding, strict, SINGLE_QUOTE_BACKSLASH, STRINGCHUNKSINGLEQUOTED.match)


def py_scanstring(s, end, encoding=None, strict=True,
        _b=BACKSLASH, _m=STRINGCHUNK.match, _join=u('').join,
        _PY3=PY3, _maxunicode=sys.maxunicode):
    """Scan the string s for a JSON string. End is the index of the
    character in s after the quote that started the JSON string.
    Unescapes all valid JSON string escape sequences and raises ValueError
    on attempt to decode an invalid string. If strict is False then literal
    control characters are allowed in the string.

    Returns a tuple of the decoded string and the index of the character in s
    after the end quote."""
    if encoding is None:
        encoding = DEFAULT_ENCODING
    chunks = []
    _append = chunks.append
    begin = end - 1
    while 1:
        chunk = _m(s, end)
        if chunk is None:
            raise JSONDecodeError(
                "Unterminated string starting at", s, begin)
        end = chunk.end()
        content, terminator = chunk.groups()
        # Content is contains zero or more unescaped string characters
        if content:
            if not _PY3 and not isinstance(content, text_type):
                content = text_type(content, encoding)
            _append(content)
        # Terminator is the end of string, a literal control character,
        # or a backslash denoting that an escape sequence follows

        # Changed Code Here.

        if not is_not_quote(terminator):
            break
        elif terminator != '\\':
            if strict:
                msg = "Invalid control character %r at"
                raise JSONDecodeError(msg, s, end)
            else:
                _append(terminator)
                continue
        try:
            esc = s[end]
        except IndexError:
            raise JSONDecodeError(
                "Unterminated string starting at", s, begin)
        # If not a unicode escape sequence, must be in the lookup table
        if esc != 'u':
            try:
                char = _b[esc]
            except KeyError:
                msg = "Invalid \\X escape sequence %r"
                raise JSONDecodeError(msg, s, end)
            end += 1
        else:
            # Unicode escape sequence
            msg = "Invalid \\uXXXX escape sequence"
            esc = s[end + 1:end + 5]
            escX = esc[1:2]
            if len(esc) != 4 or escX == 'x' or escX == 'X':
                raise JSONDecodeError(msg, s, end - 1)
            try:
                uni = int(esc, 16)
            except ValueError:
                raise JSONDecodeError(msg, s, end - 1)
            end += 5
            # Check for surrogate pair on UCS-4 systems
            # Note that this will join high/low surrogate pairs
            # but will also pass unpaired surrogates through
            if (_maxunicode > 65535 and
                uni & 0xfc00 == 0xd800 and
                s[end:end + 2] == '\\u'):
                esc2 = s[end + 2:end + 6]
                escX = esc2[1:2]
                if len(esc2) == 4 and not (escX == 'x' or escX == 'X'):
                    try:
                        uni2 = int(esc2, 16)
                    except ValueError:
                        raise JSONDecodeError(msg, s, end)
                    if uni2 & 0xfc00 == 0xdc00:
                        uni = 0x10000 + (((uni - 0xd800) << 10) |
                                         (uni2 - 0xdc00))
                        end += 6
            char = unichr(uni)
        # Append the unescaped character
        _append(char)
    return _join(chunks), end


# Use speedup if available
scanstring = c_scanstring or py_scanstring

WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
WHITESPACE_STR = ' \t\n\r'

# Changed Code Here.
UNQUOTEDDICT = {'/': '/', '\\': '\\', ';': ';', '#': '#',
            '=': '=', '{': '{', '}': '}', '[': '[', ']': ']',
            ':': ':', ',': ',', ' ': ' ', '\t': '\t',
            '\f': '\f', '\r': '\r', '\n': '\n'}

# Changed Code Here.
QUOTE_DICT = {'"': '"', "'": "'"}

# Changed Code Here.
def is_literal(char):
    return not UNQUOTEDDICT.get(char, None)

# Changed Code Here.
def is_not_quote(char):
    return not QUOTE_DICT.get(char, None)


# Changed Code Here.
def nexUnquotedKey(s, end):
    chunk = STRINGCHUNKUNQUOTED.match(s,end)
    for i in range(chunk.end()):
        index = i+end
        if not is_literal(s[index]):
            return s[end:index], index




def JSONObject(state, encoding, strict, scan_once, object_hook,
        object_pairs_hook, memo=None,
        _w=WHITESPACE.match, _ws=WHITESPACE_STR):
    (s, end) = state
    # Backwards compatibility
    if memo is None:
        memo = {}
    memo_get = memo.setdefault
    pairs = []
    # Use a slice to prevent IndexError from being raised, the following
    # check will raise a more specific ValueError if the string is empty
    nextchar = s[end:end + 1]
    # Normally we expect nextchar == '"'

    literal_check = False

    # Changed Code Here.
    if is_not_quote(nextchar):
        if nextchar in _ws:
            end = _w(s, end).end()
            nextchar = s[end:end + 1]
        # Trivial empty object
        literal_check = is_literal(nextchar)
        if nextchar == '}':
            if object_pairs_hook is not None:
                result = object_pairs_hook(pairs)
                return result, end + 1
            pairs = {}
            if object_hook is not None:
                pairs = object_hook(pairs)
            return pairs, end + 1
        elif nextchar != '"' and not literal_check:  # Changed Code Here.
            raise JSONDecodeError(
                "Expecting property name enclosed in double quotes",
                s, end)

    # Changed Code Here.
    if not literal_check:
        end += 1

    while True:
        if literal_check:
            key, end = nexUnquotedKey(s,end)
        else:
            # Changed Code Here.
            if nextchar == "'":
                key, end = scanstring(s, end, encoding, strict, SINGLE_QUOTE_BACKSLASH, STRINGCHUNKSINGLEQUOTED.match)
            else:
                key, end = scanstring(s, end, encoding, strict)

        key = memo_get(key, key)

        # To skip some function call overhead we optimize the fast paths where
        # the JSON key separator is ": " or just ":".
        if s[end:end + 1] != ':':
            end = _w(s, end).end()
            if s[end:end + 1] != ':':
                raise JSONDecodeError("Expecting ':' delimiter", s, end)

        end += 1

        try:
            if s[end] in _ws:
                end += 1
                if s[end] in _ws:
                    end = _w(s, end + 1).end()
        except IndexError:
            pass

        value, end = scan_once(s, end)
        pairs.append((key, value))

        try:
            nextchar = s[end]
            if nextchar in _ws:
                end = _w(s, end + 1).end()
                nextchar = s[end]
        except IndexError:
            nextchar = ''
        end += 1

        if nextchar == '}':
            break
        elif nextchar != ',':
            raise JSONDecodeError("Expecting ',' delimiter or '}'", s, end - 1)

        try:
            nextchar = s[end]
            if nextchar in _ws:
                end += 1
                nextchar = s[end]
                if nextchar in _ws:
                    end = _w(s, end + 1).end()
                    nextchar = s[end]
        except IndexError:
            nextchar = ''

        # Changed Code Here.
        if not literal_check:
            end += 1
            # Changed Code Here.
            if is_not_quote(nextchar):
                raise JSONDecodeError(
                    "Expecting property name enclosed in double quotes",
                    s, end - 1)

    if object_pairs_hook is not None:
        result = object_pairs_hook(pairs)
        return result, end
    pairs = dict(pairs)
    if object_hook is not None:
        pairs = object_hook(pairs)
    return pairs, end

def JSONArray(state, scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
    (s, end) = state
    values = []
    nextchar = s[end:end + 1]
    if nextchar in _ws:
        end = _w(s, end + 1).end()
        nextchar = s[end:end + 1]
    # Look-ahead for trivial empty array
    if nextchar == ']':
        return values, end + 1
    elif nextchar == '':
        raise JSONDecodeError("Expecting value or ']'", s, end)
    _append = values.append
    while True:
        value, end = scan_once(s, end)
        _append(value)
        nextchar = s[end:end + 1]
        if nextchar in _ws:
            end = _w(s, end + 1).end()
            nextchar = s[end:end + 1]
        end += 1
        if nextchar == ']':
            break
        elif nextchar != ',':
            raise JSONDecodeError("Expecting ',' delimiter or ']'", s, end - 1)

        try:
            if s[end] in _ws:
                end += 1
                if s[end] in _ws:
                    end = _w(s, end + 1).end()
        except IndexError:
            pass

    return values, end

class JSONDecoder(object):
    """Simple JSON <http://json.org> decoder

    Performs the following translations in decoding by default:

    +---------------+-------------------+
    | JSON          | Python            |
    +===============+===================+
    | object        | dict              |
    +---------------+-------------------+
    | array         | list              |
    +---------------+-------------------+
    | string        | str, unicode      |
    +---------------+-------------------+
    | number (int)  | int, long         |
    +---------------+-------------------+
    | number (real) | float             |
    +---------------+-------------------+
    | true          | True              |
    +---------------+-------------------+
    | false         | False             |
    +---------------+-------------------+
    | null          | None              |
    +---------------+-------------------+

    It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as
    their corresponding ``float`` values, which is outside the JSON spec.

    """

    def __init__(self, encoding=None, object_hook=None, parse_float=None,
            parse_int=None, parse_constant=None, strict=True,
            object_pairs_hook=None):
        """
        *encoding* determines the encoding used to interpret any
        :class:`str` objects decoded by this instance (``'utf-8'`` by
        default).  It has no effect when decoding :class:`unicode` objects.

        Note that currently only encodings that are a superset of ASCII work,
        strings of other encodings should be passed in as :class:`unicode`.

        *object_hook*, if specified, will be called with the result of every
        JSON object decoded and its return value will be used in place of the
        given :class:`dict`.  This can be used to provide custom
        deserializations (e.g. to support JSON-RPC class hinting).

        *object_pairs_hook* is an optional function that will be called with
        the result of any object literal decode with an ordered list of pairs.
        The return value of *object_pairs_hook* will be used instead of the
        :class:`dict`.  This feature can be used to implement custom decoders
        that rely on the order that the key and value pairs are decoded (for
        example, :func:`collections.OrderedDict` will remember the order of
        insertion). If *object_hook* is also defined, the *object_pairs_hook*
        takes priority.

        *parse_float*, if specified, will be called with the string of every
        JSON float to be decoded.  By default, this is equivalent to
        ``float(num_str)``. This can be used to use another datatype or parser
        for JSON floats (e.g. :class:`decimal.Decimal`).

        *parse_int*, if specified, will be called with the string of every
        JSON int to be decoded.  By default, this is equivalent to
        ``int(num_str)``.  This can be used to use another datatype or parser
        for JSON integers (e.g. :class:`float`).

        *parse_constant*, if specified, will be called with one of the
        following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``.  This
        can be used to raise an exception if invalid JSON numbers are
        encountered.

        *strict* controls the parser's behavior when it encounters an
        invalid control character in a string. The default setting of
        ``True`` means that unescaped control characters are parse errors, if
        ``False`` then control characters will be allowed in strings.

        """
        if encoding is None:
            encoding = DEFAULT_ENCODING
        self.encoding = encoding
        self.object_hook = object_hook
        self.object_pairs_hook = object_pairs_hook
        self.parse_float = parse_float or float
        self.parse_int = parse_int or int
        self.parse_constant = parse_constant or _CONSTANTS.__getitem__
        self.strict = strict
        self.parse_object = JSONObject
        self.parse_array = JSONArray
        self.parse_string = scanstring
        self.parse_single_quoted_string = parse_single_quoted_string  # Changed Code Here.
        self.memo = {}
        self.scan_once = make_scanner(self)

    def decode(self, s, _w=WHITESPACE.match, _PY3=PY3):
        """Return the Python representation of ``s`` (a ``str`` or ``unicode``
        instance containing a JSON document)

        """
        if _PY3 and isinstance(s, binary_type):
            s = s.decode(self.encoding)
        obj, end = self.raw_decode(s)
        end = _w(s, end).end()
        if end != len(s):
            raise JSONDecodeError("Extra data", s, end, len(s))
        return obj

    def raw_decode(self, s, idx=0, _w=WHITESPACE.match, _PY3=PY3):
        """Decode a JSON document from ``s`` (a ``str`` or ``unicode``
        beginning with a JSON document) and return a 2-tuple of the Python
        representation and the index in ``s`` where the document ended.
        Optionally, ``idx`` can be used to specify an offset in ``s`` where
        the JSON document begins.

        This can be used to decode a JSON document from a string that may
        have extraneous data at the end.

        """
        if idx < 0:
            # Ensure that raw_decode bails on negative indexes, the regex
            # would otherwise mask this behavior. #98
            raise JSONDecodeError('Expecting value', s, idx)
        if _PY3 and not isinstance(s, text_type):
            raise TypeError("Input string must be text, not bytes")
        # strip UTF-8 bom
        if len(s) > idx:
            ord0 = ord(s[idx])
            if ord0 == 0xfeff:
                idx += 1
            elif ord0 == 0xef and s[idx:idx + 3] == '\xef\xbb\xbf':
                idx += 3
        return self.scan_once(s, idx=_w(s, idx).end())

2) simplejson有scanner.py的Python文件。请用此代码替换该文件的代码

 """JSON token scanner
"""
import re
from .errors import JSONDecodeError
def _import_c_make_scanner():
    try:
        from ._speedups import make_scanner
        return make_scanner
    except ImportError:
        return None
c_make_scanner = _import_c_make_scanner()

__all__ = ['make_scanner', 'JSONDecodeError']

NUMBER_RE = re.compile(
    r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
    (re.VERBOSE | re.MULTILINE | re.DOTALL))


def py_make_scanner(context):
    parse_object = context.parse_object
    parse_array = context.parse_array
    parse_string = context.parse_string
    parse_single_quoted_string = context.parse_single_quoted_string  # Changed Code Here.
    match_number = NUMBER_RE.match
    encoding = context.encoding
    strict = context.strict
    parse_float = context.parse_float
    parse_int = context.parse_int
    parse_constant = context.parse_constant
    object_hook = context.object_hook
    object_pairs_hook = context.object_pairs_hook
    memo = context.memo

    def _scan_once(string, idx):
        errmsg = 'Expecting value'
        try:
            nextchar = string[idx]
        except IndexError:
            raise JSONDecodeError(errmsg, string, idx)

        if nextchar == '"':
            return parse_string(string, idx + 1, encoding, strict)
        elif nextchar == "'":
            return parse_single_quoted_string(string, idx + 1, encoding, strict)  # Changed Code Here.
        elif nextchar == '{':
            return parse_object((string, idx + 1), encoding, strict,
                _scan_once, object_hook, object_pairs_hook, memo)
        elif nextchar == '[':
            return parse_array((string, idx + 1), _scan_once)
        elif nextchar == 'n' and string[idx:idx + 4] == 'null':
            return None, idx + 4
        elif nextchar == 't' and string[idx:idx + 4] == 'true':
            return True, idx + 4
        elif nextchar == 'f' and string[idx:idx + 5] == 'false':
            return False, idx + 5

        m = match_number(string, idx)
        if m is not None:
            integer, frac, exp = m.groups()
            if frac or exp:
                res = parse_float(integer + (frac or '') + (exp or ''))
            else:
                res = parse_int(integer)
            return res, m.end()
        elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
            return parse_constant('NaN'), idx + 3
        elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
            return parse_constant('Infinity'), idx + 8
        elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
            return parse_constant('-Infinity'), idx + 9
        else:
            raise JSONDecodeError(errmsg, string, idx)

    def scan_once(string, idx):
        if idx < 0:
            # Ensure the same behavior as the C speedup, otherwise
            # this would work for *some* negative string indices due
            # to the behavior of __getitem__ for strings. #98
            raise JSONDecodeError('Expecting value', string, idx)
        try:
            return _scan_once(string, idx)
        finally:
            memo.clear()

    return scan_once

make_scanner = c_make_scanner or py_make_scanner

现在你已经准备好了。 Simplejson 是我使用过的最快和稳定的库。

我已经修改了 simplejson 的几行代码,现在这个神奇的库可以支持:

  • 未引用的 JSON 键和单引号的 JSON 字符串和键

我只更改了 Python 代码。因此,如果您使用 C 扩展来加速提升,则此代码将无法工作。

在我进行更改的任何地方,我都添加了注释 # Changed Code Here。

我之前错误地以 guest 用户回答了,现在我无法登录以编辑该答案,因此我发布了新帖子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接