Python: Update XML-file using ElementTree while conserving layout as much as possible
Question: I have a document which uses an XML namespace for which I want to increase
`/group/house/dogs` by one: (the file is called `houses.xml`)

    
    
    <?xml version="1.0"?>
    <group xmlns="http://dogs.house.local">
        <house>
                <id>2821</id>
                <dogs>2</dogs>
        </house>
    </group>
    

My current result using the code below is: (the created file is called
`houses2.xml`)

    
    
    <ns0:group xmlns:ns0="http://dogs.house.local">
        <ns0:house>
            <ns0:id>2821</ns0:id>
            <ns0:dogs>3</ns0:dogs>
        </ns0:house>
    </ns0:group>
    

I would like to fix two things (if it is possible using ElementTree. If it
isn´t, I´d be greatful for a suggestion as to what I should use instead):

  1. I want to keep the `<?xml version="1.0"?>` line.
  2. I do not want to prefix all tags, I´d like to keep it as is.

In conclusion, **I don´t want to mess with the document more than I absolutely
have to.**

My current code (which works except for the above mentioned flaws) generating
the above result follows.

I have made a utility function which loads an XML file using ElementTree and
returns the elementTree and the namespace (as I do not want to hard code the
namespace, and am willing to take the risk it implies):

    
    
    def elementTreeRootAndNamespace(xml_file):
        from xml.etree import ElementTree
        import re
        element_tree = ElementTree.parse(xml_file)
    
        # Search for a namespace on the root tag
        namespace_search = re.search('^({\S+})', element_tree.getroot().tag)
        # Keep the namespace empty if none exists, if a namespace exists set
        # namespace to {namespacename}
        namespace = ''
        if namespace_search:
            namespace = namespace_search.group(1)
    
        return element_tree, namespace
    

This is my code to update the number of dogs and save it to the new file
`houses2.xml`:

    
    
    elementTree, namespace = elementTreeRootAndNamespace('houses.xml')
    
    # Insert the namespace before each tag when when finding current number of dogs,
    # as ElementTree requires the namespace to be prefixed within {...} when a
    # namespace is used in the document.
    dogs = elementTree.find('{ns}house/{ns}dogs'.format(ns = namespace))
    
    # Increase the number of dogs by one
    dogs.text = str(int(dogs.text) + 1)
    
    # Write the result to the new file houses2.xml.
    elementTree.write('houses2.xml')
    


Answer: An XML based solution to this problem is to write a helper class for
ElementTree which:

  * Grabs the XML-declaration line before parsing as ElementTree at _the time of writing_ is unable to write an XML-declaration line without also writing an encoding attribute(I checked the source).
  * Parses the input file once, grabs the namespace of the root element. Registers that namespace with ElementTree as having the empty string as prefix. When that is done the source file is parsed using ElementTree _again_ , with that new setting.

It has one major drawback:

  * **XML-comments are lost.** Which I have learned is not acceptable for this situation(I initially didn´t think the input data had any comments, but it turns out it has).

My helper class with example:

    
    
    from xml.etree import ElementTree as ET
    import re
    
    
    class ElementTreeHelper():
        def __init__(self, xml_file_name):
            xml_file = open(xml_file_name, "rb")
    
            self.__parse_xml_declaration(xml_file)
    
            self.element_tree = ET.parse(xml_file)
            xml_file.seek(0)
    
            root_tag_namespace = self.__root_tag_namespace(self.element_tree)
            self.namespace = None
            if root_tag_namespace is not None:
                self.namespace = '{' + root_tag_namespace + '}'
                # Register the root tag namespace as having an empty prefix, as
                # this has to be done before parsing xml_file we re-parse.
                ET.register_namespace('', root_tag_namespace)
                self.element_tree = ET.parse(xml_file)
    
        def find(self, xpath_query):
            return self.element_tree.find(xpath_query)
    
        def write(self, xml_file_name):
            xml_file = open(xml_file_name, "wb")
            if self.xml_declaration_line is not None:
                xml_file.write(self.xml_declaration_line + '\n')
    
            return self.element_tree.write(xml_file)
    
        def __parse_xml_declaration(self, xml_file):
            first_line = xml_file.readline().strip()
            if first_line.startswith('<?xml') and first_line.endswith('?>'):
                self.xml_declaration_line = first_line
            else:
                self.xml_declaration_line = None
            xml_file.seek(0)
    
        def __root_tag_namespace(self, element_tree):
            namespace_search = re.search('^{(\S+)}', element_tree.getroot().tag)
            if namespace_search is not None:
                return namespace_search.group(1)
            else:
                return None
    
    
    def __main():
        el_tree_hlp = ElementTreeHelper('houses.xml')
    
        dogs_tag = el_tree_hlp.element_tree.getroot().find(
                       '{ns}house/{ns}dogs'.format(
                             ns=el_tree_hlp.namespace))
        one_dog_added = int(dogs_tag.text.strip()) + 1
        dogs_tag.text = str(one_dog_added)
    
        el_tree_hlp.write('hejsan.xml')
    
    if __name__ == '__main__':
        __main()
    

The output:

    
    
    <?xml version="1.0"?>
    <group xmlns="http://dogs.house.local">
        <house>
                <id>2821</id>
                <dogs>3</dogs>
        </house>
    </group>
    

If someone has an improvement to this solution please don´t hesitate to grab
the code and improve it.

