DaveF
DaveF

Reputation: 173

xpath remove nodes based on child's attribute value

I have this xml code:

<osm>
  <count>
    <tag k="nodes" v="608"/>   
  </count>
  <node>
    <tag k="addr:housenumber" v="5-10"/>
    <tag k="addr:postcode" v="BA1 2BX"/>
  </node>
  <way>
    <tag k="amenity" v="nightclub"/>
    <tag k="facebook" v="https://www.facebook.com"/>
  </way>
  <node>
    <tag k="amenity" v="cafe"/>
    <tag k="addr:postcode" v="BS3 2BX"/> 
  </node>
  <node>
    <tag k="amenity" v="school"/>
    <tag k="facebook" v="https://www.facebook.com"/>
  </node>
  <way>
    <tag k="amenity" v="church"/>
    <tag k="addr:postcode" v="BAX 5RT"/>   
  </way>
</osm>

I need to remove all nodes below <osm> which contain a <tag> node with the attribute k with the value 'addr:postcode':

<osm>
  <count>
    <tag k="nodes" v="608"/>   
  </count>
  <way>
    <tag k="amenity" v="nightclub"/>
    <tag k="facebook" v="https://www.facebook.com"/>
  </way>
  <node>
    <tag k="amenity" v="school"/>
    <tag k="facebook" v="https://www.facebook.com"/>
  </node>
</osm>

I've tried many permutations around this query, but it returns every occurrence of <tag>:

<xsl:apply-templates select="osm/*/tag[not(@k='addr:postcode')]"> 

I assume I need to find the parent, but that's where my knowledge runs out.

Upvotes: 1

Views: 1775

Answers (2)

Vitaliy Moskalyuk
Vitaliy Moskalyuk

Reputation: 2583

Here is xpath to select all nodes under osm that doesn't include specific tag:

//osm/*[not(.//tag[@k='addr:postcode'])]

Here is some important note for not() placement:

to search for element that doesn't include child element (any levels below) use pattern:

//element[not(.//child)]

please note that locator:

    //element[.//child[not(@id=1)]]

will select all elements that includes at least 1 child with id != 1.

So in example:

    <row class="row1">
    <cell id=1/>
    <cell id=2/>
    <cell id=3/>
    </row>
    <row class="row2">
    <cell id=1/>
    <cell id=2/>
    </row>
    <row class="row3">
    <cell id=1/>
    <cell id=2/>
    </row>
    <row class="row4">
    <cell id=3/>
    </row>
     //row[not(.//cell[@id=3])]  

will select rows 2 and 3 (rows that doesn't include cell with id=3)

    //row[.//cell[not(@id=3)]] 

will select rows 1, 2 and 3, but NOT 4 (rows that has at least one cell with id other than 3)

So it's important where you place not() statement, as it could totally change XPath logic.

Upvotes: 1

Daniel Haley
Daniel Haley

Reputation: 52858

You're right; you need to select the parent of tag. To do that, just select * and put the tag in the predicate.

Example...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="osm">
    <xsl:copy>
      <xsl:apply-templates select="*[not(tag/@k='addr:postcode')]"/>      
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Working demo here: http://xsltransform.net/pNmBy23

Alternatively, you can add a matching template instead...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*[tag/@k='addr:postcode']"/>

</xsl:stylesheet>

This will produce the same output. (http://xsltransform.net/pNmBy23/1)

Upvotes: 3

Related Questions