1

This is a follow-up question to a previous post. I'm working with a set of library catalog records (in MARC XML format), which I want to clean and enhance using a tool called OpenRefine. OpenRefine does not work very well with XML data, so I needed to transform the MARC XML to TSV.

The solution to my earlier post helped me do that. However, once I export from OpenRefine, I need to round-trip the data back into MARC XML. The output from OpenRefine is more complex than typical TSV: a single record can be spread across multiple lines because certain fields are repeatable:

leader  001 005 007 008 020__$a 1001_$a 1001_$4 2451_$a 260__$b 260__$c 300__$a 520__$a 546__$a 650_0$a 653__$a 7001_$a 7001_$4 85640$u 85640$z
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 ...        Mathematics Economics   Apt ,Krzysztof  aut http://www.doabooks.org/doab?func=fulltext&rid=12789    Description of rights in Directory of Open Access Books (DOAB): Attribution Non-commercial (CC by-nc)
                                                    Philosophy (General)    Philosophy          http://www.oapen.org/download?type=document&docid=340074    
                                                    Economic theory. Demography Mathematics             
                                                        Economie                
                                                        Filosofie               
                                                        Wiskunde                
01914 am a  002413u         13087   20120521    cuuuu---auuuu   120521s||||    xx      o     0   u ||| |    9783938616352   Roquette, Peter aut Helmut Hasse und Emmy Noether ; die Korrespondenz 1925 - 1935.  Universitätsverlag Göttingen  2006    1 electronic resource ( p.) This book reproduces ...    German  Science (General)   mathematics Lemmermeyer, Franz  aut http://www.doabooks.org/doab?func=fulltext&rid=13087    Description of rights in Directory of Open Access Books (DOAB): Attribution No Derivatives (CC by-nd)
                                                    Mathematics correspondence          http://www.oapen.org/download?type=document&docid=353934    
02345 am a  002773u         13241   20120521    cuuuu---auuuu   120521s||||    xx      o     0   u ||| |    9783940344502   Roquette, Peter aut Emil Artin und Helmut Hasse Universitätsverlag Göttingen  2008    1 electronic resource ( p.) This book contains ...  German  Mathematics history of mathematics  Lemmermeyer, Franz  aut http://www.doabooks.org/doab?func=fulltext&rid=13241    Description of rights in Directory of Open Access Books (DOAB): Attribution No Derivatives (CC by-nd)
                                                        Geschichte der Mathematik   Frei, Günther  aut http://www.oapen.org/download?type=document&docid=359593    
                                                        OAPEN   Noether, Emmy   aut     
                                                            Hasse, Helmut   aut     

