Registering Namespace in Python XPath query

Viewed 73

This is the XML document that I have:

<products xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Product Id="1">
      <Product Id="1_1">
        <Attribute Name="Whatever"></Attribute>
      </Product>
      <Attributes xmlns="http://some/path/to/entity/def">
        <Attribute Name="Identifier">NumberOne</Attribute>
      </Attributes>
  </Product>
  <Product Id="2">
    <Attributes xmlns="http://some/path/to/entity/def">
      <Attribute Name="Identifier">NumberTwo</Attribute>
    </Attributes>
  </Product>
</products>

I'm trying to use XPath for getting a Product by its child Attributes.Attribute[Name=Identifier] value (e.g. "NumberOne"). So in that case my expected result would be:

<Product Id="1">
      <Product Id="1_1">
        <Attribute Name="Whatever"></Attribute>
      </Product>
      <Attributes xmlns="http://some/path/to/entity/def">
        <Attribute Name="Identifier">NumberOne</Attribute>
      </Attributes>
</Product>

Based on this explanation, I tried to implement the query in Python by using the lxml lib:

found_products = xml_tree_from_string.xpath('//products//Product[c:Attributes[Attribute[@Name="Identifier" and text()="NumberOne"]]]', namespaces={"c": "http://some/path/to/entity/def"})

Unfortunately, this never returns a result due to the Attributes namespace definition.

What am I missing?

2 Answers

What am I missing?

You're missing that Attribute is also in the same namespace as Attributes because default namespace declarations are inherited by descendent XML elements.

So, just add a c: to Attribute in your XPath, and it should work as you observed in your comment to Jack's answer.

You need to first define a namespace map, declare a prefix for those namespaces that don't have one (as is the case here) and then apply xpath:

from lxml import etree
prods ="""[your xml above]"""
ns = { (k if k else "xx"):(v) for k, v in doc.xpath('//namespace::*') } #create ns map
doc = etree.XML(prods)
for product in doc.xpath('//products//Product[.//xx:Attribute[@Name="Identifier"][text()="NumberOne"]]', namespaces=ns):
    print(etree.tostring(product).decode())

Output:

<Product xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Id="1">
      <Product Id="1_1">
        <Attribute Name="Whatever"/>
      </Product>
      <Attributes xmlns="http://some/path/to/entity/def">
        <Attribute Name="Identifier">NumberOne</Attribute>
      </Attributes>
  </Product>

To suppress the namespaces attributes, change the for loop to:

for product in doc.xpath('//products//Product[.//xx:Attribute[@Name="Identifier"][text()="NumberOne"]]', namespaces=ns):
    etree.cleanup_namespaces(doc) #note: the parameter is "doc", not "product"
    print(etree.tostring(product).decode())

Output:

<Product Id="1">
      <Product Id="1_1">
        <Attribute Name="Whatever"/>
      </Product>
      <Attributes xmlns="http://some/path/to/entity/def">
        <Attribute Name="Identifier">NumberOne</Attribute>
      </Attributes>
  </Product>
Related