15

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">
            <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">
            <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.

dwich
  • 1,710
  • 1
  • 15
  • 20
  • `For "en" language the output file should containt this:` I assume you meant to say that the given output is for "cs" language? – LarsH Aug 31 '10 at 18:21
  • @LarsH: I updated the question to add some unicode characters there. You're right, there should be written: for "cs" language. Will change it. – dwich Aug 31 '10 at 22:27

3 Answers3

14

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>
Gal Bracha
  • 19,004
  • 11
  • 72
  • 86
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thanks a lot. Can't install lxml on my WinXP, problem with compiler. Will give it a try later. – dwich Aug 29 '10 at 03:01
  • Works! Thanks! You saved my night :) I thank both of you, both solutions are good. – dwich Aug 29 '10 at 03:56
6

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
  • 854,459
  • 170
  • 1,222
  • 1,395
  • Thank you Alex, works great. Except two things - namespace and unicode. If there's an xmlns attribute, for example ``, the new node itself gets an `xmlns:ns0="http://example.org/ns"` attribute and all the child nodes get an ` – dwich Aug 29 '10 at 03:17
  • @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 Aug 29 '10 at 03:40
  • 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 Aug 29 '10 at 03:55
  • @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 Aug 29 '10 at 04:32
3

updating @Alex Martelli's code to remove a bug where the element list is updated in place. Above solution will give wrong answer if the input is little more complex.

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

Code in line 7 for subelement in element: is changed to for subelement in element[:]: as it is incorrect to update list in place while iterating over it.

This code iterates over a copy of element list and removes elements when lang != "en" in the original element list.

bhuvi
  • 66
  • 5