Removing empty xml nodes - python

I have an xml file that I'm trying to remove empty nodes from with python. When I've tested it to check if a the value is, say, 'shark', it works. But when i check for it being none, it doesn't remove the empty node.
for records in recordList:
for fieldGroup in records:
for field in fieldGroup:
if field.text is None:
fieldGroup.remove(field)

xpath is your friend here.
from lxml import etree
doc = etree.XML("""<root><a>1</a><b><c></c></b><d></d></root>""")
def remove_empty_elements(doc):
for element in doc.xpath('//*[not(node())]'):
element.getparent().remove(element)
Then:
>>> print etree.tostring(doc,pretty_print=True)
<root>
<a>1</a>
<b>
<c/>
</b>
<d/>
</root>
>>> remove_empty_elements(doc)
>>> print etree.tostring(doc,pretty_print=True)
<root>
<a>1</a>
<b/>
</root>
>>> remove_empty_elements(doc)
>>> print etree.tostring(doc,pretty_print=True)
<root>
<a>1</a>
</root>

Related

Parsing xml in python to get all child elements

I have parsed an XML file to get all its elements. I am getting the following output
[<Element '{urn:mitel:params:xml:ns:yang:vld}vld-list' at 0x0000000003059188>, <Element '{urn:mitel:params:xml:ns:yang:vld}vl-id' at 0x00000000030689F8>, <Element '{urn:mitel:params:xml:ns:yang:vld}descriptor-version' at 0x0000000003068A48>]
I need to select the value between } and ' only for each element of the list.
This is my Code till now :
import xml.etree.ElementTree as ET
tree = ET.parse('UMR_VLD01_OAM_V6-Provider_eth0.xml')
root = tree.getroot()
# all items
print('\nAll item data:')
for elem in root:
all_descendants = list(elem.iter())
print(all_descendants)
How can i achieve this ?
The text in {} is the namespace part of the qualified name (QName) of the XML element. AFAIK there is no method in ElementTree to return only the local name. So, you have to either
extract the local part of the name with string handling, as already proposed in a comment to your question,
use lxml.etree instead of xml.etree.ElementTree and apply xpath('local-name()') on each element,
or provide an XML source without namespace. You can strip the namespace with XSLT.
So, given this XML input:
<?xml version="1.0" encoding="UTF-8"?>
<foo xmlns="urn:mitel:params:xml:ns:yang:vld">
<bar>
<baz x="1"/>
<yet>
<more>
<nested/>
</more>
</yet>
</bar>
<bar/>
</foo>
You can print a list of the local names only with this variation of your program:
import xml.etree.ElementTree as ET
tree = ET.parse('UMR_VLD01_OAM_V6-Provider_eth0.xml')
root = tree.getroot()
# all items
print('\nAll item data:')
for elem in root:
all_descendants = [e.tag.split('}', 1)[1] for e in elem.iter()]
print(all_descendants)
Output:
['bar', 'baz', 'yet', 'more', 'nested']
['bar']
The version with lxml.etree and xpath('local-name()') looks like this:
import lxml.etree as ET
tree = ET.parse('UMR_VLD01_OAM_V6-Provider_eth0.xml')
root = tree.getroot()
# all items
print('\nAll item data:')
for elem in root:
all_descendants = [e.xpath('local-name()') for e in elem.iter()]
print(all_descendants)
The output is the same as with the string handling version.
For stripping the namespace completely from your input, you can apply this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="*">
<xsl:element name="{local-name()}">
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Then your original program outputs:
[<Element 'bar' at 0x04583B40>, <Element 'baz' at 0x04583B70>, <Element 'yet' at 0x04583BD0>, <Element 'more' at 0x04583C30>, <Element 'nested' at 0x04583C90>]
[<Element 'bar' at 0x04583CC0>]
Now the elements themselves do not bear a namespace. So, you don't have to strip it anymore.
You can apply the XSLT with with xsltproc, then you don't need to change your program. Alternatively, you can apply XSLT in python, but this also requires you to use lxml.etree. So, the last variation of your program looks like this:
import lxml.etree as ET
tree = ET.parse('UMR_VLD01_OAM_V6-Provider_eth0.xml')
xslt = ET.parse('stripns.xslt')
transform = ET.XSLT(xslt)
tree = transform(tree)
root = tree.getroot()
# all items
print('\nAll item data:')
for elem in root:
all_descendants = list(elem.iter())
print(all_descendants)

Removing an element, but not the text after it

I have an XML file similar to this:
<root>
<a>Some <b>bad</b> text <i>that</i> I <u>do <i>not</i></u> want to keep.</a>
</root>
I want to remove all text in <b> or <u> elements (and descendants), and print the rest. This is what I tried:
from __future__ import print_function
import xml.etree.ElementTree as ET
tree = ET.parse('a.xml')
root = tree.getroot()
parent_map = {c:p for p in root.iter() for c in p}
for item in root.findall('.//b'):
parent_map[item].remove(item)
for item in root.findall('.//u'):
parent_map[item].remove(item)
print(''.join(root.itertext()).strip())
(I used the recipe in this answer to build the parent_map). The problem, of course, is that with remove(item) I'm also removing the text after the element, and the result is:
Some that I
whereas what I want is:
Some text that I want to keep.
Is there any solution?
If you won't end up using anything better, you can use clear() instead of remove() keeping the tail of the element:
import xml.etree.ElementTree as ET
data = """<root>
<a>Some <b>bad</b> text <i>that</i> I <u>do <i>not</i></u> want to keep.</a>
</root>"""
tree = ET.fromstring(data)
a = tree.find('a')
for element in a:
if element.tag in ('b', 'u'):
tail = element.tail
element.clear()
element.tail = tail
print ET.tostring(tree)
prints (see empty b and u tags):
<root>
<a>Some <b /> text <i>that</i> I <u /> want to keep.</a>
</root>
Also, here's a solution using xml.dom.minodom:
import xml.dom.minidom
data = """<root>
<a>Some <b>bad</b> text <i>that</i> I <u>do <i>not</i></u> want to keep.</a>
</root>"""
dom = xml.dom.minidom.parseString(data)
a = dom.getElementsByTagName('a')[0]
for child in a.childNodes:
if getattr(child, 'tagName', '') in ('u', 'b'):
a.removeChild(child)
print dom.toxml()
prints:
<?xml version="1.0" ?><root>
<a>Some text <i>that</i> I want to keep.</a>
</root>

