Многие ругают xslt за его ресурсоемкость, некрасивость, негибкость, сложность…да много за что еще, наверное, его ругают. Как раз для тех, кто ругает его по последним трем пунктам я и написала данный пост.
Этот пост признан компенсировать пробел в ваших знаниях и представить xslt во всей его красоте.
Недавно передо мной встала задача написать скрипт, который бы из исходного xml получал xml, состоящий только из уникальных элементов. Об исходном файле ничего заранее не известно, абсолютно ничего.
Файлы для экспериментов. Будем считать, что они указывают историю сессий на компьютере в интернет-клубе по логину пользователя.
Исходный:
Результат:
Вот что у меня получилось в итоге, помимо слез радости от проделанной работы:
А теперь поясню все, что я понаделала выше:
1. Первым делом мы подключаем расширения для xslt c exslt.org и назначаем ему пространство имен exsl, чтобы в будущем воспользоваться одной из возможностей данной библиотеки;
2. exclude-result-prefixes=«exsl» – этой строкой мы отключаем префикс из результатов трансформации, дабы не засорять эти самые результаты. Иногда, когда я забываю это сделать, приходится потом долго ломать голову, а почему я получаю на выходе совсем не то, что нужно получить;
3. <xsl:output method=«xml» indent=«yes» encoding=«UTF-8»/> — этой строкой мы говорим xslt процессору, что на выходе хотим получить валидный xml с отступами и с кодировкой UTF-8;
4. <xsl:template match="/"> — заходим в корень нашего таинственного документа;
5. Дальше мы берем наш исходный документ и к каждому элементу дописываем атрибут path, содержащий полный путь до него, начиная от корня и пробираясь все дальше по нашему дереву. Делаем мы это при помощи шаблона с <xsl:template match="*" mode=«WhoMyRelatives»> и записываем результат его работы в переменную var_NodeWithPath. А делаем мы это следующим образом:
6. Получив, наконец-то, нашу var_NodeWithPath переменную мы тут же преобразовываем ее в xml при помощи функции exsl:node-set и работаем уже с этим «файлом» — <xsl:apply-templates select=«exsl:node-set($var_NodeWithPath)/*» mode=«INeedYourTree»/> . Кстати, вот что в ней находится (спонсор вывода переменной – copy-of):
7. После того, как мы поместили нашу переменную в шаблон «INeedYourTree», мы выводим имя корня документа, а потомков пропускаем через шаблон «tree», в котором-то и происходит самое интересное;
8. В шаблоне «tree» мы делаем следующее:
Вот и все. Надеюсь, что кому-то мой опыт принесет пользу. Спасибо за внимание.
UPD:
MikhailEdoshin предложил более элегантное решение данной задачи:
Этот пост признан компенсировать пробел в ваших знаниях и представить xslt во всей его красоте.
Недавно передо мной встала задача написать скрипт, который бы из исходного xml получал xml, состоящий только из уникальных элементов. Об исходном файле ничего заранее не известно, абсолютно ничего.
Файлы для экспериментов. Будем считать, что они указывают историю сессий на компьютере в интернет-клубе по логину пользователя.
Исходный:
- <computer_1>
- <SentimentalSea>
- <forum.tomsk.ru />
- <free-lance.ru />
- <somesite.com>
- <forum.somesite.com>
- <avto />
- <examples />
- <joblist />
- </forum.somesite.com>
- </somesite.com>
- </SentimentalSea>
- <Liloo>
- <forum.tomsk.ru />
- <somesite.com>
- <forum.somesite.com>
- <avto />
- <examples />
- </forum.somesite.com>
- </somesite.com>
- </Liloo>
- <SentimentalSea>
- <forum.tomsk.ru />
- <somesite.com>
- <forum.somesite.com>
- <pets />
- </forum.somesite.com>
- </somesite.com>
- </SentimentalSea>
- <Anonim_1>
- <drom.ru />
- <mamba.ru />
- </Anonim_1>
- <SentimentalSea>
- <forum.tomsk.ru />
- <ozon.ru />
- <torrents.ru />
- </SentimentalSea>
- <Guest>
- <forum.ru />
- <somesite.com>
- <forum.somesite.com>
- <avto />
- <examples />
- </forum.somesite.com>
- </somesite.com>
- </Guest>
- <Anonim_1>
- <exapmle_example.ru />
- </Anonim_1>
- </computer_1>
* This source code was highlighted with Source Code Highlighter.
Результат:
- <?xml version="1.0"?>
- <computer_1>
- <SentimentalSea>
- <forum.tomsk.ru/>
- <free-lance.ru/>
- <somesite.com>
- <forum.somesite.com>
- <avto/>
- <examples/>
- <joblist/>
- <pets/>
- </forum.somesite.com>
- </somesite.com>
- <ozon.ru/>
- <torrents.ru/>
- </SentimentalSea>
- <Liloo>
- <forum.tomsk.ru/>
- <somesite.com>
- <forum.somesite.com>
- <avto/>
- <examples/>
- </forum.somesite.com>
- </somesite.com>
- </Liloo>
- <Anonim_1>
- <drom.ru/>
- <mamba.ru/>
- <exapmle_example.ru/>
- </Anonim_1>
- <Guest>
- <forum.ru/>
- <somesite.com>
- <forum.somesite.com>
- <avto/>
- <examples/>
- </forum.somesite.com>
- </somesite.com>
- </Guest>
- </computer_1>
* This source code was highlighted with Source Code Highlighter.
Вот что у меня получилось в итоге, помимо слез радости от проделанной работы:
- <?xml version="1.0" encoding="UTF-8"?>
- <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
- version="1.0"
- xmlns:exsl="http://exslt.org/common"
- exclude-result-prefixes="exsl">
-
- <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
-
- <xsl:template match="/">
- <xsl:variable name="var_NodeWithPath">
- <xsl:apply-templates select="*" mode="WhoMyRelatives"/>
- </xsl:variable>
- <xsl:apply-templates select="exsl:node-set($var_NodeWithPath)/*" mode="INeedYourTree"/>
- </xsl:template>
-
- <xsl:template match="*" mode="WhoMyRelatives">
- <xsl:param name="parents"/>
- <xsl:element name="{name()}">
- <xsl:attribute name="path">
- <xsl:value-of select="concat($parents, '/', name(.))"/>
- </xsl:attribute>
- <xsl:apply-templates select="child::*" mode="WhoMyRelatives">
- <xsl:with-param name="parents">
- <xsl:choose>
- <xsl:when test="not(boolean($parents))">
- <xsl:value-of select="name(.)"/>
- </xsl:when>
- <xsl:otherwise>
- <xsl:value-of select="concat($parents, '/', name(.))"/>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:with-param>
- </xsl:apply-templates>
- </xsl:element>
- </xsl:template>
-
- <xsl:template match="*" mode="INeedYourTree">
- <xsl:element name="{name()}">
- <xsl:apply-templates select="*" mode="tree"/>
- </xsl:element>
- </xsl:template>
- <xsl:template match="*" mode="tree">
- <xsl:variable name="name" select="name(.)"/>
- <xsl:variable name="path" select="./@path"/>
- <xsl:if test="generate-id(.) = generate-id(ancestor::*//child::*[name() = $name][@path = $path])">
- <xsl:element name="{$name}">
- <xsl:apply-templates select="ancestor::*//child::*[name() = $name][@path = $path]/child::*" mode="tree"/>
- </xsl:element>
- </xsl:if>
- </xsl:template>
- </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.
А теперь поясню все, что я понаделала выше:
1. Первым делом мы подключаем расширения для xslt c exslt.org и назначаем ему пространство имен exsl, чтобы в будущем воспользоваться одной из возможностей данной библиотеки;
2. exclude-result-prefixes=«exsl» – этой строкой мы отключаем префикс из результатов трансформации, дабы не засорять эти самые результаты. Иногда, когда я забываю это сделать, приходится потом долго ломать голову, а почему я получаю на выходе совсем не то, что нужно получить;
3. <xsl:output method=«xml» indent=«yes» encoding=«UTF-8»/> — этой строкой мы говорим xslt процессору, что на выходе хотим получить валидный xml с отступами и с кодировкой UTF-8;
4. <xsl:template match="/"> — заходим в корень нашего таинственного документа;
5. Дальше мы берем наш исходный документ и к каждому элементу дописываем атрибут path, содержащий полный путь до него, начиная от корня и пробираясь все дальше по нашему дереву. Делаем мы это при помощи шаблона с <xsl:template match="*" mode=«WhoMyRelatives»> и записываем результат его работы в переменную var_NodeWithPath. А делаем мы это следующим образом:
- <xsl:apply-templates select="*" mode=«WhoMyRelatives»/> — заходим в корень;
- <xsl:element name="{name()}"> — добавляем в результат имя текущего элемента (не забывайте про фигурные скобочки);
- <xsl:attribute name=«path»><xsl:value-of select=«concat($parents, '/', name(.))»/></xsl:attribute> — и атрибут, содержащий полный путь до него;
- <xsl:apply-templates select=«child::*» mode=«WhoMyRelatives»> — а тут функция вызывает саму себя и обрабатывает уже потомков данного узла, добавляя в переменную parents все новые и новые подробности;
6. Получив, наконец-то, нашу var_NodeWithPath переменную мы тут же преобразовываем ее в xml при помощи функции exsl:node-set и работаем уже с этим «файлом» — <xsl:apply-templates select=«exsl:node-set($var_NodeWithPath)/*» mode=«INeedYourTree»/> . Кстати, вот что в ней находится (спонсор вывода переменной – copy-of):
- <?xml version="1.0" encoding="UTF-8"?>
- <computer_1 path="/computer_1">
- <SentimentalSea path="computer_1/SentimentalSea">
- <forum.tomsk.ru path="computer_1/SentimentalSea/forum.tomsk.ru"/>
- <free-lance.ru path="computer_1/SentimentalSea/free-lance.ru"/>
- <somesite.com path="computer_1/SentimentalSea/somesite.com">
- <forum.somesite.com path="computer_1/SentimentalSea/somesite.com/forum.somesite.com">
- <avto path="computer_1/SentimentalSea/somesite.com/forum.somesite.com/avto"/>
- <examples path="computer_1/SentimentalSea/somesite.com/forum.somesite.com/examples"/>
- <joblist path="computer_1/SentimentalSea/somesite.com/forum.somesite.com/joblist"/>
- </forum.somesite.com>
- </somesite.com>
- </SentimentalSea>
- <Liloo path="computer_1/Liloo">
- <forum.tomsk.ru path="computer_1/Liloo/forum.tomsk.ru"/>
- <somesite.com path="computer_1/Liloo/somesite.com">
- <forum.somesite.com path="computer_1/Liloo/somesite.com/forum.somesite.com">
- <avto path="computer_1/Liloo/somesite.com/forum.somesite.com/avto"/>
- <examples path="computer_1/Liloo/somesite.com/forum.somesite.com/examples"/>
- </forum.somesite.com>
- </somesite.com>
- </Liloo>
- <SentimentalSea path="computer_1/SentimentalSea">
- <forum.tomsk.ru path="computer_1/SentimentalSea/forum.tomsk.ru"/>
- <somesite.com path="computer_1/SentimentalSea/somesite.com">
- <forum.somesite.com path="computer_1/SentimentalSea/somesite.com/forum.somesite.com">
- <pets path="computer_1/SentimentalSea/somesite.com/forum.somesite.com/pets"/>
- </forum.somesite.com>
- </somesite.com>
- </SentimentalSea>
- <Anonim_1 path="computer_1/Anonim_1">
- <drom.ru path="computer_1/Anonim_1/drom.ru"/>
- <mamba.ru path="computer_1/Anonim_1/mamba.ru"/>
- </Anonim_1>
- <SentimentalSea path="computer_1/SentimentalSea">
- <forum.tomsk.ru path="computer_1/SentimentalSea/forum.tomsk.ru"/>
- <ozon.ru path="computer_1/SentimentalSea/ozon.ru"/>
- <torrents.ru path="computer_1/SentimentalSea/torrents.ru"/>
- </SentimentalSea>
- <Guest path="computer_1/Guest">
- <forum.ru path="computer_1/Guest/forum.ru"/>
- <somesite.com path="computer_1/Guest/somesite.com">
- <forum.somesite.com path="computer_1/Guest/somesite.com/forum.somesite.com">
- <avto path="computer_1/Guest/somesite.com/forum.somesite.com/avto"/>
- <examples path="computer_1/Guest/somesite.com/forum.somesite.com/examples"/>
- </forum.somesite.com>
- </somesite.com>
- </Guest>
- <Anonim_1 path="computer_1/Anonim_1">
- <exapmle_example.ru path="computer_1/Anonim_1/exapmle_example.ru"/>
- </Anonim_1>
- </computer_1>
* This source code was highlighted with Source Code Highlighter.
7. После того, как мы поместили нашу переменную в шаблон «INeedYourTree», мы выводим имя корня документа, а потомков пропускаем через шаблон «tree», в котором-то и происходит самое интересное;
8. В шаблоне «tree» мы делаем следующее:
- <xsl:variable name=«name» select=«name(.)»/> — запоминаем имя текущего элемента;
- <xsl:variable name=«path» select="./@path"/> — запоминаем его xPath;
- <xsl:if test=«generate-id(.) = generate-id(ancestor::*//child::*[name() = $name][@path = $path])»> — если текущий элемент первый в своем роде (первый из всех с таким же значением атрибута path), то…
- <xsl:element name="{$name}"> <xsl:apply-templates select=«ancestor::*//child::*[name() = $name][@path = $path]/child::*» mode=«tree»/> — выводим этот элемент и собираем всех потомков текущего узла и узлов с такой же историей.
Вот и все. Надеюсь, что кому-то мой опыт принесет пользу. Спасибо за внимание.
UPD:
MikhailEdoshin предложил более элегантное решение данной задачи:
- <?xml version="1.0" encoding="UTF-8"?>
- <xsl:stylesheet version="1.0"
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
-
- <xsl:template match="/">
- <!-- Initialize -->
- <xsl:apply-templates select="*[1]">
- <xsl:with-param name="found-set" select="*" />
- </xsl:apply-templates>
- </xsl:template>
-
- <xsl:template match="*">
- <xsl:param name="found-set" />
- <xsl:copy>
- <!-- Process children of all found elements with same names. -->
- <xsl:variable name="children" select="$found-set[name() = name(current())]/*" />
- <xsl:apply-templates select="$children[1]">
- <xsl:with-param name="found-set" select="$children" />
- </xsl:apply-templates>
- </xsl:copy>
- <!-- Exclude all siblings with the same name and continue -->
- <xsl:variable name="remaining-siblings" select="$found-set[name() != name(current())]" />
- <xsl:apply-templates select="$remaining-siblings[1]">
- <xsl:with-param name="found-set" select="$remaining-siblings" />
- </xsl:apply-templates>
- </xsl:template>
-
- </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.