I am trying to modify an XSLT 2.0 stylesheet that converts from TSV to XML (based on a solution proposed here):

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>


    <!-- File path parameter. -->
    <xsl:param name="filePath">stack_test-tsv-2.tsv</xsl:param>

    <!-- Main template that parses the TSV and creates structured XML. -->
    <xsl:template match="dummy">
        <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">

            <!-- Read in TSV file. -->
            <xsl:variable name="text" select="unparsed-text($filePath,'UTF-8')"/>
            <xsl:variable name="header">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="position()=1">
                            <xsl:value-of select="replace(regex-group(1),'\t','|')"/>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>
            <xsl:variable name="headerTokens" select="tokenize($header,'\|')"/>
            <xsl:variable name="recordBody">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="not(position()=1)">

                            <!-- Begin creating the records. 
                                 Assign column headers to field elements as @name attributes. -->
                            <xsl:analyze-string select="." regex="([^\t][^\t]*)\t?|\t">
                                <xsl:matching-substring>
                                    <xsl:variable name="pos" select="position()"/>
                                    <xsl:variable name="headerToken" select="$headerTokens[$pos]"/>
                                    <xsl:if test="regex-group(1)[position() = 1]">
                                        <field name="{$headerToken}">
                                            <xsl:value-of select="regex-group(1)"/>
                                        </field>
                                    </xsl:if>
                                </xsl:matching-substring>
                            </xsl:analyze-string>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>

            <!-- Split into record chunks. -->
            <xsl:variable name="recompile">
                <xsl:for-each select="$recordBody/field[@name='leader'][.!='']">
                    <xsl:variable name="ID" select="."/>
                    <record>
                        <xsl:sequence select="."/>
                        <xsl:for-each select="following-sibling::field[. != $ID]">
                            <xsl:if
                                test="preceding-sibling::field[@name='leader'][5][. != ''] = $ID 
                                and not(self::field[@name='leader'])">
                                <xsl:sequence select="."/>
                            </xsl:if>
                        </xsl:for-each>
                    </record>
                </xsl:for-each>
            </xsl:variable>

            <!-- Rebuild MARC record. -->
            <xsl:for-each select="$recompile/record">
                <marc:record>
                    <marc:leader>
                        <xsl:value-of select="child::node()[@name='leader']"/>
                    </marc:leader>
                    <xsl:if test="child::node()/@name='001'">
                        <marc:controlfield tag="001">
                            <xsl:value-of select="child::node()[@name='001']"/>
                        </marc:controlfield>
                    </xsl:if>
                    <xsl:if test="child::node()/@name='005'">
                        <marc:controlfield tag="005">
                            <xsl:value-of select="child::node()[@name='005']"/>
                        </marc:controlfield>
                    </xsl:if>
                    <xsl:if test="child::node()/@name='007'">
                        <marc:controlfield tag="007">
                            <xsl:value-of select="child::node()[@name='007']"/>
                        </marc:controlfield>
                    </xsl:if>
                    <xsl:if test="child::node()/@name='008'">
                        <marc:controlfield tag="008">
                            <xsl:value-of select="child::node()[@name='008']"/>
                        </marc:controlfield>
                    </xsl:if>

                    <xsl:for-each-group
                        select="child::node()[number(substring(@name,1, 3)) &gt;= 020]"
                        group-adjacent="substring(@name, 1, 3)">
                        <xsl:sort select="current-grouping-key()"/>

                        <xsl:choose>
                            <xsl:when test="starts-with(current-grouping-key(),'6')">
                                <xsl:for-each select="current-group()">
                                    <marc:datafield tag="{current-grouping-key()}"
                                        ind1="
                                        {if (substring(@name,4,1) = '_') 
                                        then ' ' 
                                        else substring(@name, 4, 1)}"
                                        ind2="{
                                        if (substring(@name, 5, 1) = '_') 
                                        then ' ' 
                                        else substring(@name, 5, 1)}">
                                        <marc:subfield code="{substring(@name, 7, 1)}">
                                            <xsl:value-of select="."/>
                                        </marc:subfield>
                                    </marc:datafield>
                                </xsl:for-each>
                            </xsl:when>
                            <xsl:otherwise>
                                <marc:datafield tag="{current-grouping-key()}"
                                    ind1="{if (substring(@name,4,1) = '_') 
                                    then ' ' 
                                    else substring(@name, 4, 1)}"
                                    ind2="{
                                    if (substring(@name, 5, 1) = '_') 
                                    then ' ' 
                                    else substring(@name, 5, 1)}">
                                    <xsl:for-each select="current-group()">
                                        <marc:subfield code="{substring(@name, 7, 1)}">
                                            <xsl:value-of select="."/>
                                        </marc:subfield>
                                    </xsl:for-each>
                                </marc:datafield>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:for-each-group>
                </marc:record>
            </xsl:for-each>
        </marc:collection>
    </xsl:template>
</xsl:stylesheet>

This gives me basically what I need, but it is not flexible, and it would be challenging to account for all the possible variations in the source data using this approach.

Instead, I would like to modify the xsl:analyze-string regex in order to process this more complex tab-delimited structure up front. Basically, every time there is a value for "leader," there should be a new record. The individual values that appear on subsequent lines should be parsed as separate XML elements, like so:

<?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>01914 am a  002413u     </marc:leader>
        <marc:controlfield tag="001">13087</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">9783938616352</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="100" ind1="1" ind2=" ">
            <marc:subfield code="a">Roquette, Peter</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="245" ind1="1" ind2=" ">
            <marc:subfield code="a">Helmut Hasse und Emmy Noether ; die Korrespondenz 1925 - 1935.</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="260" ind1=" " ind2=" ">
            <marc:subfield code="b">Universitätsverlag Göttingen</marc:subfield>
            <marc:subfield code="c">2006</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">This book reproduces...</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="546" ind1=" " ind2=" ">
            <marc:subfield code="a">German</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Science (General)</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Mathematics</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">correspondence</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Lemmermeyer, Franz</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=13087</marc:subfield>
            <marc:subfield code="z">Description of rights in Directory of Open Access Books (DOAB): Attribution No Derivatives (CC by-nd)</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=353934</marc:subfield>
        </marc:datafield>
    </marc:record>
    <marc:record>
        <marc:leader>02345 am a  002773u     </marc:leader>
        <marc:controlfield tag="001">13241</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">9783940344502</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="100" ind1="1" ind2=" ">
            <marc:subfield code="a">Roquette, Peter</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="245" ind1="1" ind2=" ">
            <marc:subfield code="a">Emil Artin und Helmut Hasse</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="260" ind1=" " ind2=" ">
            <marc:subfield code="b">Universitätsverlag Göttingen</marc:subfield>
            <marc:subfield code="c">2008</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">This book contains ...</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="546" ind1=" " ind2=" ">
            <marc:subfield code="a">German</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="650" ind1=" " ind2="0">
            <marc:subfield code="a">Mathematics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">history of mathematics</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">Geschichte der Mathematik</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="653" ind1=" " ind2=" ">
            <marc:subfield code="a">OAPEN</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Lemmermeyer, Franz</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Frei, Günther</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Noether, Emmy</marc:subfield>
            <marc:subfield code="4">aut</marc:subfield>
        </marc:datafield>
        <marc:datafield tag="700" ind1="1" ind2=" ">
            <marc:subfield code="a">Hasse, Helmut</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=13241</marc:subfield>
            <marc:subfield code="z">Description of rights in Directory of Open Access Books (DOAB): Attribution No Derivatives (CC by-nd)</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=359593</marc:subfield>
        </marc:datafield>
    </marc:record>
</marc:collection> 
Community
  • 1
  • 1
tat
  • 321
  • 1
  • 19
  • "_every time there is a value for "leader," there should be a new record_" - In your sample text file there appear to be 3 lines with a "leader" value, but your sample XML only shows 2 `marc:record` elements. Is there a reason for this or is the XML incomplete? – Daniel Haley Dec 10 '14 at 00:31

1 Answers1

1

I made a couple of minor tweaks to your XSLT and the output is very close to what you posted. There are 2 differences.

  1. The text "This volume is a collection of papers ..." is "This volume is a ..." because "collection of papers" isn't in the input

  2. The 3rd to last marc:datafield has the subfields combined. This is because the @name was the same and the grouping-key() didn't start with a 6. Not sure if this is correct or not.

Possibly bad datafield:

<marc:datafield tag="700" ind1="1" ind2=" ">
   <marc:subfield code="a">Noether, Emmy</marc:subfield>
   <marc:subfield code="4">aut</marc:subfield>
   <marc:subfield code="a">Hasse, Helmut</marc:subfield>
   <marc:subfield code="4">aut</marc:subfield>
