【问题标题】:Pyparsing: Parsing semi-JSON nested plaintext data to a listPyparsing:将半 JSON 嵌套的明文数据解析为列表
【发布时间】:2013-12-19 20:32:39
【问题描述】:

我有一堆嵌套的数据,格式大致类似于 JSON:

company="My Company"
phone="555-5555"
people=
{
    person=
    {
        name="Bob"
        location="Seattle"
        settings=
        {
            size=1
            color="red"
        }
    }
    person=
    {
        name="Joe"
        location="Seattle"
        settings=
        {
            size=2
            color="blue"
        }
    }
}
places=
{
    ...
}

有许多具有不同深度级别的不同参数——这只是一个非常小的子集。

还可能值得注意的是,当创建一个新的子数组时,总是有一个等号,后跟一个换行符,然后是左括号(如上所示)。

是否有任何简单的循环或递归技术可以将此数据转换为系统友好的数据格式,例如数组或 JSON?我想避免对属性名称进行硬编码。我正在寻找可以在 Python、Java 或 PHP 中使用的东西。伪代码也可以。

感谢您的帮助。

编辑:我发现了 Python 的 Pyparsing 库,它看起来很有帮助。我找不到任何关于如何使用 Pyparsing 解析未知深度的嵌套结构的示例。任何人都可以根据我上面描述的数据阐明 Pyparsing 吗?

编辑 2:好的,这是 Pyparsing 中的一个有效解决方案:

def parse_file(fileName):

#get the input text file
file = open(fileName, "r")
inputText = file.read()

#define the elements of our data pattern
name = Word(alphas, alphanums+"_")
EQ,LBRACE,RBRACE = map(Suppress, "={}")
value = Forward() #this tells pyparsing that values can be recursive
entry = Group(name + EQ + value) #this is the basic name-value pair


#define data types that might be in the values
real = Regex(r"[+-]?\d+\.\d*").setParseAction(lambda x: float(x[0]))
integer = Regex(r"[+-]?\d+").setParseAction(lambda x: int(x[0]))
quotedString.setParseAction(removeQuotes)

#declare the overall structure of a nested data element
struct = Dict(LBRACE + ZeroOrMore(entry) + RBRACE) #we will turn the output into a Dictionary

#declare the types that might be contained in our data value - string, real, int, or the struct we declared
value << (quotedString | struct | real | integer)

#parse our input text and return it as a Dictionary
result = Dict(OneOrMore(entry)).parseString(inputText)
return result.dump()

这可行,但是当我尝试使用 json.dump(result) 将结果写入文件时,文件的内容用双引号引起来。此外,许多数据对之间还有\n 字符。我尝试在上面的代码中使用LineEnd().suppress() 抑制它们,但我一定没有正确使用它。



好的,我想出了一个最终解决方案,该解决方案实际上将这些数据转换为我最初想要的 JSON 友好的 Dict。它首先使用 Pyparsing 将数据转换为一系列嵌套列表,然后循环遍历列表并将其转换为 JSON。这让我可以克服 Pyparsing 的 toDict() 方法无法处理同一对象具有两个同名属性的问题。为了确定列表是普通列表还是属性/值对,prependPropertyToken 方法会在 Pyparsing 检测到属性名称前添加字符串 __property__

