Lister
XSLT
XSLT
Børre Stenseth

Unike og inverterte lister

Hva

Vi har av og til behov for å finne unike lister av elementer eller attributter i en XML-fil. Vi skal analysere et enkelt eksempel i to forskjellige varianter og introdusere en mekanisme for å lage slike lister. Endelig skal vi bruke dataene på en enkel, AJAX-drevet, vevside.

Eksempel 1

Anta følgende XML-fil som viser hvilke land en rekke personer (tourist) har besøkt:

<?xml version="1.0" encoding="UTF-8"?>
<travellers>
    <tourist>
        <first-name>Jens</first-name>
        <last-name>Pedersen</last-name>
        <country>Sveits</country>
    </tourist>
    <tourist>
        <first-name>Ole</first-name>
        <last-name>Tangen</last-name>
        <country>Sveits</country>
        <country>Tyskland</country>
    </tourist>
    <tourist>
        <first-name>Marit</first-name>
        <last-name>Syversen</last-name>
        <country>Italia</country>
        <country>Tyskland</country>
    </tourist>
    <tourist>
        <first-name>Gro</first-name>
        <last-name>Borgen</last-name>
        <country>Italia</country>
        <country>Tyskland</country>
        <country>Tyrkia</country>
    </tourist>
</travellers>

Som vi ser inngår et land i flere tourist-elementer. Problemstillingen vi reiser er hvilke land inngår i fila og videre hvordan kan vi sortere dataene på land og vise hvem som har besøkt landet.

Vi prøver oss med følgende transformasjon:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output  method="xml"  indent="yes" encoding="utf-8"/>
<xsl:key name="countrynames" match="country" use="."/>
<xsl:template match="/">
<root>
<xsl:for-each select="//country[generate-id() = generate-id(key('countrynames',.)[1])]">
<xsl:sort order="ascending"/>
 <xsl:variable name="theCountry" select="."/>
 <xsl:element name="country">
     <xsl:attribute name="name"><xsl:value-of select="$theCountry"/></xsl:attribute>
     <xsl:apply-templates select="//tourist[country=$theCountry]"/>
</xsl:element>
</xsl:for-each>
</root>
</xsl:template>
<xsl:template match="//tourist">
<visitor>
 <xsl:value-of select="last-name"/>,<xsl:value-of select="first-name"/>
</visitor>
</xsl:template>
</xsl:stylesheet>

Det nye i dette er bruken av linjene:

<xsl:key name="countrynames" match="country" use="."/>
...
<xsl:for-each select="//country[generate-id() = generate-id(key('countrynames',.)[1])]">

Vi definerer og genererer et nøkkelset countrynames. Det vi egentlig gjør er å lage en hashtabell, for så å løpe gjennom nøklene i en løkke.

Resultatet blir slik:

<?xml version="1.0" encoding="utf-8"?>
<root>
    <country name="Italia">
        <visitor>Syversen,Marit</visitor>
        <visitor>Borgen,Gro</visitor>
    </country>
    <country name="Sveits">
        <visitor>Pedersen,Jens</visitor>
        <visitor>Tangen,Ole</visitor>
    </country>
    <country name="Tyrkia">
        <visitor>Borgen,Gro</visitor>
    </country>
    <country name="Tyskland">
        <visitor>Tangen,Ole</visitor>
        <visitor>Syversen,Marit</visitor>
        <visitor>Borgen,Gro</visitor>
    </country>
</root>

Eksempel 2

Vi bygger ut XML-fila slik at den inneholder rapporter fra turistenes besøk i landet. Vi gjør dette bare med enkel tekst. Vi kunnet godt ha mer kompliserte rapportformater. Merk også at landets navn nå er en attributt til elementet country:

_turister2.xml

Vi skriver om transformasjonen. Forskjellen er nå at landets navn opptrer som attributt, ikke elementverdi.

_transtur2.xml

og får:

_output2.xml

En webløsning

Vi ønsker å etablere en vevside der vi kan lete etter reiserapporter for et land. Siden er bygget opp svært enkelt med en dropdownliste (select element) for å velge land og et div-element der vi viser alle rapportene for dette landet. Det er to felter som skal fylles inn: selectelementet og rapportelementet. Dette ønsker vi å gjøre ved hjelp av et Pythonskript som via lxml [1] transformerer XML-fila slik at vi får ut de to ønskede fragmentene i HTML-format.

