Using the xmltodict (v0.12.0) on python, I have an xml that will get parsed and converted into a json format. For example:
XML:
<test temp="temp" temp2="temp2">This is a test</test>
Will get converted to the following json:
"test": {
"#temp": "temp",
"#temp2": "temp2",
"#text": "This is a test"
}
I have a front end parser that reads JSON objects and converts them into XML. Unfortunately, the tags are required to be shaped in a different way.
What the front end parser expects:
{
test: {
"#": {
temp: "temp",
temp2: "temp2"
},
"#": "This is a test"
}
}
I feel like this formatting is better served to be modified on Python but I am having a bit of trouble iterating a much larger dictionary, where we don't know how deep an xml would go, and collecting all of the keys that start with "#" and giving that it's own object within the overall tag object. What are some ways I could approach shaping this data?
For anyone who is curious, this is how I ended up solving the issue. Like #furas stated, I decided that recursion was my best bet. I ended up iterating through my original xml data I converted to JSON with the incorrect formatting of attributes, then creating a copy while finding any attribute markers:
def structure_xml(data):
curr_dict = {}
for key,value in data.items():
if isinstance(value, dict):
curr_dict[key] = structure_xml(value)
elif isinstance(value, list):
value_list = []
for val in value:
if isinstance(val,dict) or isinstance(val,list):
value_list.append(structure_xml(val))
curr_dict[key] = value_list
else:
if '#' in key:
new_key = key.split("#",1)[1]
new_obj = { new_key: value }
if "#" in curr_dict:
curr_dict["#"][new_key] = value
else:
curr_dict["#"] = new_obj
elif '#text' in key:
curr_dict['#'] = data[key]
else:
curr_dict[key] = data[key]
return curr_dict
I have a json file (an ansible fact file)
What I'm trying to do is based on an array of keys, if the key is in the file, replace the value... This is so we can replace values people don't want to be made public (IP Address for example).
So far, the python I have can do it if its a simple Key value.... but not if its nested...
So this would be OK and it will replace...
"ansible_diff_mode": false,
"ansible_distribution": "CentOS",
"ansible_distribution_file_parsed": true,
"ansible_distribution_file_path": "/etc/redhat-release",
"ansible_distribution_file_variety": "RedHat",
"ansible_distribution_major_version": "7",
"ansible_distribution_release": "Core",
However, It can't find these values...
"ansible_all_ipv4_addresses": [
"1.2.3.4"
],
"ansible_apparmor": {
"status": "disabled"
},
Here is the code I'm using, and appreciate any pointers...
import json
keys_to_sanitise = ['ansible_all_ipv4_addressess','ansible_machine','ansible_bios_version',
'ansible_domain','environment']
factfile = 'host.yaml'
def sanitiseDict(d):
for k in keys_to_sanitise:
if k in d.keys():
d.update({k: 'EXCLUDED'})
for v in d.values():
if isinstance(v, dict):
sanitiseDict(v)
return
with open(factfile, "r") as infile:
jdata = json.load(infile)
mydict = {}
sanitiseDict(jdata)
print(json.dumps(jdata))
Well, for starters you have an extra s in 'ansible_all_ipv4_addressess'.
You can also clean up the syntax of sanitiseDict a bit, to get this, which reads a litte better:
def sanitiseDict(d):
for k in keys_to_sanitise:
if k in d:
d[k] = 'EXCLUDED'
for v in d.values():
if isinstance(v, dict):
sanitiseDict(v)
I am trying to convert a Json file that looks like
{
# "item_1":"value_11",
# "item_2":"value_12",
# "item_3":"value_13",
# "item_4":["sub_value_14", "sub_value_15"],
# "item_5":{
# "sub_item_1":"sub_item_value_11",
# "sub_item_2":["sub_item_value_12", "sub_item_value_13"]
# }
# }
TO something that looks like this:
{
# "node_item_1":"value_11",
# "node_item_2":"value_12",
# "node_item_3":"value_13",
# "node_item_4_0":"sub_value_14",
# "node_item_4_1":"sub_value_15",
# "node_item_5_sub_item_1":"sub_item_value_11",
# "node_item_5_sub_item_2_0":"sub_item_value_12",
# "node_item_5_sub_item_2_0":"sub_item_value_13"
# }
I am aware that you can't maintain the order of the Json file when converted to CSV. I am considering to do a workaround by loading the JSON data into OrderedDic objects (which cause them to be added in the order that the input document lists them. However, I am new to working with JSON files, as well as OrderedDic function.
To split items into subgroups i used:
def reduce_item(key, value):
global reduced_item
#Reduction Condition 1
if type(value) is list:
i=0
for sub_item in value:
reduce_item(key+'_'+to_string(i), sub_item)
i=i+1
#Reduction Condition 2
elif type(value) is dict:
sub_keys = value.keys()
for sub_key in sub_keys:
reduce_item(key+'_'+to_string(sub_key), value[sub_key])
#Base Condition
else:
reduced_item[to_string(key)] = to_string(value)
But how do I use the orderedDic along with the above code to show this output:
{
# "node_item_1":"value_11",
# "node_item_2":"value_12",
# "node_item_3":"value_13",
# "node_item_4_0":"sub_value_14",
# "node_item_4_1":"sub_value_15",
# "node_item_5_sub_item_1":"sub_item_value_11",
# "node_item_5_sub_item_2_0":"sub_item_value_12",
# "node_item_5_sub_item_2_0":"sub_item_value_13"
# }
I have the below code as well but it does not split each in subgroups based on the conditions of the subtring code above:
import json
from collections import OrderedDict
with open("/home/file/official.json", 'r') as fp:
metrics_types = json.load(fp, object_pairs_hook=OrderedDict)
print(metrics_types)
That shows:
Any suggestions?
You can use a function that iterates through the given dict or list items and merges the keys from the dict output of the recursive calls:
def flatten(d):
if not isinstance(d, (dict, list)):
return d
out = {}
for k, v in d.items() if isinstance(d, dict) else enumerate(d):
f = flatten(v)
if isinstance(f, dict):
out.update({'%s_%s' % (k, i): s for i, s in f.items()})
else:
out[k] = f
return out
so that given:
d = {
"item_1":"value_11",
"item_2":"value_12",
"item_3":"value_13",
"item_4":["sub_value_14", "sub_value_15"],
"item_5":{
"sub_item_1":"sub_item_value_11",
"sub_item_2":["sub_item_value_12", "sub_item_value_13"]
}
}
flatten(d) returns:
{'item_1': 'value_11',
'item_2': 'value_12',
'item_3': 'value_13',
'item_4_0': 'sub_value_14',
'item_4_1': 'sub_value_15',
'item_5_sub_item_1': 'sub_item_value_11',
'item_5_sub_item_2_0': 'sub_item_value_12',
'item_5_sub_item_2_1': 'sub_item_value_13'}
The above assumes that you're using Python 3.7 or later, where dict keys are guaranteed to be ordered. If you're using earlier versions, you can use OrderedDict in place of a regular dict.
I am trying to set the python json library up in order to save to file a dictionary having as elements other dictionaries. There are many float numbers and I would like to limit the number of digits to, for example, 7.
According to other posts on SO encoder.FLOAT_REPR shall be used. However it is not working.
For example the code below, run in Python3.7.1, prints all the digits:
import json
json.encoder.FLOAT_REPR = lambda o: format(o, '.7f' )
d = dict()
d['val'] = 5.78686876876089075543
d['name'] = 'kjbkjbkj'
f = open('test.json', 'w')
json.dump(d, f, indent=4)
f.close()
How can I solve that?
It might be irrelevant but I am on macOS.
EDIT
This question was marked as duplicated. However in the accepted answer (and until now the only one) to the original post it is clearly stated:
Note: This solution doesn't work on python 3.6+
So that solution is not the proper one. Plus it is using the library simplejson not the library json.
It is still possible to monkey-patch json in Python 3, but instead of FLOAT_REPR, you need to modify float. Make sure to disable c_make_encoder just like in Python 2.
import json
class RoundingFloat(float):
__repr__ = staticmethod(lambda x: format(x, '.2f'))
json.encoder.c_make_encoder = None
if hasattr(json.encoder, 'FLOAT_REPR'):
# Python 2
json.encoder.FLOAT_REPR = RoundingFloat.__repr__
else:
# Python 3
json.encoder.float = RoundingFloat
print(json.dumps({'number': 1.0 / 81}))
Upsides: simplicity, can do other formatting (e.g. scientific notation, strip trailing zeroes etc). Downside: it looks more dangerous than it is.
Option 1: Use regular expression matching to round.
You can dump your object to a string using json.dumps and then use the technique shown on this post to find and round your floating point numbers.
To test it out, I added some more complicated nested structures on top of the example you provided::
d = dict()
d['val'] = 5.78686876876089075543
d['name'] = 'kjbkjbkj'
d["mylist"] = [1.23456789, 12, 1.23, {"foo": "a", "bar": 9.87654321}]
d["mydict"] = {"bar": "b", "foo": 1.92837465}
# dump the object to a string
d_string = json.dumps(d, indent=4)
# find numbers with 8 or more digits after the decimal point
pat = re.compile(r"\d+\.\d{8,}")
def mround(match):
return "{:.7f}".format(float(match.group()))
# write the modified string to a file
with open('test.json', 'w') as f:
f.write(re.sub(pat, mround, d_string))
The output test.json looks like:
{
"val": 5.7868688,
"name": "kjbkjbkj",
"mylist": [
1.2345679,
12,
1.23,
{
"foo": "a",
"bar": 9.8765432
}
],
"mydict": {
"bar": "b",
"foo": 1.9283747
}
}
One limitation of this method is that it will also match numbers that are within double quotes (floats represented as strings). You could come up with a more restrictive regex to handle this, depending on your needs.
Option 2: subclass json.JSONEncoder
Here is something that will work on your example and handle most of the edge cases you will encounter:
import json
class MyCustomEncoder(json.JSONEncoder):
def iterencode(self, obj):
if isinstance(obj, float):
yield format(obj, '.7f')
elif isinstance(obj, dict):
last_index = len(obj) - 1
yield '{'
i = 0
for key, value in obj.items():
yield '"' + key + '": '
for chunk in MyCustomEncoder.iterencode(self, value):
yield chunk
if i != last_index:
yield ", "
i+=1
yield '}'
elif isinstance(obj, list):
last_index = len(obj) - 1
yield "["
for i, o in enumerate(obj):
for chunk in MyCustomEncoder.iterencode(self, o):
yield chunk
if i != last_index:
yield ", "
yield "]"
else:
for chunk in json.JSONEncoder.iterencode(self, obj):
yield chunk
Now write the file using the custom encoder.
with open('test.json', 'w') as f:
json.dump(d, f, cls = MyCustomEncoder)
The output file test.json:
{"val": 5.7868688, "name": "kjbkjbkj", "mylist": [1.2345679, 12, 1.2300000, {"foo": "a", "bar": 9.8765432}], "mydict": {"bar": "b", "foo": 1.9283747}}
In order to get other keyword arguments like indent to work, the easiest way would be to read in the file that was just written and write it back out using the default encoder:
# write d using custom encoder
with open('test.json', 'w') as f:
json.dump(d, f, cls = MyCustomEncoder)
# load output into new_d
with open('test.json', 'r') as f:
new_d = json.load(f)
# write new_d out using default encoder
with open('test.json', 'w') as f:
json.dump(new_d, f, indent=4)
Now the output file is the same as shown in option 1.
Here's something that you may be able to use that's based on my answer to the question:
Write two-dimensional list to JSON file.
I say may because it requires "wrapping" all the float values in the Python dictionary (or list) before JSON encoding it with dump().
(Tested with Python 3.7.2.)
from _ctypes import PyObj_FromPtr
import json
import re
class FloatWrapper(object):
""" Float value wrapper. """
def __init__(self, value):
self.value = value
class MyEncoder(json.JSONEncoder):
FORMAT_SPEC = '##{}##'
regex = re.compile(FORMAT_SPEC.format(r'(\d+)')) # regex: r'##(\d+)##'
def default(self, obj):
return (self.FORMAT_SPEC.format(id(obj)) if isinstance(obj, FloatWrapper)
else super(MyEncoder, self).default(obj))
def iterencode(self, obj, **kwargs):
for encoded in super(MyEncoder, self).iterencode(obj, **kwargs):
# Check for marked-up float values (FloatWrapper instances).
match = self.regex.search(encoded)
if match: # Get FloatWrapper instance.
id = int(match.group(1))
float_wrapper = PyObj_FromPtr(id)
json_obj_repr = '%.7f' % float_wrapper.value # Create alt repr.
encoded = encoded.replace(
'"{}"'.format(self.FORMAT_SPEC.format(id)), json_obj_repr)
yield encoded
d = dict()
d['val'] = FloatWrapper(5.78686876876089075543) # Must wrap float values.
d['name'] = 'kjbkjbkj'
with open('float_test.json', 'w') as file:
json.dump(d, file, cls=MyEncoder, indent=4)
Contents of file created:
{
"val": 5.7868688,
"name": "kjbkjbkj"
}
Update:
As I mentioned, the above requires all the float values to be wrapped before calling json.dump(). Fortunately doing that could be automated by adding and using the following (minimally tested) utility:
def wrap_type(obj, kind, wrapper):
""" Recursively wrap instances of type kind in dictionary and list
objects.
"""
if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
if not isinstance(value, (dict, list)):
new_dict[key] = wrapper(value) if isinstance(value, kind) else value
else:
new_dict[key] = wrap_type(value, kind, wrapper)
return new_dict
elif isinstance(obj, list):
new_list = []
for value in obj:
if not isinstance(value, (dict, list)):
new_list.append(wrapper(value) if isinstance(value, kind) else value)
else:
new_list.append(wrap_type(value, kind, wrapper))
return new_list
else:
return obj
d = dict()
d['val'] = 5.78686876876089075543
d['name'] = 'kjbkjbkj'
with open('float_test.json', 'w') as file:
json.dump(wrap_type(d, float, FloatWrapper), file, cls=MyEncoder, indent=4)
Here is a python code snippet that shows how to quantize json output to the specified number of digits:
#python example code, error handling not shown
#open files
fin = open(input_file_name)
fout = open(output_file_name, "w+")
#read file input (note this could be done in one step but breaking it up allows more flexibilty )
indata = fin.read()
# example quantization function
def quant(n):
return round((float(n) * (10 ** args.prec))) / (
10 ** args.prec
) # could use decimal.quantize
# process the data streams by parsing and using call back to quantize each float as it parsed
outdata = json.dumps(json.loads(indata, parse_float=quant), separators=(",", ":"))
#write output
fout.write(outdata)
The above is what the jsonvice command-line tool uses to quantize the floating-point json numbers to whatever precision is desired to save space.
https://pypi.org/project/jsonvice/
This can be installed with pip or pipx (see docs).
pip3 install jsonvice
Disclaimer: I wrote this when needing to test quantized machine learning model weights.
I found the above options within the python standard library to be very limiting and cumbersome, so if you're not strictly limited to the python standard lib, pandas has a json module that includes a dumps method which has a double_precision parameter to control the number of digits in a float (default 10):
import json
import pandas.io.json
d = {
'val': 5.78686876876089075543,
'name': 'kjbkjbkj',
}
print(json.dumps(d))
print(pandas.io.json.dumps(d))
print(pandas.io.json.dumps(d, double_precision=5))
gives:
{"val": 5.786868768760891, "name": "kjbkjbkj"}
{"val":5.7868687688,"name":"kjbkjbkj"}
{"val":5.78687,"name":"kjbkjbkj"}
Doesn't answer this question, but for the decoding side, you could do something like this, or override the hook method.
To solve this problem with this method though would require encoding, decoding, then encoding again, which is overly convoluted and no longer the best choice. I assumed Encode had all the bells and whistles Decode did, my mistake.
# d = dict()
class Round7FloatEncoder(json.JSONEncoder):
def iterencode(self, obj):
if isinstance(obj, float):
yield format(obj, '.7f')
with open('test.json', 'w') as f:
json.dump(d, f, cls=Round7FloatEncoder)
Inspired by this answer, here is a solution that works for Python >= 3.6 (tested with 3.9) and that allows customization of the format on a case by case basis. It works for both json and simplejson (tested with json=2.0.9 and simplejson=3.17.6).
Note however that this is not thread-safe.
from contextlib import contextmanager
class FormattedFloat(float):
def __new__(self, value, fmt=None):
return float.__new__(self, value)
def __init__(self, value, fmt=None):
float.__init__(value)
if fmt:
self.fmt = fmt
def __repr__(self):
if hasattr(self, 'fmt'):
return f'{self:{self.fmt}}'
return float.__repr__(self)
#contextmanager
def formatted_floats():
c_make_encoder = json.encoder.c_make_encoder
json_float = json.encoder.float
json.encoder.c_make_encoder = None
json.encoder.float = FormattedFloat
try:
yield
finally:
json.encoder.c_make_encoder = c_make_encoder
json.encoder.float = json_float
Example
x = 12345.6789
d = dict(
a=x,
b=FormattedFloat(x),
c=FormattedFloat(x, '.4g'),
d=FormattedFloat(x, '.08f'),
)
>>> d
{'a': 12345.6789, 'b': 12345.6789, 'c': 1.235e+04, 'd': 12345.67890000}
Now,
with formatted_floats():
out = json.dumps(d)
>>> out
'{"a": 12345.6789, "b": 12345.6789, "c": 1.235e+04, "d": 12345.67890000}'
>>> json.loads(out)
{'a': 12345.6789, 'b': 12345.6789, 'c': 12350.0, 'd': 12345.6789}
Note that the original json.encoder attributes are restored by the context manager, so:
>>> json.dumps(d)
'{"a": 12345.6789, "b": 12345.6789, "c": 12345.6789, "d": 12345.6789}'
I found a method to iterate a python dictionary object recursively on this forum. However, I wish to extend that function so that I get a string similar to the structure of a file path. With my function below, I expect an output in the form of
/key1/value1
/key2/value2
/key3/key3a/value3a
/key4/key4a/key4a1/value4a1
/key4/key4a/key4a2/value4a2
/key4/key4a/key4a3/value4a3
/key4/key4b/key4b1/key4b1a/value4b1a
/key4/key4b/key4b1/key4b1b/value4b1b
/key4/key4b/key4b1/key4b1c/value4b1c
/key4/key4c/key4c1/key4c1a/value4c1a
/key4/key4c/key4c1/key4c1b/value4c1b
/key4/key4c/key4c1/key4c1c/value4c1c
Unfortunately, I hit a block. I cannot figure out how to achieve that. Below is the code that I came up with. Any help is greatly appreciated.
import sys
import collections
dict_object = {
'key1': 'value1',
'key2': 'value2',
'key3': {'key3a': 'value3a'},
'key4': {
'key4a': {
'key4a1': 'value4a1',
'key4a2': 'value4a2',
'key4a3': 'value4a3'
},
'key4b': {
'key4b1': {
'key4b1a': 'value4b1a',
'key4b1b': 'value4b1b',
'key4b1c': 'value4b1c'
},
'key4c': {
'key4c1': {
'key4c1a': 'value4c1a',
'key4c1b': 'value4c1b',
'key4c1c': 'value4c1c'
}
}
}
}
}
def print_dict(dictionary, path='', parent=''):
""" This finction recursively prints nested dictionaries."""
#Sort the dictionary object by its keys
if isinstance(dictionary, dict):
dictionary = collections.OrderedDict(sorted(dictionary.items()))
else:
dictionary = sorted(dictionary.items(), key=operator.itemgetter(1))
#iterate each sorted dictionary key
for key, value in dictionary.iteritems():
if isinstance(value, dict):
path = ''
path = '%s/%s/%s' % (path, parent, key)
#Repeat this funtion for nested {} instances
print_dict(value, path, key)
else:
#Print the last node i.e PATH + KEY + VALUE
print '%s/%s/%s' % (path, key, value)
if __name__ == '__main__':
print_dict(dict_object)
Your function appears overly complicated. Only actually print when you have an object that's not a dictionary, otherwise recurse for all values in a dictionary. I simplified path handling to just one string:
def print_dict(ob, path=''):
if not isinstance(ob, dict):
print '{}/{}'.format(path, ob)
else:
for key, value in sorted(ob.items()):
print_dict(value, '{}/{}'.format(path, key))
I didn't bother with creating OrderedDict objects; all you need is iteration in sorted order.
This produces the expected output:
>>> print_dict(dict_object)
/key1/value1
/key2/value2
/key3/key3a/value3a
/key4/key4a/key4a1/value4a1
/key4/key4a/key4a2/value4a2
/key4/key4a/key4a3/value4a3
/key4/key4b/key4b1/key4b1a/value4b1a
/key4/key4b/key4b1/key4b1b/value4b1b
/key4/key4b/key4b1/key4b1c/value4b1c
/key4/key4b/key4c/key4c1/key4c1a/value4c1a
/key4/key4b/key4c/key4c1/key4c1b/value4c1b
/key4/key4b/key4c/key4c1/key4c1c/value4c1c