Сложный XML в TSV с использованием XSLT

Я нашел пару предыдущих вопросов, которые касаются частей моей проблемы (см. Здесь и здесь, но у меня возникают проблемы с их интеграцией. У меня есть набор записей XML, которые я хочу преобразовать в формат с разделителями табуляции. Однако не все записи XML имеют все поля, а некоторые содержат несколько экземпляров поля.

Два примера XML-записей:

<?xml version="1.0" encoding="UTF-8" ?>
<marc:collection xmlns:marc="http://www.loc.gov/MARC21/slim"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd">
    <marc:record>
        <marc:leader>02179 am a  002893u     </marc:leader>
        <marc:controlfield tag="001">12789</marc:controlfield>
        <marc:controlfield tag="005">20120521</marc:controlfield>
        <marc:controlfield tag="007">cuuuu---auuuu</marc:controlfield>
        <marc:controlfield tag="008">120521s||||    xx      o     0   u ||| |</marc:controlfield>
        <marc:datafield tag="020" ind1=" " ind2=" ">
            <marc:subfield code="a">9789089640574</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="100" ind1="1" ind2=" ">
            <marc:subfield code="a">Rooij van ,Robert</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="245" ind1="1" ind2=" ">
            <marc:subfield code="a">New Perspectives on Games and Interaction</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="260" ind1=" " ind2=" ">
            <marc:subfield code="b">Amsterdam University Press</marc:subfield>
            <marc:subfield code="c">2008</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="300" ind1=" " ind2=" ">
            <marc:subfield code="a">1 electronic resource (330 p.)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="520" ind1=" " ind2=" ">
            <marc:subfield code="a">This volume is a collection of papers ...</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Mathematics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Philosophy (General)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Economic theory. Demography</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Economics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Philosophy</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Mathematics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Economie</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Filosofie</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Wiskunde</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Apt ,Krzysztof</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="856" ind1="4" ind2="0">
            <marc:subfield code="u">http://www.doabooks.org/doab?func=fulltext&amp;rid=12789</marc:subfield>
            <marc:subfield code="z">Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial (CC by-nc)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="856" ind1="4" ind2="0">
            <marc:subfield code="u">http://www.oapen.org/download?type=document&amp;docid=340074</marc:subfield>
        </marc:datafield>
    </marc:record>
    <marc:record>
        <marc:leader>01452 am a  001933u     </marc:leader>
        <marc:controlfield tag="001">15497</marc:controlfield>
        <marc:controlfield tag="005">20140217</marc:controlfield>
        <marc:controlfield tag="007">cuuuu---auuuu</marc:controlfield>
        <marc:controlfield tag="008">140217s||||    xx      o     0   u ||| |</marc:controlfield>
        <marc:datafield tag="020" ind1=" " ind2=" ">
            <marc:subfield code="a">9788867050673</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="100" ind1="1" ind2=" ">
            <marc:subfield code="a">Emanuele Haus</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="245" ind1="1" ind2=" ">
            <marc:subfield code="a">Dynamics of an elastic satellite with internal friction.</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="260" ind1=" " ind2=" ">
            <marc:subfield code="b">Ledizioni - LediPublishing</marc:subfield>
            <marc:subfield code="c">2013</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="300" ind1=" " ind2=" ">
            <marc:subfield code="a">1 electronic resource ( p.)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="520" ind1=" " ind2=" ">
            <marc:subfield code="a">n this thesis, we study the dynamics...</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="546" ind1=" " ind2=" ">
            <marc:subfield code="a">english</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Mathematics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="856" ind1="4" ind2="0">
            <marc:subfield code="u">http://www.doabooks.org/doab?func=fulltext&amp;rid=15497</marc:subfield>
            <marc:subfield code="z">Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial Share Alike (CC by-nc-sa)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="856" ind1="4" ind2="0">
            <marc:subfield code="u">http://www.ledizioni.it/stag/wp-content/uploads/2014/02/tesi_haus.pdf</marc:subfield>
        </marc:datafield>
    </marc:record>
</marc:collection>

Я пытался адаптировать XSLT из этого предыдущего ответа, но пока без особой удачи:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xpath-default-namespace="http://www.loc.gov/MARC21/slim">
    <xsl:output method="text"/>
    <xsl:variable name="delimiter" select="'&#09;'"/>

    <xsl:strip-space elements="*"/>
    <xsl:output method="text"/>

    <xsl:key name="field" 
      match="/collection/record/datafield/subfield" 
      use="concat(../@tag,@code)"/>

    <!-- variable containing the first occurrence of each field -->
    <xsl:variable name="allFields"
        select="/collection/record/datafield/subfield
                [generate-id()
                 =generate-id(key('field', 
                                   concat(../@tag,@code))[1])]" />

    <xsl:template match="/">

        <xsl:for-each select="$allFields">
            <xsl:sort select="substring(concat(../@tag,@code),1,3)"
                      data-type="number"/>
            <xsl:value-of select="concat(../@tag,@code)" />
            <xsl:if test="position() &lt; last()">
                <xsl:value-of select="$delimiter" />
            </xsl:if>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
        <xsl:apply-templates select="*/*" />
    </xsl:template>

    <xsl:template match="*">
        <xsl:variable name="this" select="." />

        <xsl:for-each select="$allFields">
            <xsl:sort 
              select="substring(concat(../@tag,@code),1,3)" 
              data-type="number"/>
            <xsl:value-of 
              select="$this/*[@code = current()/@code]" />
            <xsl:if test="position() &lt; last()">
                <xsl:value-of select="$delimiter" />
            </xsl:if>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
    </xsl:template>
</xsl:stylesheet>

В выводе, который я пытаюсь достичь, заголовок будет состоять из leader с последующими уникальными значениями @tag (соединено с subfield/@code для подполей), отсортированные в порядке возрастания tag:

leader  001 005 007 008 020a    100a    1004    245a    260b    260c    300a    520a    546a    650a    653a    700a    7004    856u    856z

Если запись имеет несколько значений для одного field/subfield сочетание, я хочу объединить их вместе, например:

653a
Economics|Philosophy|Mathematics

Однако, если в записи отсутствует определенное поле, я хочу просто вывести символ табуляции, чтобы все выровнялось.

Полная выборка TSV:

leader  001 005 007 008 020a    100a    1004    245a    260b    260c    300a    520a    546a    650a    653a    700a    7004    856u    856z                                        
02179 am a  002893u         12789   20120521    cuuuu---auuuu   120521s||||    xx      o     0   u ||| |    9789089640574   Rooij van ,Robert   aut New Perspectives on Games and Interaction   Amsterdam University Press  2008    1 electronic resource (330 p.)  This volume is a collection of papers       Mathematics|Philosophy (General)|Economic theory. Demography    Economics|Philosophy|Mathematics|Economie|Filosofie|Wiskunde    Apt ,Krzysztof< aut http://www.doabooks.org/doab?func=fulltext&amp;rid=12789|http://www.oapen.org/download?type=document&amp;docid=340074   Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial (CC by-nc)                                       
01452 am a  001933u         15497   20140217    cuuuu---auuuu   140217s||||    xx      o     0   u ||| |    9788867050673   Emanuele Haus   aut Dynamics of an elastic satellite with internal friction.    Ledizioni - LediPublishing  2013    1 electronic resource ( p.) In this thesis, we study the dynamics of an elastic body    english Mathematics             http://www.doabooks.org/doab?func=fulltext&amp;rid=15497|http://www.ledizioni.it/stag/wp-content/uploads/2014/02/tesi_haus.pdf  Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial Share Alike (CC by-nc-sa)                                        

3 ответа

Решение

Я бы посоветовал вам попробовать это так:

XSLT 2.0

<xsl:stylesheet version="2.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:marc="http://www.loc.gov/MARC21/slim"
exclude-result-prefixes="marc">
<xsl:output method="text" encoding="UTF-8"/>

<xsl:variable name="fields">
    <xsl:for-each-group select="/marc:collection/marc:record/marc:datafield" group-by="@tag">
        <xsl:sort select="@tag"/>
            <xsl:for-each select="marc:subfield">
                <xsl:sort/>
                <field tag="{current-grouping-key()}" code="{@code}">a</field>
            </xsl:for-each>
    </xsl:for-each-group>
</xsl:variable>

<xsl:template match="/">
    <!-- header -->
    <xsl:for-each select="$fields/field">
        <xsl:value-of select="@tag"/>
        <xsl:value-of select="@code"/>
        <xsl:if test="position()!=last()">
            <xsl:text>&#9;</xsl:text>
        </xsl:if>
    </xsl:for-each>
    <xsl:text>&#10;</xsl:text>
    <!-- data -->
    <xsl:for-each select="marc:collection/marc:record">
        <xsl:variable name="current-record" select="." />
        <xsl:for-each select="$fields/field">
            <xsl:value-of select="$current-record/marc:datafield[@tag=current()/@tag]/marc:subfield[@code=current()/@code]" separator="|"/>
            <xsl:if test="position()!=last()">
                <xsl:text>&#9;</xsl:text>
            </xsl:if>
        </xsl:for-each>
        <xsl:if test="position()!=last()">
            <xsl:text>&#10;</xsl:text>
        </xsl:if>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Результат при применении к вашему примеру ввода:

020a    100a    1004    245a    260c    260b    300a    520a    546a    650a    653a    700a    7004    856z    856u
9789089640574   Rooij van ,Robert   aut New Perspectives on Games and Interaction   2008    Amsterdam University Press  1 electronic resource (330 p.)  This volume is a collection of papers ...       Mathematics|Philosophy (General)|Economic theory. Demography    Economics|Philosophy|Mathematics|Economie|Filosofie|Wiskunde    Apt ,Krzysztof  aut Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial (CC by-nc)   http://www.doabooks.org/doab?func=fulltext&rid=12789|http://www.oapen.org/download?type=document&docid=340074
9788867050673   Emanuele Haus   aut Dynamics of an elastic satellite with internal friction.    2013    Ledizioni - LediPublishing  1 electronic resource ( p.) n this thesis, we study the dynamics... english Mathematics             Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial Share Alike (CC by-nc-sa)    http://www.doabooks.org/doab?func=fulltext&rid=15497|http://www.ledizioni.it/stag/wp-content/uploads/2014/02/tesi_haus.pdf

Примечание: я не мог понять роль "лидера" ни на входе, ни на выходе.

Это возможно и в XSLT 1.0.

Следующее решение построено вокруг списка уникальных тегов по всему документу и повторяет этот список для каждой записи. По сути, это позволяет выводить разделители, даже если определенный тег отсутствует в записи.

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:marc="http://www.loc.gov/MARC21/slim"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
  <xsl:output method="text" encoding="Windows-1252" />

  <xsl:param name="hDelim" select="'&#x9;'" /><!-- vertical delimiter -->
  <xsl:param name="vDelim" select="'&#xA;'" /><!-- horizontal delimiter -->
  <xsl:param name="sDelim" select="'|'" /><!-- subfield delimiter -->

  <!-- group tags by @tag + @code -->
  <xsl:key name="kAllTags" match="marc:controlfield | marc:subfield" use="
    concat(@tag, ../@tag, @code)
  " />
  <!-- group tags by record ID +  @tag + @code -->
  <xsl:key name="kRecordTags" match="marc:controlfield | marc:subfield" use="
    concat(generate-id(ancestor::marc:record), ':', @tag|../@tag, @code)
  " />
  <!-- build a list of unique tags to iterate over -->
  <xsl:variable name="uniqueTags" select="
    (//marc:controlfield | //marc:subfield)[
      generate-id() = generate-id(key('kAllTags', concat(@tag | ../@tag, @code)))
    ]
  " />

  <xsl:template match="marc:collection">
    <!-- write header line -->
    <xsl:text>leader</xsl:text>
    <xsl:value-of select="$hDelim" />

    <xsl:apply-templates select="$uniqueTags" mode="head">
      <xsl:sort select="concat(@tag|../@tag, @code)" />
    </xsl:apply-templates>
    <xsl:value-of select="$vDelim" />

    <!-- write individual records -->
    <xsl:apply-templates select="marc:record" />
  </xsl:template>

  <xsl:template match="marc:record">
    <xsl:variable name="recordId" select="generate-id()" />

    <xsl:value-of select="marc:leader" />
    <xsl:value-of select="$hDelim" />

    <!-- for each unique tag, find the fields that have that tag on this record -->
    <xsl:for-each select="$uniqueTags">
      <xsl:variable name="tagKey" select="concat($recordId, ':', @tag|../@tag, @code)" />
      <xsl:apply-templates select="key('kRecordTags', $tagKey)" mode="data" />
      <xsl:if test="position() != last()"><xsl:value-of select="$hDelim" /></xsl:if>
    </xsl:for-each>
    <xsl:if test="position() != last()"><xsl:value-of select="$vDelim" /></xsl:if>
  </xsl:template>

  <xsl:template match="marc:controlfield | marc:subfield" mode="head">
    <xsl:value-of select="concat(@tag|../@tag, @code)" />
    <xsl:if test="position() != last()"><xsl:value-of select="$hDelim" /></xsl:if>
  </xsl:template>

  <xsl:template match="marc:controlfield | marc:subfield" mode="data">
    <xsl:value-of select="normalize-space()" />
    <xsl:if test="position() != last()"><xsl:value-of select="$sDelim" /></xsl:if>
  </xsl:template>
</xsl:stylesheet>

Этот шаблон генерирует с вашими входными данными:

Лидер 001 005 007 008 020a    1004    100a    245a    260b    260c    300a    520a    546a    650a    653a    7004    700a    856u    856z
02179 002893u         12789   20120521    cuuuu---auuuu   120521s|||| xx o 0 u ||| |  9789089640574 Рой ван ван, Роберт авт. Новые перспективы в играх и взаимодействии Amsterdam University Press  2008    1 электронный ресурс (330 стр.) Этот том представляет собой сборник статей... Математика | Философия (общее)| Экономическая теория. Демография Экономика | Философия | Математика | Экономика | Философия |Wiskunde    Apt,Krzysztof  aut http://www.doabooks.org/doab?func=fulltext&rid=12789|http://www.oapen.org/download?type=document&docid=340074 Описание прав в Каталоге книг открытого доступа (DOAB): атрибуция некоммерческая (CC by-nc)   
01452 001933u         15497   20140217    cuuuu---auuuu   140217s|||| xx o 0 u ||| |  9788867050673   Emanuele Haus   aut Динамика упругого спутника с внутренним трением.    Ledizioni - LediPublishing  2013    1 электронный ресурс (стр.) В этом тезисе мы изучаем динамику... Математика http://www.doabooks.org/doab?func=fulltext&rid=15497|http://www.ledizioni.it/stag/wp-content/uploads/2014/02/tesi_haus.pdf Описание прав в Каталоге книг открытого доступа (DOAB): некоммерческая атрибуция Share Alike (CC by-nc-sa)    english

Вы говорите "если в записи отсутствует определенное поле" - из этого я делаю вывод, что у вас должен быть список полей, которые вы хотите экспортировать. (Все MARC? Каждое теоретически возможное поле от 000 до 999? Только вы можете сказать, а вы еще не сказали.) Если у вас нет списка полей, которые вы хотите экспортировать, то ваша задача состоит в противоречив, и вам нужно лучше понять проблему.

Допустим, например, что вы хотите экспортировать поля, перечисленные в переменной $fields.

<xsl:variable name="fields" as="xs:string*"
  select="tokenize('001 005 007 008 020 
                    100 245 260 260 300 
                    520 546 650 653 700 
                    856', '\s+')"/>

Ваша текущая проблема заключается в том, что ваш вывод формируется полями, присутствующими во входных данных, которые многие программисты XSLT называют "push" таблицей стилей. Вы хотите, чтобы выходные данные формировались списком полей в $fields, а не входными данными - вам нужно то, что эти программисты XSLT называют "стилевой" таблицей стилей. Таблицы стилей Pull распространены, когда мы готовим данные для не-XML-систем, таких как электронные таблицы, которые не очень хорошо разбираются в изменениях в структуре; они также распространены среди процедурных программистов, которые не знают другого способа думать о проблемах. И то и другое заставляет некоторых программистов XSLT немного взглянуть на таблицы стилей извлечения, но если вы правильно описали свою проблему, то вам нужна таблица стилей извлечения.

Исходя из сказанного выше, вы должны увидеть, что ваша проблема в том, что шаблон для / формирует вывод путем обработки ввода с <xsl:apply-templates select="*/*" />, Если на входе нет 546 полей, нет возможности вставить вкладку, где бы они появились, без особых излишних усилий.

Вы хотите заменить текущий apply-templates, который перебирает внуков, с конструкцией, которая перебирает номера полей в $fields, и для каждого номера поля выдает вкладку и любую другую соответствующую информацию, где другая соответствующая информация зависит от того, присутствуют ли поля с этим номером на входе или нет. В XSLT 3.0 вы сможете применять шаблоны к последовательности значений, чтобы вы могли написать <xsl:apply-templates select="$fields"/>, но в 2.0 это не вариант. Опции, доступные в 2.0, включают в себя:

  • Представлять поля $ не как последовательность строк, а как последовательность элементов; вызов <xsl:apply-templates select="$fields"/> итерировать по нужным номерам полей. Вам нужно будет не забывать передавать узел из входного документа (корень - хороший выбор), чтобы вы могли вернуться в него из шаблона для номера поля.

  • Вызовите именованный шаблон с $ fields в качестве параметра; в названном шаблоне выберите первый номер поля из списка, обработайте его, а затем рекурсивно вызовите тот же именованный шаблон с остальной частью списка. Если первого номера поля нет, последовательность номеров полей пуста, и все готово.

  • Напишите рекурсивную функцию, которая работает так же, как только что описанный шаблон.

  • Напишите функцию, которая обрабатывает один номер поля для одной записи MARC, и вызовите ее из XPath for выражение:

    <xsl:template match="marc:record">
      ...
      <xsl:sequence select="for $fn in $fields
         return my:one-field-one-record($fn, .)
         "/>
      ...
    </xsl:template>
    
Другие вопросы по тегам