jhscheer
jhscheer

Reputation: 351

How to set/replace values in xml with Python etree lxml xpath?

I want to parse an AndroidManifest.xml with Python2.7 and etree/lxml/xpath to find attribute names and to get and set their values.

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.wearable.timer" >
<uses-sdk android:minSdkVersion="20"
          android:targetSdkVersion="22" />
<application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.DeviceDefault.Light"
        android:allowBackup="true">
</application>
</manifest>

On Bash I used xmlstarlet for this:

xmlstarlet ed --inplace -u '/manifest/application/@android:allowBackup' -v true AndroidManifest.xml

This is what I have so far:

from lxml import etree
NS = {'android' : 'http://schemas.android.com/apk/res/android'}
tree = etree.parse('AndroidManifest.xml')
# get values
print tree.xpath("///@android:allowBackup", namespaces=NS)[0]
print tree.xpath("///@android:minSdkVersion", namespaces=NS)[0]
# set values ?
# write changes back to file ?
print etree.tostring(tree, encoding='utf-8', pretty_print=True)

This prints true, 20 and then the whole unchanged xml document.

Bonus:

Upvotes: 1

Views: 3192

Answers (1)

Padraic Cunningham
Padraic Cunningham

Reputation: 180391

xml = """<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.wearable.timer" >
<uses-sdk android:minSdkVersion="20"
          android:targetSdkVersion="22" />
<application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.DeviceDefault.Light"
        android:allowBackup="true">
</application>
</manifest>"""

To set the values, find the node then access it's attrib dict then set the new value on it's attribute by using QName where the first arg is the namespace URI and the second is the attribute name:

import lxml.etree as et

tree = et.fromstring(xml)
# holds namespace mappings
nsmap = tree.nsmap
# get prefix URI
android = tree.nsmap["android"]

# find app and set the attribute value.
tree.find("application", nsmap).attrib[et.QName(android, "allowBackup")] = "false"
tree.find("uses-sdk", nsmap).attrib[et.QName(android, "minSdkVersion")] = "17"
print(et.tostring(tree, pretty_print=True))

Which gives you:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.wearable.timer">
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="22"/>
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@android:style/Theme.DeviceDefault.Light" android:allowBackup="false">
</application>
</manifest>

To read and write from and to a file, the logic is the same just at the end you write passing in the filename and any other args:

import lxml.etree as et
from StringIO import StringIO

tree = et.parse(StringIO(xml))
root = tree.getroot()
nsmap = root.nsmap
android = nsmap["android"]

tree.find("application", nsmap).attrib[et.QName(android, "allowBackup")] = "false"
tree.find("uses-sdk", nsmap).attrib[et.QName(android, "minSdkVersion")] = "17"

# write the updated html to "new.xml"
tree.write("new.xml", encoding="utf-8")

new.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.wearable.timer">
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="22"/>
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@android:style/Theme.DeviceDefault.Light" android:allowBackup="false">
</application>
</manifest>

You now know how to write the changes, as for missing attributes it works as is, if the value exists we update it, if not we create it:

# no minSDk...
In [31]: !cat  test.xml<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.wearable.timer" >
<uses-sdk  android:targetSdkVersion="22" />
<application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.DeviceDefault.Light"
        android:allowBackup="true">
</application>
</manifest>
In [32]: tree = et.parse("test.xml")

In [33]: root = tree.getroot()

In [34]: nsmap = root.nsmap

In [35]: android = nsmap["android"]

In [36]: tree.find("uses-sdk", nsmap).attrib[et.QName(android, "minSdkVersion")] = "17"

In [37]: tree.write("test.xml", encoding="utf-8")

# New attribute and value created.
In [38]: !cat  test.xml<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.wearable.timer">
<uses-sdk android:targetSdkVersion="22" android:minSdkVersion="17"/>
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@android:style/Theme.DeviceDefault.Light" android:allowBackup="true">
</application>
</manifest>
In [39]: 

Upvotes: 1

Related Questions