20

I have thousands of text files containing multiple JSON objects, but unfortunately there is no delimiter between the objects.

The objects are stored as dictionaries and some of their fields are themselves objects. Each object might have a variable number of nested objects. Concretely, an object might look like this:

{field1: {}, field2: "some value", field3: {}, ...} 

and hundreds of such objects are concatenated without a delimiter in a text file. This means that I can neither use json.load() nor json.loads().

Any suggestion on how I can solve this problem. Is there a known parser to do this?

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
Lejlek
  • 814
  • 3
  • 9
  • 15
  • 2
    are they at least separated onto different lines, or it is just one long single-line `{...}{...}{...}` pileup? – Marc B Jan 04 '12 at 16:24
  • 1
    No, that's the problem, it's just one long single-line. – Lejlek Jan 04 '12 at 16:39
  • Could you add delimiters using `str.replace`? As in: `single_line_json.replace('}{',}\n{')` – aganders3 Jan 04 '12 at 16:40
  • if you need an even faster solution you can avoid the large object list by switching to a generator: ```while end != s_len: obj, end = decoder.raw_decode(s, idx=end) yield obj```. – tback Jan 04 '12 at 17:54

9 Answers9

23

This decodes your "list" of JSON Objects from a string:

from json import JSONDecoder

def loads_invalid_obj_list(s):
    decoder = JSONDecoder()
    s_len = len(s)

    objs = []
    end = 0
    while end != s_len:
        obj, end = decoder.raw_decode(s, idx=end)
        objs.append(obj)

    return objs

The bonus here is that you play nice with the parser. Hence it keeps telling you exactly where it found an error.

Examples

>>> loads_invalid_obj_list('{}{}')
[{}, {}]

>>> loads_invalid_obj_list('{}{\n}{')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "decode.py", line 9, in loads_invalid_obj_list
    obj, end = decoder.raw_decode(s, idx=end)
  File     "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 376, in raw_decode
    obj, end = self.scan_once(s, idx)
ValueError: Expecting object: line 2 column 2 (char 5)

Clean Solution (added later)

import json
import re

#shameless copy paste from json/decoder.py
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)

class ConcatJSONDecoder(json.JSONDecoder):
    def decode(self, s, _w=WHITESPACE.match):
        s_len = len(s)

        objs = []
        end = 0
        while end != s_len:
            obj, end = self.raw_decode(s, idx=_w(s, end).end())
            end = _w(s, end).end()
            objs.append(obj)
        return objs

Examples

>>> print json.loads('{}', cls=ConcatJSONDecoder)
[{}]

>>> print json.load(open('file'), cls=ConcatJSONDecoder)
[{}]

>>> print json.loads('{}{} {', cls=ConcatJSONDecoder)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 339, in loads
    return cls(encoding=encoding, **kw).decode(s)
  File "decode.py", line 15, in decode
    obj, end = self.raw_decode(s, idx=_w(s, end).end())
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 376, in raw_decode
    obj, end = self.scan_once(s, idx)
ValueError: Expecting object: line 1 column 5 (char 5)
tback
  • 11,138
  • 7
  • 47
  • 71
  • 1
    Really cool, I was hoping that the json module would have something like this and it has. It's perfect. Thank you! – Lejlek Jan 04 '12 at 17:38
4

Solution

As far as I know }{ does not appear in valid JSON, so the following should be perfectly safe when trying to get strings for separate objects that were concatenated (txt is the content of your file). It does not require any import (even of re module) to do that:

retrieved_strings = map(lambda x: '{'+x+'}', txt.strip('{}').split('}{'))

or if you prefer list comprehensions (as David Zwicker mentioned in the comments), you can use it like that:

retrieved_strings = ['{'+x+'}' for x in txt.strip('{}').split('}{'))]

It will result in retrieved_strings being a list of strings, each containing separate JSON object. See proof here: http://ideone.com/Purpb

Example

The following string:

'{field1:"a",field2:"b"}{field1:"c",field2:"d"}{field1:"e",field2:"f"}'

will be turned into:

['{field1:"a",field2:"b"}', '{field1:"c",field2:"d"}', '{field1:"e",field2:"f"}']

as proven in the example I mentioned.

