How to substitute predicate value by a variable using LXML find() with Python 3.6

Viewed 379

I am new to Python coding. I am able to create the output XML file. I want to use a variable which holds a string value and pass it to 'predicate' of 'find()'. Is this achievable? How to make this work?

I am using LXML package with Python 3.6. Below is my code. Area of problem is commented at the end of the code.

import lxml.etree as ET

# Create root element
root = ET.Element("Base", attrib={'Name': 'My Base Node'})
# Create first child element
FirstElement = ET.SubElement(root, "FirstNode", attrib={'Name': 'My First Node', 'Comment':'Hello'})

# Create second child element
SecondElement = ET.SubElement(FirstElement, "SecondNode", attrib={'Name': 'My Second Node', 'Comment': 'World'})

# Create XML file
XML_data_as_string = ET.tostring(root, encoding='utf8')
with open("TestFile.xml", "wb") as f:
    f.write(XML_data_as_string)

# Variable to substitute in second portion of predicate
NewValue = "My Second Node"

# #### AREA OF PROBLEM ###
# Question. How to pass variable 'NewValue' in the predicate?

# Gives "SyntaxError: invalid predicate"
x = root.find("./FirstNode/SecondNode[@Name={subs}]".format(subs=NewValue))

# I commented above line and reexecuted the code with this below line 
# enabled. It gave "ValueError: empty namespace prefix must be passed as None, 
# not the empty string"
x = root.find("./FirstNode/SecondNode[@Name=%s]", NewValue) 
2 Answers

As Daniel Haley said - you're missing a single quotes in @Name={subs}.

The following line works for me:

x = root.find("./FirstNode/SecondNode[@Name='{subs}']".format(subs=NewValue))

Since you use Python 3.6, you can utilize f-strings:

x = root.find(f"./FirstNode/SecondNode[@Name='{NewValue}']")

The "proper" way to solve this would be to use XPath variables, which are not supported by find() (and consequently, aren't supported by xml.etree from the standard library either) but are supported by xpath().

NewValue = "AJL's Second Node" # Uh oh, that apostrophe is going to break something!!
x_list = root.xpath("./FirstNode/SecondNode[@Name=$subs]", subs=NewValue)
x = x_list[0]

This avoids any sort of issue you might otherwise run into with quoting and escaping.


The main caveat of this method is namespace support, since it doesn't use the bracket syntax of find.

x = root.find("./{foobar.xsd}FirstNode")
# Brackets are doubled to avoid conflicting with `.format()`
x = root.find("./{{foobar.xsd}}FirstNode/SecondNode[@Name='{subs}']".format(subs=NewValue))

Instead, you must specify those in a separate dict:

ns_list = {'hello':'foobar.xsd'}
x_list = root.xpath("./hello:FirstNode/SecondNode[@Name=$subs]", namespaces=ns_list , subs=NewValue)
x = x_list[0]
Related