23

I have this array that I need to convert to xml.

array = [
    {
        'time': {"hour":"1", "minute":"30","seconds": "40"}
    },
    {
        'place': {"street":"40 something", "zip": "00000"}
    }
]

The xml should have a title that I can put in as a variable for example,

xml_title = "test"

The result I want based on the array above and the xml title is this:

<test>
    <time hour="1" minute="30" second="40"></time>
    <place>
        <street>40 something</street>
        <zip>00000</zip>
    </place>
</test>

I liked the answer given in a similar stack overflow question (https://stackoverflow.com/a/18991263/875139) but I am confused how I can use that answer to get this desired result.

Help please.

Community
  • 1
  • 1
user875139
  • 1,599
  • 4
  • 23
  • 44
  • 1
    and you tried ... what? – MSeifert Mar 15 '16 at 20:36
  • @MSeifert I tried this http://stackoverflow.com/a/18991263/875139 but it gave me this output: {"street":"40 something", "zip": "00000"} – user875139 Mar 15 '16 at 20:38
  • You are mixing XML elements and attributes. You can't do one or the other, you have to do both – OneCricketeer Mar 15 '16 at 20:38
  • @cricket_007 oh really? Ok let's say I just want it to be an element, how can I tackle this to give me this kind of result: 40 something 00000 – user875139 Mar 15 '16 at 20:41
  • I mean, at least I think so if you want to make it easier to convert. Also, is it possible to flatten the lists to just `'place': {"street":"40 something", "zip": "00000"}`, for example? Or will there be multiple places and times? – OneCricketeer Mar 15 '16 at 20:48
  • @cricket_007 I actually just double checked and it can actually be flattened. I edited the array in the question – user875139 Mar 15 '16 at 20:55

3 Answers3

26

As noted in the comments, your original question mixes attributes and elements. If you want everything as elements, you might be able to use dicttoxml. For example:

from dicttoxml import dicttoxml

array = [
    {
        'time': {"hour":"1", "minute":"30","seconds": "40"}
    },
    {
        'place': {"street":"40 something", "zip": "00000"}
    }
]

xml = dicttoxml(array, custom_root='test', attr_type=False)

Produces the following XML:

<?xml version="1.0" encoding="UTF-8" ?>
<test>
    <item>
        <time>
            <seconds>40</seconds>
            <minute>30</minute>
            <hour>1</hour>
        </time>
    </item>
    <item>
        <place>
            <street>40 something</street>
            <zip>00000</zip>
        </place>
    </item>
</test>

If you can convert your dictionary to:

dictionary = {
    'time': {"hour":"1", "minute":"30","seconds": "40"},
    'place': {"street":"40 something", "zip": "00000"}
}

Then your XML will look as desired.

<?xml version="1.0" encoding="UTF-8" ?>
<test>
    <place>
        <street>40 something</street>
        <zip>00000</zip>
    </place>
    <time>
        <seconds>40</seconds>
        <minute>30</minute>
        <hour>1</hour>
    </time>
</test>

Note that, in general, the order of dictionary keys are not guaranteed, so if you want to preserve the order of keys in a dict, you may want to check out collections.OrderedDict.

Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • 2
    Even though this `dicttoxml` thing still seems to work it was not updated on pypi / github since **july 8 2016**. This project seems to be dead, I am not quite sure if people should still use this. – Ente Sep 04 '19 at 06:22
  • @Ente use `xmltodict` instead. it has an `unparse` function which similar result. – Mahmoud Jan 04 '21 at 20:23
  • `AttributeError: module 'collections' has no attribute 'Iterable'` – Umair Ayub Aug 12 '22 at 06:26
7

For simple cases, you can go with something like this:

def object_to_xml(data: Union[dict, bool], root='object'):
    xml = f'<{root}>'
    if isinstance(data, dict):
        for key, value in data.items():
            xml += object_to_xml(value, key)

    elif isinstance(data, (list, tuple, set)):
        for item in data:
            xml += object_to_xml(item, 'item')

    else:
        xml += str(data)

    xml += f'</{root}>'
    return xml

Examples:

xml = object_to_xml([1, 2, 3], 'root')
# <root><item>1</item><item>2</item><item>3</item></root>
xml = object_to_xml({"name": "the matrix", "age": 20, "metadata": {"dateWatched": datetime.datetime.now()}}, 'movie')
# <movie><name>the matrix</name><age>20</age><metadata><dateWatched>2020-11-01 00:35:39.020358</dateWatched></metadata></movie>
Jossef Harush Kadouri
  • 32,361
  • 10
  • 130
  • 129
  • thanks! with a little bit of tweaking for newlines and indent, and using a different tag for list elements, it's just what I was looking for – nmz787 Oct 26 '21 at 17:56
1

I ended up taking the solution from here, then adding a for-loop over the elements in your array. The output uses attributes instead of elements like you had asked, though.

Full code outside of that function is this. I ended up using regex to strip out the intermediate <test></test> tags, then placed the on the outside at the end.

import re 

array = [
    {
        'time': {"hour":"1", "minute":"30","seconds": "40"}
    },
    {
        'place': {"street":"40 something", "zip": "00000"}
    }
]

xml_title = "test"
xml_tag_pattern = re.compile(r'</?{}>'.format(xml_title))
inner_xml = re.sub(xml_tag_pattern, '', ''.join(dict2xml(e, root_node=tag_name) for e in array))

print('<{0}>{1}</{0}>'.format(xml_title, inner_xml))

The output is this (new lines added for clarity)

<test>
    <time hour="1" seconds="40" minute="30"/>
    <place street="40 something" zip="00000"/>
</test>
Community
  • 1
  • 1
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245