Selve HTML-fila er slik:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>Turister</title>
    <script src="https://borres.hiof.no/resources/jslibs/jQ/jquery-min.js" type="text/javascript"> 
    </script>
    <script type="text/javascript">
    // filling the report element
    function showChoice(val){
      $.ajax({
        url:'content.py',
        data:'country='+val,
        success:function(data)
        {
          $('#report').html(data);
        },
        error:function(data)
        {
          $('#report').html('error');
        }
        });
      }
    // filling the select element
    function initSelect(){
      $.ajax({
        url:'basicpage.py',
        success:function(data)
        {
          $('#select').html(data);
        },
        error:function(data)
        {
          $('#select').html('error');
        }
        });
      }
    </script>
  </head>
  <body onload="initSelect()">
    <h1>Rapport fra utsendte i </h1>
    <div id="select">
    <!-- select goes here -->
    </div>
    <div id="report">
    <!-- report goes here -->
    </div>
  </body>
</html>

Her er valgt å lage et pythonskript for hver av de to behovene: basipage.py som fyller opp select elementet og content.py som fyller opp reiserapportene for det aktuelle landet. De to scriptene er svært enkle og de betjener seg begge av en felles modul utils.py.

basicpage.py

#! /usr/bin/python
import cgi
import utils
T=utils.produce('turister2.xml','basicpage.xsl',None)
print 'Content-type: text/html; charset=utf-8\n'
print T
    

content.py

#! /usr/bin/python
import cgi
import utils
form=cgi.FieldStorage()
# expect to find country
# default value:
country='Tyrkia'
if form.has_key('country'):
    country=form['country'].value
T=utils.produce('turister2.xml','content.xsl',country)
print 'Content-type: text/html; charset=utf-8\n'
print T

utils.py

from lxml import etree
"""
Transform xmlfile with xsltfile
country is used as parameter if != None
"""
def produce(xmlfile,xsltfile,country):
    tree=etree.parse(xmlfile)
    xsltree=etree.parse(xsltfile)
    transform=etree.XSLT(xsltree)
    if country==None:
        resulttree=transform(tree)
    else:
        c=etree.XSLT.strparam(country)
        resulttree=transform(tree,selectedCountry=c)
    return str(resulttree)

if __name__=="__main__":
    # testing
    #T=produce('turister2.xml','basicpage.xsl',None)
    T=produce('turister2.xml','content.xsl','Italia')
    print T

De to aktuelle transformasjonene har samme navn som Pythonskriptet som bruker dem. Transformasjonen som gir select elementet, basicpage.xsl:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" 
 indent="yes"
 omit-xml-declaration="yes"/>
 
 <xsl:key name="countrynames" match="country" use="@name"/>
 
<xsl:template match="/">
<xsl:element name="select">
  <xsl:attribute name="name">country</xsl:attribute>
  <xsl:attribute name="onchange">showChoice(this.value);</xsl:attribute>
  <xsl:for-each select="//country[generate-id()=generate-id(key('countrynames',@name))]">
    <xsl:sort select="@name" order="ascending"/>
    <xsl:variable name="theCountry" select="@name"/>
    <xsl:element name="option">
    <xsl:attribute name="value"><xsl:value-of select="$theCountry"/></xsl:attribute>
    <xsl:value-of select="$theCountry"/>
    </xsl:element>
  </xsl:for-each>
</xsl:element> 
 </xsl:template>
</xsl:stylesheet>

Transformasjonen som gir rapportene, content.xsl:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="yes" omit-xml-declaration="yes"/>
 
 <xsl:param name="selectedCountry" select="'Tyskland'"/>
 
 <xsl:key name="countrynames" match="country" use="@name"/>
 
 <xsl:template match="/">
<div>
<xsl:for-each select="//country[generate-id()=generate-id(key('countrynames',@name))]"> 
<xsl:if test="$selectedCountry=@name">
 <xsl:variable name="theCountry" select="@name"/>    
<xsl:apply-templates select="//country[@name=$theCountry]"/>
</xsl:if>
</xsl:for-each>
</div>
</xsl:template>
<xsl:template match="//country">
<div class="onevisitor">
<h3>
 <xsl:value-of select="ancestor::tourist/last-name"/>
</h3>
<p style="margin-left:20px">
    <xsl:value-of select="."/>
</p>
</div>
</xsl:template>
</xsl:stylesheet>
Du kan teste her https://borres.hiof.no/wep/xslt/list/rapport.html
Referanser
  1. lxml - XML and HTML with Python lxml.de/ 03-03-2014