解析 html 文件,使用 xslt 3 从嵌套类别层次结构中获取数据

parse html file, obtaining data from nested categories hierarchy using xslt 3

给定以下 html 文件:

http://bpeck.com/references/DDC/ddc_mine900.htm

http://bpeck.com/references/DDC/ddc_mine200.htm

http://bpeck.com/references/DDC/ddc_mine500.htm

等等,

如何获得显示类别层次结构的输出?

/---------------------
| ID  | Name
| 1   | Main Category
| 3   |   Sub Category
| 5   |     Sub-Sub Category
| 4   |   Sub Category
| 2   | Next Main Category
\----------------------

理想情况下,如果输出结果可以是 json 格式,但我想 xml 可以。

努力使用串行解析器 (SAX),但失败了,正在寻找一个优雅的解决方案。

主要类别

    900 World History

    910 Geography and travel [see area subdivisions]

    920 Biography, genealogy, insignia

    930 History of the ancient world

    940 General history of Europe [check schedules for date subdivisions]

    950 General history of Asia, Far East

等...

900 个子类别:

900 Geography & history
901 Philosophy & theory
902 Miscellany
903 Dictionaries & encyclopedias
904 Collected accounts of events
905 Serial publications
906 Organizations & management
907 Education, research, related topics
908 With respect to kinds of persons

...

在 909 世界历史下找到的子类别示例:

909.7 18th century, 1700-1799
909.8 1800-
909.82 1900-

输出我更喜欢你认为最好的方法。 每个键都是ID,即900、901、902等,对应的值是名称:Geography & history、Philosophy & theory、Miscellany。此输出 json 应该是嵌套的,显示类别的层次结构。 我使用 saxon HE 版本 9.8

