XML в CSV с XSLT - Группировка узлов
Итак, у меня есть XML-файл, сгенерированный из ответа php curl, который затем преобразуется в CSV, так что каждый элемент mods ниже представляет собой одну строку. У меня есть несколько CSV с использованием таблицы стилей в проверенном ответе здесь, но это не совсем то, что я пытаюсь сделать.
Мой XML (упрощенный):
<xml>
<mods xmlns="http://www.loc.gov/mods/">
<typeOfResource>StillImage</typeOfResource>
<titleInfo ID="T-1">
<title>East Bay Street</title>
</titleInfo>
<subject ID="SBJ-2">
<topic>Railroads</topic>
</subject>
<subject ID="SBJ-3">
<geographic>Low Country</geographic>
</subject>
<subject ID="SBJ-4">
<geographic>Charleston (S.C.)</geographic>
</subject>
<subject ID="SBJ-7">
<hierarchicalGeographic>
<county>Charleston County (S.C.)</county>
</hierarchicalGeographic>
</subject>
<physicalDescription>
<form>Images</form>
</physicalDescription>
<note>Caption: 'War Views. No.179. Ruins of the Northeastern Railway Depot, Charleston.' This is a stereograph image which measures 3 1/2" X 7". Date assumed to be 1865.</note>
<originInfo>
<dateCreated>1865</dateCreated>
</originInfo>
<location>
<physicalLocation>The Charleston Museum Archives</physicalLocation>
</location>
<relatedItem type="host">
<titleInfo>
<title>Charleston Museum Civil War Photographs</title>
</titleInfo>
</relatedItem>
</mods>
<mods>
more nodes...
</mods>
</xml>
Мой текущий XSL из стека пост выше:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*" />
<xsl:template match="/*/child::*">
<xsl:for-each select="child::*">
<xsl:if test="position() != last()"><xsl:value-of select="normalize-space(.)"/>, </xsl:if>
<xsl:if test="position() = last()"><xsl:value-of select="normalize-space(.)"/> <xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Это выдает CSV, где каждый элемент MODS представляет собой одну строку, а каждый дочерний элемент представляет собой значение в этой строке через запятую. Можно ли изменить XSL так, чтобы каждый элемент MODS представлял собой одну строку, но значения совпадающих дочерних элементов группировались? Что-то вроде:
StillImage,East Bay Street,Railroads,**Low County;Charleston (S.C.)**,Charleston County (S.C.), Images
.......и так далее.
Итак, когда узлы (например, несколько объектов -> географические записи) совпадают, они группируются и разделяются точкой с запятой, а не принимают несколько значений, разделенных запятыми? Надеюсь, у меня есть смысл. Спасибо!
1 ответ
Один из способов сделать это - сначала изменить свой XSLT, чтобы выбрать только те элементы, которые не имеют предшествующего брата с тем же дочерним именем (т.е. выбрать элементы, которые являются "первыми" в каждой группе)
<xsl:for-each select="*[name(*) != name(preceding-sibling::*[1]/*)]">
Затем вы можете определить переменную, чтобы получить следующий брат, если (и только если) он имеет то же имя, так что вы можете проверить, действительно ли текущий элемент находится в группе более 1.
<xsl:variable name="nextWithSameName"
select="following-sibling::*[1][name(*)=name(current()/*)]"/>
<xsl:if test="$nextWithSameName">**</xsl:if>
(Я не уверен, что вы действительно хотели ** в окончательных результатах, или они просто предназначены для выделения группы! Я держу их в своем примере, но, очевидно, будет достаточно легко удалить соответствующие строки кода).
Чтобы сгруппировать следующих братьев и сестер с одинаковыми именами, вы можете вызвать рекурсивный шаблон для первого следующего брата
<xsl:apply-templates select="$nextWithSameName" mode="group"/>
Затем в этом шаблоне вы будете рекурсивно вызывать его, если имя ближайшего следующего брата совпадает
<xsl:template match="*" mode="group">
<xsl:text>;</xsl:text>
<xsl:value-of select="normalize-space(.)"/>
<xsl:apply-templates select="following-sibling::*[1][name(*)=name(current()/*)]" />
</xsl:template>
Попробуйте следующее XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/*/*">
<xsl:for-each select="*[name(*) != name(preceding-sibling::*[1]/*)]">
<xsl:variable name="nextWithSameName" select="following-sibling::*[1][name(*)=name(current()/*)]"/>
<xsl:if test="position() > 1">, </xsl:if>
<xsl:if test="$nextWithSameName">**</xsl:if>
<xsl:value-of select="normalize-space(.)"/>
<xsl:apply-templates select="$nextWithSameName" mode="group"/>
<xsl:if test="$nextWithSameName">**</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="*" mode="group">
<xsl:text>;</xsl:text>
<xsl:value-of select="normalize-space(.)"/>
<xsl:apply-templates select="following-sibling::*[1][name(*)=name(current()/*)]" />
</xsl:template>
</xsl:stylesheet>
Теперь, если бы вы могли использовать XSLT 2.0, все стало намного, намного проще, так как вы могли бы использовать конструкцию xsl:for-each-group, которая, помимо прочего, поставляется с операцией для "соседнего с группой". И вы также можете покончить с рекурсивным шаблоном, используя улучшенный xsl:value-of, который будет иметь свойство "разделитель" для использования при выборе нескольких элементов.
Для XSLT 2.0 также должно работать следующее
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/*/*">
<xsl:for-each-group select="*" group-adjacent="name(*)">
<xsl:if test="position() > 1">, </xsl:if>
<xsl:if test="current-group()[2]">**</xsl:if>
<xsl:value-of select="current-group()" separator=";" />
<xsl:if test="current-group()[2]">**</xsl:if>
</xsl:for-each-group >
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>