tags:

views:

90

answers:

2

Hi, I got stuck with XML and Python. The task is simple but I couldn't resolve it so far and spent on that long time. I came here for an advice how to solve it with couple of lines.

Thanks for any help with traversing the tree. I always ended up with too many or too few elements. Elements can be nested without limit. Given example is just an example. I will accept any solution, not picky about dom, minidom, sax, whatever..

I have an XML file similar to this one:

<root>
    <elm>
        <elm>Common content</elm>

        <elm xmlns="http://example.org/ns"&gt;
            <elm lang="en">Content EN</elm>
            <elm lang="cs">žluťoučký koníček</elm>
        </elm>

        <elm xml:id="abc123">Common content</elm>

        <elm lang="en">Content EN</elm>
        <elm lang="cs">Content CS</elm>

        <elm lang="en">
            <elm>Content EN</elm>
            <elm>Content EN</elm>
        </elm>

        <elm lang="cs">
            <elm>Content CS</elm>
            <elm>Content CS</elm>
        </elm>
    </elm>
</root>

What I need - parse the XML and write a new file. The new file should contain all the elements for given language and elements without lang attribute.

For "cs" language the output file should containt this:

<root>
    <elm>
        <elm>Common content</elm>

        <elm xmlns="http://example.org/ns"&gt;
            <elm lang="cs">žluťoučký koníček</elm>
        </elm>

        <elm xml:id="abc123">Common content</elm>

        <elm lang="cs">Content CS</elm>

        <elm lang="cs">
            <elm>Content CS</elm>
            <elm>Content CS</elm>
        </elm>
    </elm>
</root>

If you can make it to omit the lang attribute in the new file, even better. But it's not that important.

UPDATE1: Added unicode characters and namespace attribute.

UPDATE2: Using Python 2.5, standard libraries preferred.

+3  A: 

I'm not sure how best to remove the lang attribute, but here's some code that does the other changes (Python 2.7; for 2.5 or 2.6, use getIterator instead of iter), assuming that when you remove an element you also always want to remove everything contained in that element.

This code just prints the result to standard output (you could redirect it as you wish, of course, or directly write it to some new file, and so on):

import sys
from xml.etree import cElementTree as et

def picklang(path, lang='en'):
    tr = et.parse(path)
    for element in tr.iter():
        for subelement in element:
            la = subelement.get('lang')
            if la is not None and la != lang:
                element.remove(subelement)
    return tr

if __name__ == '__main__':
    tr = picklang('la.xml')
    tr.write(sys.stdout)
    print

With la.xml being your example, this writes

<root>
    <elm>Common content</elm>

    <elm>
        <elm lang="en">Content EN</elm>
        </elm>

    <elm>Common content</elm>

    <elm lang="en">Content EN</elm>
    <elm lang="en">
        <elm>Content EN</elm>
        <elm>Content EN</elm>
    </elm>

    </root>
Alex Martelli
Thank you Alex, works great. Except two things - namespace and unicode. If there's an xmlns attribute, for example `<elm xmlns="http://example.org/ns">`, the new node itself gets an `xmlns:ns0="http://example.org/ns"` attribute and all the child nodes get an `<ns0:` prefix. These prefixes are not present in the source file. Also cannot force write() method to write unicode characters in their original form. I'll update the example file.
dwich
@dwich, for the writing you can just add to the `write` call an `encoding` parameter of your choice. Aesthetics such as the namespace issue (which I believe don't change the semantics of the XML) are much ticklier to deal with, alas (just like, e.g., you may have noticed, the indentation in the output is different, because whitespace in elements being removed also goes away).
Alex Martelli
@Alex: That unicode thing was my mistake, I started playing with codecs and even though I used `encoding='utf-8'`, it didn't work (coz of opening it incorrectly). Thank you for your answer, I will pick ~unutbu`s solution as his code doesn't have problems with the namespace thing. Both answers are correct. Thank you guys!
dwich
@dwich, I agree with you - @unutbu's answer is better (if you can use third party packages like lxml), among other things because it does remove the attribute, as you ideally desired, while mine, as I mentioned, didn't.
Alex Martelli
+2  A: 

Using lxml:

import lxml.etree as le

with open('doc.xml','r') as f:
    doc=le.parse(f)
    for elem in doc.xpath('//*[attribute::lang]'):
        if elem.attrib['lang']=='en':
            elem.attrib.pop('lang')
        else:
            parent=elem.getparent()
            parent.remove(elem)
    print(le.tostring(doc))

yields

<root>
    <elm>Common content</elm>

    <elm>
        <elm>Content EN</elm>
        </elm>

    <elm>Common content</elm>

    <elm>Content EN</elm>
    <elm>
        <elm>Content EN</elm>
        <elm>Content EN</elm>
    </elm>

    </root>
unutbu
Thanks a lot. Can't install lxml on my WinXP, problem with compiler. Will give it a try later.
dwich
Works! Thanks! You saved my night :) I thank both of you, both solutions are good.
dwich
Glad I could help :)
unutbu