def parse_file(self,fileName):
            
            #get the input text file
            file = open(fileName, "r")
            inputText = file.read()


            #define data types that might be in the values
            real = Regex(r"[+-]?\d+\.\d*").setParseAction(lambda x: float(x[0]))
            integer = Regex(r"[+-]?\d+").setParseAction(lambda x: int(x[0]))
            yes = CaselessKeyword("yes").setParseAction(replaceWith(True))
            no = CaselessKeyword("no").setParseAction(replaceWith(False))
            quotedString.setParseAction(removeQuotes)
            unquotedString =  Word(alphanums+"_-?\"")
            comment = Suppress("#") + Suppress(restOfLine)
            EQ,LBRACE,RBRACE = map(Suppress, "={}")
            
            data = (real | integer | yes | no | quotedString | unquotedString)
            
            #define structures
            value = Forward()
            object = Forward() 
            
            dataList = Group(OneOrMore(data))
            simpleArray = (LBRACE + dataList + RBRACE)
            
            propertyName = Word(alphanums+"_-.").setParseAction(self.prependPropertyToken)
            property = dictOf(propertyName + EQ, value)
            properties = Dict(property)
            
            object << (LBRACE + properties + RBRACE)
            value << (data | object | simpleArray)
            
            dataset = properties.ignore(comment)
            
            #parse it
            result = dataset.parseString(inputText)
            
            #turn it into a JSON-like object
            dict = self.convert_to_dict(result.asList())
            return json.dumps(dict)
            
    
    
    def convert_to_dict(self, inputList):
            dict = {}
            for item in inputList:
                    #determine the key and value to be inserted into the dict
                    dictval = None
                    key = None
                    
                    if isinstance(item, list):
                            try:
                                    key = item[0].replace("__property__","")
                                    if isinstance(item[1], list):
                                            try:
                                                    if item[1][0].startswith("__property__"):
                                                            dictval = self.convert_to_dict(item)
                                                    else:
                                                            dictval = item[1]
                                            except AttributeError:
                                                    dictval = item[1]
                                    else:
                                            dictval = item[1]
                            except IndexError:
                                    dictval = None
                    #determine whether to insert the value into the key or to merge the value with existing values at this key
                    if key:
                            if key in dict:
                                    if isinstance(dict[key], list):
                                            dict[key].append(dictval)
                                    else:
                                            old = dict[key]
                                            new = [old]
                                            new.append(dictval)
                                            dict[key] = new
                            else:
                                    dict[key] = dictval
            return dict

    
                    
    def prependPropertyToken(self,t):
            return "__property__" + t[0]

【问题讨论】:

  • pyparsing wiki 的示例页面包含许多递归结构的示例 - 查找标有“螺旋”图标的示例。
  • 谢谢,我没有注意到大部分例子,因为我错误地认为只有开发中和用户贡献的例子。
  • 为什么要在解析表达式中添加Optional(NL)? pyparsing 的主要功能之一是它在 pyparsing 期间自动跳过空格,其中包括换行符。这就是为什么你看不到+ Optional(White()) 在整个解析器中乱扔垃圾的原因,不像你必须通过正则表达式来处理\s* 来处理可能出现空白的地方。 result 不是字典,即使您可以像访问它一样访问它 - 它是 ParseResults 对象,所以 json.dump(result) 可能不会做您想做的事。但是就像有一个 asXML 方法一样,您可以尝试编写 asJSON。
  • 嗯,好吧,我将摆脱 Optional(NL) 的东西,用 AsJSON 试试你所说的。

标签: python json parsing pyparsing plaintext


【解决方案1】:

可以使用 pyparsing 来解析任意嵌套的结构,方法是使用 Forward 类定义一个占位符来保存嵌套部分。在这种情况下,您只是解析简单的名称-值对,其中 value 本身可能是包含名称-值对的嵌套结构。

name :: word of alphanumeric characters
entry :: name '=' value
struct :: '{' entry* '}'
value :: real | integer | quotedstring | struct

这意味着几乎逐字逐句地进行 pyparsing。要定义可以递归包含值的值,我们首先创建一个 Forward() 占位符,它可以用作条目定义的一部分。然后,一旦我们定义了所有可能的值类型,我们就使用 '

EQ,LBRACE,RBRACE = map(Suppress,"={}")

name = Word(alphas, alphanums+"_")
value = Forward()
entry = Group(name + EQ + value)

real = Regex(r"[+-]?\d+\.\d*").setParseAction(lambda x: float(x[0]))
integer = Regex(r"[+-]?\d+").setParseAction(lambda x: int(x[0]))
quotedString.setParseAction(removeQuotes)

struct = Group(LBRACE + ZeroOrMore(entry) + RBRACE)
value << (quotedString | struct | real | integer)

real 和 integer 的解析操作会在解析时将这些元素从字符串转换为浮点数或整数,以便在解析后立即将值用作它们的实际类型(无需后处理进行 string-to -其他类型转换)。

您的样本是一个或多个条目的集合,因此我们使用它来解析总输入:

result = OneOrMore(entry).parseString(sample)

我们可以以嵌套列表的形式访问解析后的数据,但显示起来不太美观。此代码使用 pprint 漂亮地打印格式化的嵌套列表:

from pprint import pprint
pprint(result.asList())

给予:

[['company', 'My Company'],
 ['phone', '555-5555'],
 ['people',
  [['person',
    [['name', 'Bob'],
     ['location', 'Seattle'],
     ['settings', [['size', 1], ['color', 'red']]]]],
   ['person',
    [['name', 'Joe'],
     ['location', 'Seattle'],
     ['settings', [['size', 2], ['color', 'blue']]]]]]]]

请注意,所有字符串都是不带引号的字符串,整数是实际整数。

我们可以做得比这更好一点,通过认识到条目格式实际上定义了一个适合像 Python dict 一样访问的名称-值对。我们的解析器只需做一些小的改动就可以做到这一点:

将结构定义更改为:

struct = Dict(LBRACE + ZeroOrMore(entry) + RBRACE)

和整个解析器:

result = Dict(OneOrMore(entry)).parseString(sample)

Dict 类将解析后的内容视为一个名称后跟一个值,这可以递归完成。通过这些更改,我们现在可以像 dict 中的元素一样访问结果中的数据:

print result['phone']

或类似对象中的属性:

print result.company

使用 dump() 方法查看结构或子结构的内容:

for person in result.people:
    print person.dump()
    print

打印:

['person', ['name', 'Bob'], ['location', 'Seattle'], ['settings', ['size', 1], ['color', 'red']]]
- location: Seattle
- name: Bob
- settings: [['size', 1], ['color', 'red']]
  - color: red
  - size: 1

['person', ['name', 'Joe'], ['location', 'Seattle'], ['settings', ['size', 2], ['color', 'blue']]]
- location: Seattle
- name: Joe
- settings: [['size', 2], ['color', 'blue']]
  - color: blue
  - size: 2

【讨论】:

  • 绝对完美。非常感谢!此外,在 Pyparsing 方面的出色工作!我想我现在会一直使用它。
  • 好的,我用关于\n 字符和额外引号的简短附录更新了我的问题。你能提供任何方向吗?
【解决方案2】:

没有“简单”的方法,但有更难和不那么难的方法。如果您不想对事物进行硬编码,那么在某些时候您将不得不将其解析为结构化格式。这将涉及逐一解析每一行,对其进行适当的标记(例如,正确地将键与值分开),然后确定您希望如何处理该行。

您可能需要以中间格式存储数据,例如(解析)树,以说明任意嵌套关系(由缩进和大括号表示),然后在完成数据解析后,获取您的生成的树,然后再次遍历它以获取您的数组或 JSON。

有一些可用的库,例如 ANTLR,可以处理一些确定如何编写解析器的手动工作。