etree.strip_tags returning 'None' when trying to strip tag

Script:
print entryDetails
for i in range(len(entryDetails)):
print etree.tostring(entryDetails[i])
print etree.strip_tags(entryDetails[i], 'entry-details')
Output:
[<Element entry-details at 0x234e0a8>, <Element entry-details at 0x234e878>]
<entry-details>2014-02-05 11:57:01</entry-details>
None
<entry-details>2014-02-05 12:11:05</entry-details>
None
How is etree.strip_tags failing to strip the entry-details tag? Is the dash in the tag name affecting it?
strip_tags() does not return anything. It strips off the tags in-place.
The documentation says: "Note that this will not delete the element (or ElementTree root element) that you passed even if it matches. It will only treat its descendants.".
Demo code:
from lxml import etree
XML = """
<root>
<entry-details>ABC</entry-details>
</root>"""
root = etree.fromstring(XML)
ed = root.xpath("//entry-details")[0]
print ed
print
etree.strip_tags(ed, "entry-details") # Has no effect
print etree.tostring(root)
print
etree.strip_tags(root, "entry-details")
print etree.tostring(root)
Output:
<Element entry-details at 0x2123b98>
<root>
<entry-details>ABC</entry-details>
</root>
<root>
ABC
</root>

how can I select all descendants of a certain element with ElementTree in Python 3.3?

This is the sample data.
input.xml
<root>
<entry id="1">
<headword>go</headword>
<example>I <hw>go</hw> to school.</example>
</entry>
</root>
I'd like to put node and its descendants into . That is,
output.xml
<root>
<entry id="1">
<headword>go</headword>
<examplegrp>
<example>I <hw>go</hw> to school.</example>
</examplegrp>
</entry>
</root>
My poor and incomplete script is:
import codecs
import xml.etree.ElementTree as ET
fin = codecs.open(r'input.xml', 'rb', encoding='utf-8')
data = ET.parse(fin)
root = data.getroot()
example = root.find('.//example')
for elem in example.iter():
---and then I don't know what to do---
Here's an example of how it can be done:
text = """
<root>
<entry id="1">
<headword>go</headword>
<example>I <hw>go</hw> to school.</example>
</entry>
</root>
"""
import lxml.etree
import StringIO
data = lxml.etree.parse(StringIO.StringIO(text))
root = data.getroot()
for entry in root.xpath('//example/ancestor::entry[1]'):
examplegrp = lxml.etree.SubElement(entry,"examplegrp")
nodes = [node for node in entry.xpath('./example')]
for node in nodes:
entry.remove(node)
examplegrp.append(node)
print lxml.etree.tostring(root,pretty_print=True)
which will output:
<root>
<entry id="1">
<headword>go</headword>
<examplegrp><example>I <hw>go</hw> to school.</example>
</examplegrp></entry>
</root>
http://docs.python.org/3/library/xml.dom.html?highlight=xml#node-objects
http://docs.python.org/3/library/xml.dom.html?highlight=xml#document-objects
You probably want to follow some paradigm of creating a Document Element and appending reach result to it.
group = Document.createElement(tagName)
for found in founds:
group.appendNode(found)
Or something like this

I want to split text of a node and then put each of them into independent element

There is a xml file like this:
sample.xml
<root>
<keyword_group>
<headword>sell/buy</headword>
</keyword_group>
</root>
I'd like to split headword.text with '/' and then wrap each of them with tag. And finally I need to remove the tag . The output I expect is:
<root>
<keyword_group>
<word>sell</word>
<word>buy</word>
</keyword_group>
</root>
My ugly script is:
import lxml.etree as ET
xml = '''\
<root>
<keyword_group>
<headword>sell/buy</headword>
</keyword_group>
</root>
'''
root = ET.fromstring(xml)
headword = root.find('.//headword')
if headword is not None:
words = headword.text.split('/')
for word in words:
ET.SubElement(headword, 'word')
for wr in headword.iter('word'):
if not wr.text:
wr.text = word
headword.text = ''
print(ET.tostring(root, encoding='unicode'))
But this is too complicated, and I failed to remove headword tags.
Using lxml:
import lxml.etree as ET
xml = '''\
<root>
<keyword_group>
<headword>sell/buy</headword>
</keyword_group>
</root>
'''
root = ET.fromstring(xml)
headword = root.find('.//headword')
if headword is not None:
words = headword.text.split('/')
parent = headword.getparent()
parent.remove(headword)
for word in words:
ET.SubElement(parent, 'word').text = word
print(ET.tostring(root, encoding='unicode'))
yields
<root>
<keyword_group>
<word>sell</word><word>buy</word></keyword_group>
</root>

Categories