如何使用带引号的逗号分隔逗号分隔的键值对

时间:2014-12-20 00:13:47

标签: python parsing

我知道有很多其他关于解析逗号分隔值的帖子,但我找不到分割键值对并处理带引号的逗号。

我有这样的字符串:

age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"

我希望得到这个:

{
  'age': '12',
  'name': 'bob',
  'hobbies': 'games,reading',
  'phrase': "I'm cool!",
}

我尝试使用shlex这样:

lexer = shlex.shlex('''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"''')
lexer.whitespace_split = True
lexer.whitespace = ','
props = dict(pair.split('=', 1) for pair in lexer)

问题在于shlex会将hobbies条目拆分为两个令牌,即hobbies="gamesreading"。有没有办法让双引号考虑在内?或者我可以使用另一个模块吗?

编辑:修正了whitespace_split

的拼写错误

编辑2:我不喜欢使用shlex。正则表达式也很好,但我不知道如何处理匹配的引号。

5 个答案:

答案 0 :(得分:7)

您只需在POSIX模式下使用shlex词法分析器。

创建词法分析器时添加posix=True

(见the shlex parsing rules

lexer = shlex.shlex('''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"''', posix=True)
lexer.whitespace_split = True
lexer.whitespace = ','
props = dict(pair.split('=', 1) for pair in lexer)

输出:

{'age': '12', 'phrase': "I'm cool!", 'hobbies': 'games,reading', 'name': 'bob'}

PS:只要输入可以包含带引号的=,字符,正则表达式就无法解析键值对。即使预处理字符串也不能使输入被正则表达式解析,因为这种输入不能正式定义为常规语言。

答案 1 :(得分:4)

可以使用正则表达式。在这种情况下,它实际上也可能是最好的选择。我认为这将适用于大多数输入,甚至是转义引号,例如:phrase='I\'m cool'

使用VERBOSE标志,可以使复杂的正则表达式具有可读性。

import re
text = '''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"'''
regex = re.compile(
    r'''
        (?P<key>\w+)=      # Key consists of only alphanumerics
        (?P<quote>["']?)   # Optional quote character.
        (?P<value>.*?)     # Value is a non greedy match
        (?P=quote)         # Closing quote equals the first.
        ($|,)              # Entry ends with comma or end of string
    ''',
    re.VERBOSE
    )

d = {match.group('key'): match.group('value') for match in regex.finditer(text)}

print(d)  # {'name': 'bob', 'phrase': "I'm cool!", 'age': '12', 'hobbies': 'games,reading'}

答案 2 :(得分:2)

您可以滥用Python tokenizer来解析键值列表:

#!/usr/bin/env python
from tokenize import generate_tokens, NAME, NUMBER, OP, STRING, ENDMARKER

def parse_key_value_list(text):
    key = value = None
    for type, string, _,_,_ in generate_tokens(lambda it=iter([text]): next(it)):
        if type == NAME and key is None:
            key = string
        elif type in {NAME, NUMBER, STRING}:
            value = {
                NAME: lambda x: x,
                NUMBER: int,
                STRING: lambda x: x[1:-1]
            }[type](string)
        elif ((type == OP and string == ',') or
              (type == ENDMARKER and key is not None)):
            yield key, value
            key = value = None

text = '''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"'''
print(dict(parse_key_value_list(text)))

输出

{'phrase': "I'm cool!", 'age': 12, 'name': 'bob', 'hobbies': 'games,reading'}

您可以使用有限状态机(FSM)来实现更严格的解析器。解析器仅使用当前状态和下一个标记来解析输入:

#!/usr/bin/env python
from tokenize import generate_tokens, NAME, NUMBER, OP, STRING, ENDMARKER

def parse_key_value_list(text):
    def check(condition):
        if not condition:
            raise ValueError((state, token))

    KEY, EQ, VALUE, SEP = range(4)
    state = KEY
    for token in generate_tokens(lambda it=iter([text]): next(it)):
        type, string = token[:2]
        if state == KEY:
            check(type == NAME)
            key = string
            state = EQ
        elif state == EQ:
            check(type == OP and string == '=')
            state = VALUE
        elif state == VALUE:
            check(type in {NAME, NUMBER, STRING})
            value = {
                NAME: lambda x: x,
                NUMBER: int,
                STRING: lambda x: x[1:-1]
            }[type](string)
            state = SEP
        elif state == SEP:
            check(type == OP and string == ',' or type == ENDMARKER)
            yield key, value
            state = KEY

text = '''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"'''
print(dict(parse_key_value_list(text)))

答案 3 :(得分:1)

好吧,我实际上想出了一个非常漂亮的方法,即用逗号和等号分开,然后一次取2个令牌。

input_str = '''age=12,name=bob,hobbies="games,reading",phrase="I'm cool!"'''

lexer = shlex.shlex(input_str)
lexer.whitespace_split = True
lexer.whitespace = ',='

ret = {}
try:
  while True:
    key = next(lexer)
    value = next(lexer)

    # Remove surrounding quotes
    if len(value) >= 2 and (value[0] == value[-1] == '"' or
                            value[0] == value[-1] == '\''):
      value = value[1:-1]

    ret[key] = value

except StopIteration:
  # Somehow do error checking to see if you ended up with an extra token.
  pass

print ret

然后你得到:

{
  'age': '12',
  'name': 'bob',
  'hobbies': 'games,reading',
  'phrase': "I'm cool!",
}

但是,这并不能检查您是否有类似age,12=name,bob之类的奇怪内容,但在我的用例中我可以使用它。

编辑:处理双引号和单引号。

答案 4 :(得分:0)

Python似乎提供了许多方法来解决任务。这是一个更像c实现的方式,处理每个char。知道不同的运行时间会很有趣。

str = 'age=12,name=bob,hobbies="games,reading",phrase="I\'m cool!"'
key = ""
val = ""
dict = {}
parse_string = False
parse_key = True
# parse_val = False
for c in str:
    print(c)
    if c == '"' and not parse_string:
        parse_string = True
        continue
    elif c == '"' and parse_string:
        parse_string = False
        continue
    if parse_string:
        val += c
        continue
    if c == ',': # terminate entry
        dict[key] = val #add to dict
        key = ""
        val = ""
        parse_key = True
        continue
    elif c == '=' and parse_key:
        parse_key = False
    elif parse_key:
        key += c
    else:
        val+=c
dict[key] = val
print(dict.items())
# {'phrase': "I'm cool!", 'age': '12', 'name': 'bob', 'hobbies': 'games,reading'}

演示:http://repl.it/6oC/1