【讨论】:

    【解决方案3】:

    看看这段代码:

    still_not_valid_json = re.sub (r'(\w+)=', r'"\1":', pseudo_json ) #1
    this_one_is_tricky = re.compile ('("|\d)\n(?!\s+})', re.M)
    that_one_is_tricky_too = re.compile ('(})\n(?=\s+\")', re.M)
    nearly_valid_json = this_one_is_tricky.sub (r'\1,\n', still_not_valid_json) #2
    nearly_valid_json = that_one_is_tricky_too.sub (r'\1,\n', nearly_valid_json) #3
    valid_json = '{' +  nearly_valid_json + '}' #4
    

    您可以通过一些替换将您的 pseudo_json 转换为可解析的 json。

    1. 将“=”替换为“:”
    2. 在简单值(如“2”或“Joe”)和下一个字段之间添加缺少的逗号
    3. 在复杂值的右大括号和下一个字段之间添加缺少的逗号
    4. 用大括号拥抱它

    还是有问题。在您的示例中,“人”字典包含两个相似的键“人”。解析后,字典中只剩下一个键。这是我解析后得到的:{u'phone': u'555-5555', u'company': u'My Company', u'people': {u'person': {u'settings': {u'color': u'blue', u'size': 2}, u'name': u'Joe', u'location': u'Seattle'}}}

    如果您可以将第二次出现的 'person=' 替换为 'person1=' 等等...

    【讨论】:

    • 感谢您的正则表达式建议。是否可以通过将people 的内容包装在一个列表中来解决多个person 问题?但话又说回来,除了为people 属性硬编码该行为之外,我想不出任何方法来做到这一点。
    【解决方案4】:

    将'='替换为':',然后直接读取为json,添加尾随逗号

    【讨论】:

    • 如果只有这些就好了。他如何处理出现在属性中的等号?添加尾随逗号的逻辑是什么?解析“常规”格式是您必须尝试几次才能意识到这绝非易事的事情之一。如果解析这些东西很容易,那么它已经是一种可接受的格式,并且 OP 已经完成了。
    • 是的,这有点太简单了。我可以使用正则表达式模式来确定逗号的位置吗?因为他们不能在每一行的末尾——然后我会有这样的东西=,和这个{,
    【解决方案5】:

    好的,我想出了一个最终解决方案,该解决方案实际上将这些数据转换为我最初想要的 JSON 友好的 Dict。它首先使用 Pyparsing 将数据转换为一系列嵌套列表,然后循环遍历列表并将其转换为 JSON。这使我能够克服 Pyparsing 的 toDict() 方法无法处理同一对象具有两个同名属性的问题。为了确定列表是普通列表还是属性/值对,当 Pyparsing 检测到属性名称时,prependPropertyToken 方法会在属性名称前添加字符串 __property__

    def parse_file(self,fileName):
    
                #get the input text file
                file = open(fileName, "r")
                inputText = file.read()
    
    
                #define data types that might be in the values
                real = Regex(r"[+-]?\d+\.\d*").setParseAction(lambda x: float(x[0]))
                integer = Regex(r"[+-]?\d+").setParseAction(lambda x: int(x[0]))
                yes = CaselessKeyword("yes").setParseAction(replaceWith(True))
                no = CaselessKeyword("no").setParseAction(replaceWith(False))
                quotedString.setParseAction(removeQuotes)
                unquotedString =  Word(alphanums+"_-?\"")
                comment = Suppress("#") + Suppress(restOfLine)
                EQ,LBRACE,RBRACE = map(Suppress, "={}")
    
                data = (real | integer | yes | no | quotedString | unquotedString)
    
                #define structures
                value = Forward()
                object = Forward() 
    
                dataList = Group(OneOrMore(data))
                simpleArray = (LBRACE + dataList + RBRACE)
    
                propertyName = Word(alphanums+"_-.").setParseAction(self.prependPropertyToken)
                property = dictOf(propertyName + EQ, value)
                properties = Dict(property)
    
                object << (LBRACE + properties + RBRACE)
                value << (data | object | simpleArray)
    
                dataset = properties.ignore(comment)
    
                #parse it
                result = dataset.parseString(inputText)
    
                #turn it into a JSON-like object
                dict = self.convert_to_dict(result.asList())
                return json.dumps(dict)
    
    
    
        def convert_to_dict(self, inputList):
                dict = {}
                for item in inputList:
                        #determine the key and value to be inserted into the dict
                        dictval = None
                        key = None
    
                        if isinstance(item, list):
                                try:
                                        key = item[0].replace("__property__","")
                                        if isinstance(item[1], list):
                                                try:
                                                        if item[1][0].startswith("__property__"):
                                                                dictval = self.convert_to_dict(item)
                                                        else:
                                                                dictval = item[1]
                                                except AttributeError:
                                                        dictval = item[1]
                                        else:
                                                dictval = item[1]
                                except IndexError:
                                        dictval = None
                        #determine whether to insert the value into the key or to merge the value with existing values at this key
                        if key:
                                if key in dict:
                                        if isinstance(dict[key], list):
                                                dict[key].append(dictval)
                                        else:
                                                old = dict[key]
                                                new = [old]
                                                new.append(dictval)
                                                dict[key] = new
                                else:
                                        dict[key] = dictval
                return dict
    
    
    
        def prependPropertyToken(self,t):
                return "__property__" + t[0]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-04-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-24
      • 1970-01-01
      • 2013-10-19
      相关资源
      最近更新 更多