3

I have a bunch of nested data in a format that loosely resembles 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=
{
    ...
}

There are many different parameters with varying levels of depth--this is just a very small subset.

It also might be worth noting that when a new sub-array is created that there is always an equals sign followed by a line break followed by the open bracket (as seen above).

Is there any simple looping or recursion technique for converting this data to a system-friendly data format such as arrays or JSON? I want to avoid hard-coding the names of properties. I am looking for something that will work in Python, Java, or PHP. Pseudo-code is fine, too.

I appreciate any help.

EDIT: I discovered the Pyparsing library for Python and it looks like it could be a big help. I can't find any examples for how to use Pyparsing to parse nested structures of unknown depth. Can anyone shed light on Pyparsing in terms of the data I described above?

EDIT 2: Okay, here is a working solution in 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()

This works, but when I try to write the results to a file with json.dump(result), the contents of the file are wrapped in double quotes. Also, there are \n chraacters between many of the data pairs. I tried suppressing them in the code above with LineEnd().suppress() , but I must not be using it correctly.

outis
  • 75,655
  • 22
  • 151
  • 221
Joshua Welker
  • 547
  • 1
  • 9
  • 20
  • 1
    The Examples page of the pyparsing wiki contains a number of examples of recursive structures - look for the ones tagged with the "spiral" icon. – PaulMcG Dec 19 '13 at 23:30
  • Thanks, I hadn't noticed most of those examples because I erroneously thought the only ones were the Under Development and User Contributed ones. – Joshua Welker Dec 20 '13 at 14:15
  • Why did you add `Optional(NL)` to the parse expression? One of pyparsing's main features is that it automatically skips over whitespace during pyparsing, and that includes newlines. That's why you don't see `+ Optional(White())` littered throughout the parser, unlike the way you have to sprinkle `\s*` all through a regex to handle places where whitespace might crop up. `result` is not a dict, even though you can access it like one - it is a ParseResults object, so `json.dump(result)` is probably not going to do what you want. But just as there is an asXML method, you could try writing asJSON. – PaulMcG Dec 20 '13 at 16:20
  • Hmm okay I will get rid of the Optional(NL) stuff and try what you said with AsJSON. – Joshua Welker Dec 20 '13 at 16:33

5 Answers5

6

Parsing an arbitrarily nested structure can be done with pyparsing by defining a placeholder to hold the nested part, using the Forward class. In this case, you are just parsing simple name-value pairs, where then value could itself be a nested structure containing name-value pairs.

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

This translates to pyparsing almost verbatim. To define value, which can recursively contain values, we first create a Forward() placeholder, which can be used as part of the definition of entry. Then once we have defined all the possible types of values, we use the '<<' operator to insert this definition into the value expression:

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)

The parse actions on real and integer will convert these elements from strings to float or ints at parse time, so that the values can be used as their actual types immediately after parsing (no need to post-process to do string-to-other-type conversion).

Your sample is a collection of one or more entries, so we use that to parse the total input:

result = OneOrMore(entry).parseString(sample)

We can access the parsed data as a nested list, but it is not so pretty to display. This code uses pprint to pretty-print a formatted nested list:

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

Giving:

[['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']]]]]]]]

Notice that all the strings are just strings with no enclosing quotation marks, and the ints are actual ints.

We can do just a little better than this, by recognizing that the entry format actually defines a name-value pair suitable for accessing like a Python dict. Our parser can do this with just a few minor changes:

Change the struct definition to:

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

and the overall parser to:

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

The Dict class treats the parsed contents as a name followed by a value, which can be done recursively. With these changes, we can now access the data in result like elements in a dict:

print result['phone']

or like attributes in an object:

print result.company

Use the dump() method to view the contents of a structure or substructure:

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

prints:

['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
PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • Absolutely perfect. Thank you so much! Also, excellent work on Pyparsing! I think I will be using it all the time now. – Joshua Welker Dec 20 '13 at 14:29
  • Okay, I updated my question with a brief addendum regarding `\n` characters and extra quotes. Can you provide any direction? – Joshua Welker Dec 20 '13 at 15:53
1

There is no "simple" way, but there are harder and not-so-hard ways. If you don't want to hardcode things, then at some point you're going to have to parse it as a structured format. That would involve parsing each line one-by-one, tokenizing it appropriately (for example, separating the key from the value correctly), and then determining how you want to deal with the line.

You may need to store your data in an intermediary format such as a (parse) tree in order to account for the arbitrary nesting relationships (represented by indents and braces), and then after you have finished parsing the data, take your resulting tree and then go through it again to get your arrays or JSON.

There are libraries available such as ANTLR that handles some of the manual work of figuring out how to write the parser.

MxLDevs
  • 19,048
  • 36
  • 123
  • 194
1

Take a look at this code:

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

You can convert your pseudo_json in parseable json via some substitutions.

  1. Replace '=' with ':'
  2. Add missing commas between simple value (like "2" or "Joe") and next field
  3. Add missing commas between closing brace of a complex value and next field
  4. Embrace it with braces

Still there are issues. In your example 'people' dictionary contains two similar keys 'person'. After parsing only one key remains in the dictionary. This is what I've got after parsing:{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'}}}

If only you could replace second occurence of 'person=' to 'person1=' and so on...

Graf
  • 1,437
  • 3
  • 17
  • 27
  • Thanks for the regex suggestions. Could the multiple `person` problem be solved by wrapping the content of `people` in a list? But then again I can't think of any way to do that other than hard-coding that behavior for the `people` property. – Joshua Welker Dec 19 '13 at 21:41
0

Replace the '=' with ':', Then just read it as json, add in trailing commas

mvn
  • 118
  • 1
  • 5
  • 1
    If only that were all there was to it. What does he do with equals signs that appear inside the properties? What's the logic to add the trailing commas? Parsing a "regular" format is one of those things you have to try a few times before realizing it's never easy. If parsing this stuff were easy, it would already be an accepted format and the OP would be done already. – Tom Dec 19 '13 at 20:37
  • 1
    Yeah that is a little too simplistic. Is there a regex pattern I could use to determine where the commas go? Because they can't go at the end of each line-- then I'd have stuff like this `=,` and this `{,`. – Joshua Welker Dec 19 '13 at 20:41
0

Okay, I came up with a final solution that actually transforms this data into a JSON-friendly Dict as I originally wanted. It first using Pyparsing to convert the data into a series of nested lists and then loops through the list and transforms it into JSON. This allows me to overcome the issue where Pyparsing's toDict() method was not able to handle where the same object has two properties of the same name. To determine whether a list is a plain list or a property/value pair, the prependPropertyToken method adds the string __property__ in front of property names when Pyparsing detects them.

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]
Joshua Welker
  • 547
  • 1
  • 9
  • 20