</marc:datafield>

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">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>


    <!-- File path parameter. -->
    <xsl:param name="filePath">stack_test-tsv-2.tsv</xsl:param>

    <!-- Main template that parses the TSV and creates structured XML. -->
    <xsl:template match="/*">
        <marc:collection
            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">

            <!-- Read in TSV file. -->
            <xsl:variable name="text" select="unparsed-text($filePath,'UTF-8')"/>

            <xsl:variable name="header">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="position()=1">
                            <xsl:value-of select="replace(regex-group(1),'\t','|')"/>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>
            <xsl:variable name="headerTokens" select="tokenize($header,'\|')"/>
            <xsl:variable name="recordBody">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="not(position()=1)">

                            <!-- Begin creating the records. 
                                 Assign column headers to field elements as @name attributes. -->
                            <xsl:analyze-string select="." regex="([^\t][^\t]*)\t?|\t">
                                <xsl:matching-substring>
                                    <xsl:variable name="pos" select="position()"/>
                                    <xsl:variable name="headerToken" select="$headerTokens[$pos]"/>
                                    <xsl:if test="regex-group(1)[position() = 1]">
                                        <field name="{normalize-space($headerToken)}">
                                            <xsl:value-of select="regex-group(1)"/>
                                        </field>
                                    </xsl:if>
                                </xsl:matching-substring>
                            </xsl:analyze-string>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>
            <!-- Split into record chunks. -->
            <xsl:variable name="recompile">
                <xsl:for-each-group select="$recordBody/field" group-starting-with="field[@name='leader'][string()]">
                    <record>
                        <xsl:copy-of select="current-group()"/>
                    </record>
                </xsl:for-each-group>
            </xsl:variable>

            <!-- Rebuild MARC record. -->
            <xsl:for-each select="$recompile/record">
                <marc:record>
                    <marc:leader>
                        <xsl:apply-templates select="field[@name='leader']"/>
                    </marc:leader>
                    <xsl:apply-templates select="*[@name=('001','005','007','008')]" mode="controlfield"/>                    
                    <xsl:for-each-group
                        select="field[number(substring(@name,1, 3)) >= 20]"
                        group-adjacent="substring(@name, 1, 3)">
                        <xsl:sort select="current-grouping-key()"/>

                        <xsl:choose>
                            <xsl:when test="starts-with(current-grouping-key(),'6')">
                                <xsl:for-each select="current-group()">
                                    <marc:datafield tag="{current-grouping-key()}"
                                        ind1="{if (substring(@name,4,1) = '_') 
                                        then ' ' 
                                        else substring(@name, 4, 1)}"
                                        ind2="{
                                        if (substring(@name, 5, 1) = '_') 
                                        then ' ' 
                                        else substring(@name, 5, 1)}">
                                        <marc:subfield code="{substring(@name, 7, 1)}">
                                            <xsl:value-of select="."/>
                                        </marc:subfield>
                                    </marc:datafield>
                                </xsl:for-each>
                            </xsl:when>
                            <xsl:otherwise>
                                <marc:datafield tag="{current-grouping-key()}"
                                    ind1="{if (substring(@name,4,1) = '_') 
                                    then ' ' 
                                    else substring(@name, 4, 1)}"
                                    ind2="{
                                    if (substring(@name, 5, 1) = '_') 
                                    then ' ' 
                                    else substring(@name, 5, 1)}">
                                    <xsl:for-each select="current-group()">
                                        <marc:subfield code="{substring(@name, 7, 1)}">
                                            <xsl:value-of select="."/>
                                        </marc:subfield>
                                    </xsl:for-each>
                                </marc:datafield>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:for-each-group>
                </marc:record>
            </xsl:for-each>
        </marc:collection>
    </xsl:template>

    <xsl:template match="field" mode="controlfield">
        <marc:controlfield tag="{@name}">
            <xsl:value-of select="."/>
        </marc:controlfield>
    </xsl:template>

    <xsl:template match="field">
        <xsl:value-of select="normalize-space()"/>
    </xsl:template>

</xsl:stylesheet>

EDIT