Tadeck
  • 132,510
  • 28
  • 152
  • 198
  • This should be done using a list comprehension `retrieved_strings = ['{'+x+'}' for x in txt.strip('{}').split('}{')]` – David Zwicker Jan 04 '12 at 16:58
  • @DavidZwicker: why? Are you one of the supporters of the `map()` function considered as deprecated? It is perfectly valid. However it may look simpler, I will add this to my answer. – Tadeck Jan 04 '12 at 17:00
  • 3
    valid json with `}{` : `'{"f1" : "}{}{", "b" : "{{}{}}{{{}{}"}'` – soulcheck Jan 04 '12 at 17:03
  • 1
    @Tadeck: See http://stackoverflow.com/questions/1247486/python-list-comprehension-vs-map for a discussion on map vs list-comprehension. I actually use `map` myself sometimes, but only on occasions, where the function already exists. Using `lambda` in conjunction with `map` does not make a lot of sense to me. – David Zwicker Jan 04 '12 at 17:07
  • @soulcheck: +1, very good point! It still can be solved, but now it requires checking if the `}{` sequence occurs within quotes... – Tadeck Jan 04 '12 at 17:07
4

Sebastian Blask's answer has the right idea, but there's no reason to use regexes for such a simple change.

objs = json.loads("[%s]"%(open('your_file.name').read().replace('}{', '},{')))

Or, more legibly

raw_objs_string = open('your_file.name').read() #read in raw data
raw_objs_string = raw_objs_string.replace('}{', '},{') #insert a comma between each object
objs_string = '[%s]'%(raw_objs_string) #wrap in a list, to make valid json
objs = json.loads(objs_string) #parse json
Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
Patrick Perini
  • 22,555
  • 12
  • 59
  • 88
3

You can load the file as a string, replace all }{ with },{ and surround the whole thing with []?

Something like:

re.sub('\}\s*?\{', '\}, \{', string_read_from_a_file)

Or a simple string replace if you are sure you always have }{ without whitespaces in between.

In case you expect }{ to occur in strings as well, you could also split on }{ and evaluate each fragment with json.load, and in case you get an error, the fragment wasn't complete and you have to add the next to the first one and so forth.

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
Sebastian Blask
  • 2,870
  • 1
  • 16
  • 29
  • Cool! That's clever and easy to do. I'll try it and come back with the result. Thank you! – Lejlek Jan 04 '12 at 16:49
  • 1
    what happens if you have '}{' string in some other places, like property values? for example: `'{"field1" : "}{123", "field2" : "123"}'` – soulcheck Jan 04 '12 at 17:02
3

How about something like this:

import re
import json

jsonstr = open('test.json').read()

p = re.compile( '}\s*{' )
jsonstr = p.sub( '}\n{', jsonstr )

jsonarr = jsonstr.split( '\n' )

for jsonstr in jsonarr:
   jsonobj = json.loads( jsonstr )
   print json.dumps( jsonobj )
Joshua
  • 133
  • 1
  • 5
1
import json

file1 = open('filepath', 'r')
data = file1.readlines()

for line in data :
   values = json.loads(line)

'''Now you can access all the objects using values.get('key') '''
Swapnil
  • 159
  • 1
  • 11
  • This only works if the JSON objects are separated by newlines, but OP already said in a comment that the objects are in 1 single line: https://stackoverflow.com/questions/8730119/how-to-retrieve-json-objects-from-a-text-file-using-python-where-the-objects-are#comment10871293_8730119 – Gino Mempin Mar 21 '23 at 03:23
1

How about reading through the file incrementing a counter every time a { is found and decrementing it when you come across a }. When your counter reaches 0 you'll know that you've come to the end of the first object so send that through json.load and start counting again. Then just repeat to completion.

redrah
  • 1,204
  • 11
  • 20
0

Suppose you added a [ to the start of the text in a file, and used a version of json.load() which, when it detected the error of finding a { instead of an expected comma (or hits the end of the file), spit out the just-completed object?

Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
  • Oh, I see your point. Are you suggesting to use a try/except and then split whenever the column index shows? I tried it quickly and I get the exception: "Expecting , delimiter: line 1 column 1332 (char 1332). It's doable. I was just hoping that there was a parser out there, since it seems like something that might happen. But thanks for this suggestion. – Lejlek Jan 04 '12 at 16:41
0

Replace a file with that junk in it:

$ sed -i -e 's;}{;}, {;g' foo

Do it on the fly in Python:

junkJson.replace('}{', '}, {')
Spencer
  • 665
  • 3
  • 8