您的数据结构似乎很差(只检查了 http://bpeck.com/references/DDC/ddc_mine900.htm but that doesn't pass HTML validation at https://validator.w3.org/check?uri=http%3A%2F%2Fbpeck.com%2Freferences%2FDDC%2Fddc_mine900.htm&charset=%28detect+automatically%29&doctype=Inline&group=0,特别是子类别列表没有正确嵌套,因此需要一些 XSLT 管道)。

至于使用 XSLT 2 或 3 解析 HTML,如果您无法将 Saxon 设置为使用 TagSoup 之类的 HTML 解析器而不是 XML 解析器来输入您可以尝试使用 David Carlisle 在纯 XSLT 2 中实现的 htmlparse 函数,它可以在 https://github.com/davidcarlisle/web-xslt/blob/master/htmlparse/htmlparse.xsl 在线获得,如果您想使用它来解析您的 HTML,请确保下载本地副本] 在 XSLT 2 或 3 中具有良好的性能。

这是一个使用在线副本并将输入 HTML 解析为我编写的某种 XML 格式的示例:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:d="data:,dpc"
    xmlns:mf="http://example.com/mf"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
    expand-text="yes"
    exclude-result-prefixes="#all"
    version="3.0">

    <xsl:import href="https://github.com/davidcarlisle/web-xslt/raw/master/htmlparse/htmlparse.xsl"/>

    <xsl:param name="html-file" as="xs:string">http://bpeck.com/references/DDC/ddc_mine900.htm</xsl:param>

    <xsl:param name="html-text" as="xs:string" select="unparsed-text($html-file)"/>

    <xsl:variable name="html-doc" select="d:htmlparse($html-text, '', true())"/>

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="/" name="xsl:initial-template">
        <categories>
            <xsl:apply-templates select="tail($html-doc//table)"/>
        </categories>
    </xsl:template>

    <xsl:template match="table">
        <category>
            <xsl:sequence select="mf:create-attributes(tr[1]/td[1])"/>
            <xsl:apply-templates select="head(tr)/td[2], tail(tr)/td[1]"/>
        </category>
    </xsl:template>

    <xsl:template match="td">
        <subcategory>
            <xsl:sequence select="mf:create-attributes(.)"/>
            <xsl:apply-templates select="following-sibling::td[1]/ul"/>
        </subcategory>
    </xsl:template>

    <xsl:template match="ul">
        <xsl:for-each-group select="*" group-starting-with="li">
            <sub-sub-category>
                <xsl:sequence select="mf:create-attributes(.)"/>
                <xsl:apply-templates select="tail(current-group())"/>
            </sub-sub-category>
        </xsl:for-each-group>
    </xsl:template>

    <xsl:function name="mf:create-attributes" as="attribute()*">
        <xsl:param name="input" as="xs:string"/>
        <xsl:variable name="input-components" as="xs:string*" select="tokenize(normalize-space($input))"/>
        <xsl:attribute name="name" select="head($input-components)"/>
        <xsl:attribute name="title" select="tail($input-components)"/>
    </xsl:function>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/3NzcBud

要使用 XSLT 3 创建 JSON,您有几个选择,一个是让样式表创建 xml-to-json 函数期望的格式 (https://www.w3.org/TR/xslt-30/#json-to-xml-mapping);在下面的示例中,我使用采用先前结果 XML 的模式扩展上面的样式表,以创建可以提供给 xml-to-json:

的 XML 输入
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:d="data:,dpc"
    xmlns:mf="http://example.com/mf"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
    expand-text="yes"
    exclude-result-prefixes="#all"
    version="3.0">

    <xsl:import href="https://github.com/davidcarlisle/web-xslt/raw/master/htmlparse/htmlparse.xsl"/>

    <xsl:param name="html-file" as="xs:string">http://bpeck.com/references/DDC/ddc_mine900.htm</xsl:param>

    <xsl:param name="html-text" as="xs:string" select="unparsed-text($html-file)"/>

    <xsl:variable name="html-doc" select="d:htmlparse($html-text, '', true())"/>

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="/" name="xsl:initial-template">
        <xsl:variable name="categories">
            <categories>
                <xsl:apply-templates select="tail($html-doc//table)"/>
            </categories>
        </xsl:variable>
        <xsl:apply-templates select="$categories" mode="json"/>
    </xsl:template>

    <xsl:template match="table">
        <category>
            <xsl:sequence select="mf:create-attributes(tr[1]/td[1])"/>
            <xsl:apply-templates select="head(tr)/td[2], tail(tr)/td[1]"/>
        </category>
    </xsl:template>

    <xsl:template match="td">
        <subcategory>
            <xsl:sequence select="mf:create-attributes(.)"/>
            <xsl:apply-templates select="following-sibling::td[1]/ul"/>
        </subcategory>
    </xsl:template>

    <xsl:template match="ul">
        <xsl:for-each-group select="*" group-starting-with="li">
            <sub-sub-category>
                <xsl:sequence select="mf:create-attributes(.)"/>
                <xsl:apply-templates select="tail(current-group())"/>
            </sub-sub-category>
        </xsl:for-each-group>
    </xsl:template>

    <xsl:function name="mf:create-attributes" as="attribute()*">
        <xsl:param name="input" as="xs:string"/>
        <xsl:variable name="input-components" as="xs:string*" select="tokenize(normalize-space($input))"/>
        <xsl:attribute name="name" select="head($input-components)"/>
        <xsl:attribute name="title" select="tail($input-components)"/>
    </xsl:function>

    <xsl:mode name="json" on-no-match="shallow-skip"/>

    <xsl:template match="category | subcategory | sub-sub-category" mode="json">
        <fn:map>
            <fn:map key="{@name}">
                <fn:string key="title">{@title}</fn:string>
                <xsl:where-populated>
                    <fn:array key="children">
                        <xsl:apply-templates mode="#current"/>
                    </fn:array>
                </xsl:where-populated>
            </fn:map>
        </fn:map>
    </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/3NzcBud/1

然后最后一步将使用函数 xml-to-json (https://www.w3.org/TR/xpath-functions/#func-xml-to-json) 输出 JSON 而不是 XML:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:d="data:,dpc"
    xmlns:mf="http://example.com/mf"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
    expand-text="yes"
    exclude-result-prefixes="#all"
    version="3.0">

    <xsl:import href="https://github.com/davidcarlisle/web-xslt/raw/master/htmlparse/htmlparse.xsl"/>

    <xsl:param name="html-file" as="xs:string">http://bpeck.com/references/DDC/ddc_mine900.htm</xsl:param>

    <xsl:param name="html-text" as="xs:string" select="unparsed-text($html-file)"/>

    <xsl:variable name="html-doc" select="d:htmlparse($html-text, '', true())"/>

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output method="text"/>

    <xsl:template match="/" name="xsl:initial-template">
        <xsl:variable name="categories">
            <categories>
                <xsl:apply-templates select="tail($html-doc//table)"/>
            </categories>
        </xsl:variable>
        <xsl:variable name="json-xml">
            <xsl:apply-templates select="$categories" mode="json"/>            
        </xsl:variable>
        <xsl:sequence select="xml-to-json($json-xml, map { 'indent' : true() })"/>
    </xsl:template>

    <xsl:template match="table">
        <category>
            <xsl:sequence select="mf:create-attributes(tr[1]/td[1])"/>
            <xsl:apply-templates select="head(tr)/td[2], tail(tr)/td[1]"/>
        </category>
    </xsl:template>

    <xsl:template match="td">
        <subcategory>
            <xsl:sequence select="mf:create-attributes(.)"/>
            <xsl:apply-templates select="following-sibling::td[1]/ul"/>
        </subcategory>
    </xsl:template>

    <xsl:template match="ul">
        <xsl:for-each-group select="*" group-starting-with="li">
            <sub-sub-category>
                <xsl:sequence select="mf:create-attributes(.)"/>
                <xsl:apply-templates select="tail(current-group())"/>
            </sub-sub-category>
        </xsl:for-each-group>
    </xsl:template>

    <xsl:function name="mf:create-attributes" as="attribute()*">
        <xsl:param name="input" as="xs:string"/>
        <xsl:variable name="input-components" as="xs:string*" select="tokenize(normalize-space($input))"/>
        <xsl:attribute name="name" select="head($input-components)"/>
        <xsl:attribute name="title" select="tail($input-components)"/>
    </xsl:function>

    <xsl:mode name="json" on-no-match="shallow-skip"/>

    <xsl:template match="category | subcategory | sub-sub-category" mode="json">
        <fn:map>
            <fn:map key="{@name}">
                <fn:string key="title">{@title}</fn:string>
                <xsl:where-populated>
                    <fn:array key="children">
                        <xsl:apply-templates mode="#current"/>
                    </fn:array>
                </xsl:where-populated>
            </fn:map>
        </fn:map>
    </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/3NzcBud/2

https://xsltfiddle.liberty-development.net/3NzcBud/3 是应用于不同输入文件的相同代码,至少 XML -> XML -> JSON 生成不会中断,我尚未检查 HTML table 和列表是否与之前输入的结构相同。

作为使用 XSLT 3 创建 JSON 并支持 XPath 3.1 地图和数组数据类型(所有版本的 Saxon 9.8/9.9 以及 Altova 2017/2018/2019 均提供)的另一种选择,您可以直接创建映射和数组并使用方法 json:

序列化
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:d="data:,dpc"
    xmlns:mf="http://example.com/mf"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
    xmlns:map="http://www.w3.org/2005/xpath-functions/map"
    expand-text="yes"
    exclude-result-prefixes="#all"
    version="3.0">

    <xsl:import href="https://github.com/davidcarlisle/web-xslt/raw/master/htmlparse/htmlparse.xsl"/>

    <xsl:param name="html-file" as="xs:string"
        >http://bpeck.com/references/DDC/ddc_mine900.htm</xsl:param>

    <xsl:param name="html-text" as="xs:string" select="unparsed-text($html-file)"/>

    <xsl:variable name="html-doc" select="d:htmlparse($html-text, '', true())"/>

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output method="json" indent="yes"/>

    <xsl:template match="/" name="xsl:initial-template">
        <xsl:apply-templates select="tail($html-doc//table)"/>
    </xsl:template>

    <xsl:template match="table">
        <xsl:sequence select="mf:category-map(tr[1]/td[1], (head(tr)/td[2], tail(tr)/td[1]))"/>
    </xsl:template>

    <xsl:template match="td">
        <xsl:sequence select="mf:category-map(., following-sibling::td[1]/ul)"/>
    </xsl:template>

    <xsl:template match="ul">
        <xsl:for-each-group select="*" group-starting-with="li">
            <xsl:sequence select="mf:category-map(., tail(current-group()))"/>
        </xsl:for-each-group>
    </xsl:template>

    <xsl:function name="mf:split-index-title" as="xs:string*">
        <xsl:param name="input" as="xs:string"/>
        <xsl:sequence
            select="
                let $components := tokenize(normalize-space($input))
                return
                    (head($components), string-join(tail($components), ' '))"
        />
    </xsl:function>

    <xsl:function name="mf:category-map" as="map(xs:string, item())">
        <xsl:param name="category" as="element()"/>
        <xsl:param name="subcategories" as="element()*"/>
        <xsl:variable name="components" select="mf:split-index-title($category)"/>
        <xsl:map>
            <xsl:map-entry key="$components[1]">
                <xsl:map>
                    <xsl:map-entry key="'title'" select="$components[2]"/>
                    <xsl:if test="$subcategories">
                        <xsl:map-entry key="'children'">
                            <xsl:sequence select="array{ mf:child-categories($subcategories) }"/>
                        </xsl:map-entry>
                    </xsl:if>
                </xsl:map>
            </xsl:map-entry>
        </xsl:map>
    </xsl:function>

    <xsl:function name="mf:child-categories" as="map(xs:string, item())*">
        <xsl:param name="subcategories" as="element()*"/>
        <xsl:apply-templates select="$subcategories"/>
    </xsl:function>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/3NzcBud/4