Here's an updated version of the XSLT that produces the wanted output. My opinion is that any variation in the datafields should be handled when transforming the field XML. I think adjustments to the XML would be easier than trying to modify regexes.

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">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>


    <!-- File path parameter. -->
    <xsl:param name="filePath">stack_test-tsv-2.tsv</xsl:param>

    <!-- Main template that parses the TSV and creates structured XML. -->
    <xsl:template match="/*">
        <marc:collection
            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">

            <!-- Read in TSV file. -->
            <xsl:variable name="text" select="unparsed-text($filePath,'UTF-8')"/>

            <xsl:variable name="header">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="position()=1">
                            <xsl:value-of select="replace(regex-group(1),'\t','|')"/>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>
            <xsl:variable name="headerTokens" select="tokenize($header,'\|')"/>
            <xsl:variable name="recordBody">
                <xsl:analyze-string select="$text" regex="(..*)">
                    <xsl:matching-substring>
                        <xsl:if test="not(position()=1)">

                            <!-- Begin creating the records. 
                                 Assign column headers to field elements as @name attributes. -->
                            <xsl:analyze-string select="." regex="([^\t][^\t]*)\t?|\t">
                                <xsl:matching-substring>
                                    <xsl:variable name="pos" select="position()"/>
                                    <xsl:variable name="headerToken" select="$headerTokens[$pos]"/>
                                    <xsl:if test="regex-group(1)[position() = 1]">
                                        <field name="{normalize-space($headerToken)}" nbr="{substring(tokenize(normalize-space($headerToken),'[^0-9]')[1],1,3)}">
                                            <xsl:value-of select="regex-group(1)"/>
                                        </field>
                                    </xsl:if>
                                </xsl:matching-substring>
                            </xsl:analyze-string>
                        </xsl:if>
                    </xsl:matching-substring>
                </xsl:analyze-string>
            </xsl:variable>
            <!-- Split into record chunks. -->
            <xsl:variable name="recompile">
                <xsl:for-each-group select="$recordBody/field" group-starting-with="field[@name='leader'][string()]">
                    <record>
                        <xsl:copy-of select="current-group()"/>
                    </record>
                </xsl:for-each-group>
            </xsl:variable>

            <!-- Rebuild MARC record. -->
            <xsl:for-each select="$recompile/record">
                <marc:record>
                    <marc:leader>
                        <xsl:apply-templates select="field[@name='leader']"/>
                    </marc:leader>
                    <xsl:apply-templates select="*[@name=('001','005','007','008')]" mode="controlfield"/>                    
                    <xsl:variable name="datafields">
                        <xsl:for-each-group
                            select="field[number(@nbr) >= 20]"
                            group-adjacent="@nbr">
                            <xsl:variable name="firstName" select="current-group()[1]/@name"/>
                            <xsl:for-each-group select="current-group()" group-starting-with="*[@name=$firstName]">
                                <marc:datafield tag="{@nbr}"
                                    ind1="{if (substring(@name,4,1) = '_') 
                                    then ' ' 
                                    else substring(@name, 4, 1)}"
                                    ind2="{
                                    if (substring(@name, 5, 1) = '_') 
                                    then ' ' 
                                    else substring(@name, 5, 1)}">
                                    <xsl:for-each select="current-group()">
                                        <marc:subfield code="{substring(@name, 7, 1)}">
                                            <xsl:apply-templates select="."/>
                                        </marc:subfield>
                                    </xsl:for-each>
                                </marc:datafield>                            
                            </xsl:for-each-group>                            
                        </xsl:for-each-group>                        
                    </xsl:variable>
                    <xsl:perform-sort select="$datafields/*">
                        <xsl:sort select="@tag"></xsl:sort>
                    </xsl:perform-sort>                            
                </marc:record>
            </xsl:for-each>
        </marc:collection>
    </xsl:template>

    <xsl:template match="field" mode="controlfield">
        <marc:controlfield tag="{@name}">
            <xsl:value-of select="."/>
        </marc:controlfield>
    </xsl:template>

    <xsl:template match="field">
        <xsl:value-of select="normalize-space()"/>
    </xsl:template>

</xsl:stylesheet>
Daniel Haley
  • 51,389
  • 6
  • 69
  • 95
  • Thanks. Yes, the combined subfields you reference are problematic. There should be only one subfield a/4 pair per 700 datafield. It's hard to specify the structure of each datafield, since there's a fair amount of variation. That's why I was thinking the regex could be modified to match the data structure in the TSV. – tat Dec 10 '14 at 15:17