XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件
Posted
技术标签:
【中文标题】XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件【英文标题】:XML XSLT Stream large xml file with SAXON EE10.6 【发布时间】:2021-11-15 07:15:43 【问题描述】:我必须将大型 xml 文件 (>5Gb) 导入 SOLR。我想先用 SAXON EE10.6 和流式 xsl 转换一个 xml 文件。我已阅读 SAXON EE10.6 应该可以,但我收到以下错误:
mytest.xsl 的第 20 行第 34 列出错: XTSE3430 模板规则不可流式处理
有多个消费操作数:我不熟悉流式传输 xslt 和 Saxon。如何让我的 xslt 适合流式传输以输出所需的 Solr 添加文档 xml。
我在这里有一个简化版本的 xml 和我使用的 xslt:https://xsltfiddle.liberty-development.net/asoTKU
它适用于较小的 xml 文件 (
【问题讨论】:
从saxonica.com/html/documentation10/sourcedocs/streaming开始,尝试学习。还要解释您的样式表试图实现的目标,并在帖子中显示相关部分。通常,进行两个向下选择的最简单方法是切换到非流式处理模式,该模式处理流式节点的copy-of()
,该流式节点足够“小”(例如,可能是Property
元素)以与其所有子节点一起实现/子孙。但是,不要假装我们理解或猜测您为什么在 node()
上匹配,例如,您似乎有明确的意图来处理元素节点。
如果你很绝望,另一个选择是使用xsl:fork
有两个向下选择分支,然后处理器需要找到一个缓冲策略,例如收集一个类别的所有子值,但也需要单独处理它们。但是没有一种方法可以神奇地使您的代码可流式传输,您将需要花一些时间来了解流式传输的局限性(仅转发解析,“缓冲”当前节点(例如具有属性或注释的元素节点或一个文本节点,维护一些祖先层次结构而不是兄弟层次结构)。
【参考方案1】:
假设您的 Properties
元素和 Category
“小”到足以被缓冲
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" expand-text="yes">
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:mode streamable="yes" on-no-match="shallow-skip"/>
<xsl:mode name="grounded"/>
<xsl:template match="Properties | Category">
<xsl:apply-templates select="copy-of()" mode="grounded"/>
</xsl:template>
<xsl:template match="Category" mode="grounded">
<field name="Category">.</field>
<xsl:apply-templates mode="#current"/>
</xsl:template>
<xsl:template match="Properties" mode="grounded">
<field name="Properties">.</field>
<xsl:apply-templates mode="#current"/>
</xsl:template>
<xsl:template match="Category/*" mode="grounded">
<field name="CAT_local-name()_s">.</field>
</xsl:template>
<xsl:template match="Property" mode="grounded">
<field name="key_s">value</field>
</xsl:template>
<xsl:template match="Item/*[not(self::Category | self::Properties)]">
<field name="local-name()">.</field>
</xsl:template>
<xsl:template match='/Items'>
<add>
<xsl:apply-templates select="Item"/>
</add>
</xsl:template>
<xsl:template match="Item">
<xsl:variable name="pos" select="position()"/>
<doc>
<xsl:apply-templates>
<xsl:with-param name="pos"><xsl:value-of select="$pos"/></xsl:with-param>
</xsl:apply-templates>
</doc>
</xsl:template>
</xsl:stylesheet>
但是您的代码(在<xsl:template match="Property">
中执行<xsl:apply-templates select="Property"/>
)表明,也许Property
元素可以递归嵌套,如果代码尝试像上面那样缓冲第一个元素,那么任意嵌套可能会导致内存问题Property
在内存中遇到,使用 copy-of()
。
但是,您的示例 XML 没有任何嵌套的 Property
元素。
我评论的xsl:fork
策略的一部分用于
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" expand-text="yes">
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:mode streamable="yes"/>
<xsl:mode name="text" streamable="yes"/>
<xsl:mode name="grounded"/>
<xsl:template match="Category">
<xsl:apply-templates select="copy-of()" mode="grounded"/>
</xsl:template>
<xsl:template match="Properties">
<xsl:fork>
<xsl:sequence>
<field name="Properties">
<xsl:apply-templates mode="text"/>
</field>
</xsl:sequence>
<xsl:sequence>
<xsl:apply-templates/>
</xsl:sequence>
</xsl:fork>
</xsl:template>
<xsl:template match="Category" mode="grounded">
<field name="Category">.</field>
<xsl:apply-templates mode="#current"/>
</xsl:template>
<xsl:template match="Category/*" mode="grounded">
<field name="CAT_local-name()_s">.</field>
</xsl:template>
<xsl:template match="Property">
<xsl:apply-templates select="copy-of()" mode="grounded"/>
</xsl:template>
<xsl:template match="Property" mode="grounded">
<field name="key_s">value</field>
</xsl:template>
<xsl:template match="Item/*[not(self::Category | self::Properties)]">
<field name="local-name()">.</field>
</xsl:template>
<xsl:template match='/Items'>
<add>
<xsl:apply-templates select="Item"/>
</add>
</xsl:template>
<xsl:template match="Item">
<xsl:variable name="pos" select="position()"/>
<doc>
<xsl:apply-templates>
<xsl:with-param name="pos"><xsl:value-of select="$pos"/></xsl:with-param>
</xsl:apply-templates>
</doc>
</xsl:template>
</xsl:stylesheet>
这避免了为每个 Properties
元素显式构造“一棵树”,但我不知道 Saxon 应用什么策略来确保 xsl:fork
的两个分支都可以访问子内容或后代内容。
【讨论】:
关于 xsl:fork,fork 的所有分支(叉子?)都会在输入事件发生时得到通知,有效地并行(尽管这一切都发生在单个线程中)。您需要注意的是,各种插脚的输出是缓冲的,因此可以按正确的顺序组装。所以 xsl:fork 在输入很大但输出很小的情况下效果很好。 属性没有嵌套,它在 xsl 中不正确。我会尝试您的建议以正确进行流式传输。谢谢 @MarcoDuindam,其中一项建议是否适用于 5 GB 输入?【参考方案2】:XSLT 3.0 流的规则非常复杂,教程介绍很少也无济于事。一个非常有用的资源是 Abel Braaksma 在 XML 布拉格 2014 上的演讲:在https://www.xfront.com/Transcript-of-Abel-Braaksma-talk-on-XSLT-Streaming-at-XML-Prague-2014.pdf有一个抄本和 YouTube 录音的链接
要记住的最重要的规则是:模板规则只能进行一次向下选择(它只有一次机会扫描后代树)。这就是你在写作时打破的规则:
<xsl:template match="node()">
<xsl:element name="field">
<xsl:attribute name="name">
<xsl:value-of select="local-name()"/>
</xsl:attribute>
<xsl:value-of select="."/>
</xsl:element>
<xsl:apply-templates select="*"/>
</xsl:template>
实际上,该代码可以简化为
<xsl:template match="node()">
<field name="local-name()">.</field>
<xsl:apply-templates select="*"/>
</xsl:template>
但这不会影响流能力:您要处理匹配节点的后代两次,一次获取字符串值 (.),一次将模板应用到子节点。
现在,在我看来,这个模板规则似乎只用于处理“叶元素”,即具有文本节点子节点但没有子元素的元素。如果是这种情况,那么 <xsl:apply-templates select="*"/>
永远不会选择任何东西:它是多余的,可以删除,这使得规则可流化。
您会收到另一条错误消息,即模板规则可以返回流式节点。不允许返回流式节点的原因有点微妙。它基本上使处理器无法进行数据流分析以证明流式传输是否可行。但又是 <xsl:apply-templates select="*"/>
导致问题的原因,摆脱它可以解决问题。
您的下一个问题是属性元素的模板规则。你把它写成
<xsl:template match="Property">
<xsl:element name="field">
<xsl:attribute name="name">
<xsl:value-of select="key"/>_s</xsl:attribute>
<xsl:value-of select="value"/>
</xsl:element>
<xsl:apply-templates select="Property"/>
</xsl:template>
它简化为:
<xsl:template match="Property">
<field name="key_s">value</field>
<xsl:apply-templates select="Property"/>
</xsl:template>
这是进行三个向下的选择:child::key
、child::value
和 child::Property
。在您的数据样本中,没有Property
元素有一个名为Property
的子元素,因此<xsl:apply-templates/>
可能又是多余的。对于key
和value
,一个有用的技巧是将它们读入地图:
<xsl:template match="Property">
<xsl:variable name="pair" as="map(*)">
<xsl:map>
<xsl:map-entry key="'key'" select="string(key)"/>
<xsl:map-entry key="'value'" select="string(value)"/>
</xsl:map>
</xsl:variable>
<field name="$pair?key_s">$pair?value</field>
</xsl:template>
之所以有效,是因为xsl:map
(如xsl:fork
)是“向下选择”规则的一个例外——地图可以在一次输入中构建。通过调用string()
,我们注意不要将任何流式节点放入映射中,因此我们稍后需要的数据已经在映射中捕获,我们不需要返回流式输入文档来读取它第二次。
我希望这能让您对前进的道路有所了解。 XSLT 中的流式传输不适合胆小的人,但如果您有 >5Gb 的输入文档,那么您就没有太多选择了。
【讨论】:
谢谢你,我会试试你的建议。属性没有嵌套,我在 xsl 中的错。另一种方法是将 xml 文件拆分为较小的文件并跳过流部分。 是的,如果您能够独立于所有其他人转换输入中的每个“记录”(无论“记录”是什么),这是一种常见的设计方法。使用与源中的“记录”匹配的模板规则编写流模式;此模板规则执行<xsl:apply-templates select="copy-of(.)" mode="ns"/>
,其中ns
是一种独立处理每个“记录”的非流模式。【参考方案3】:
给定的 xsl 解决方案适用于简化版本。然而,在完整的 xml 格式的 >5Gb 上,我没有让它工作。我已经解决了将 xml 文件拆分为大约 1Gb 的文件,然后在没有流式传输的情况下执行 xsl。
如果有人想要挑战,请私下联系我;)
【讨论】:
嗯,您的解决方案中有趣的部分仍然是您如何将 5GB 文档拆分为更小的文档?您是使用 SAX 或 Stax 还是如何做到的?【参考方案4】:我的 xml 文件在每个项目后都有一个换行符。所以我创建了一个简单的控制台应用程序,它将文件拆分为 500.000 行,删除空字符并使用 xsl 转换结果:
cleanxml.exe items.xml temp-items-solr.xml import.xsl
static void Main(string[] args)
string line;
XslCompiledTransform xsltTransform = new XslCompiledTransform();
xsltTransform.Load(@args[2]);
string fileToWriteTo = args[1];
StreamWriter writer = new StreamWriter(fileToWriteTo);
StreamReader file = new System.IO.StreamReader(@args[0]);
string fileOriginal = @args[1];
string firstLine = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Items>";
int i = 0;
int j = 1;
while ((line = file.ReadLine()) != null)
writer.WriteLine(CleanInvalidXmlChars(line));
if(i > 500000)
writer.WriteLine("</Items>");
writer.Flush();
writer.Dispose();
xsltTransform.Transform(fileToWriteTo, fileToWriteTo.Replace("temp-",""));
System.IO.File.Delete(fileToWriteTo);
fileToWriteTo = fileOriginal.Replace(".xml", "-" + j.ToString() + ".xml");
writer = new StreamWriter(fileToWriteTo);
writer.WriteLine(firstLine);
i = 0;
j += 1;
i += 1;
writer.Flush();
writer.Dispose();
xsltTransform.Transform(fileToWriteTo, fileToWriteTo.Replace("temp-", ""));
System.IO.File.Delete(fileToWriteTo);
file.Close();
private static MemoryStream ApplyXSLT(string xmlInput, string xsltFilePath)
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xmlInput);
XslCompiledTransform xsltTransform = new XslCompiledTransform();
xsltTransform.Load(xsltFilePath);
MemoryStream memoreStream = new MemoryStream();
xsltTransform.Transform(xmlDocument, null, memoreStream);
memoreStream.Position = 0;
return memoreStream;
public static string CleanInvalidXmlChars(string text)
string re = @"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]";
return Regex.Replace(text, re, "");
【讨论】:
以上是关于XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件的主要内容,如果未能解决你的问题,请参考以下文章
XSLT 函数返回不同的结果 [Saxon-EE vs Saxon-HE/PE]
从 Saxon 9.4he 中的嵌入式资源加载 xml 和 xslt