⽤Java分割⼤型XML⽂件
上周,我被要求⽤Java编写⼀些东西,该东西能够将单个30GB XML⽂件拆分为可配置⽂件⼤⼩的较⼩部分。 该⽂件的使⽤者将是⼀个中间件应⽤程序,该应⽤程序存在XML较⼤的问题。 在后台,它使⽤某种DOM解析技术,使它在⼀段时间后耗尽内存。 由于它是基于供应商的中间件,因此我们⽆法⾃⾏纠正。 最好的选择是创建⼀些预处理⼯具,该⼯具会先将⼤⽂件分成多个较⼩的块,然后再由中间件处理。
XML⽂件带有⼀个相应的W3C模式,该模式由强制性头部分和紧随其后嵌套有多个0 .. *数据元素的内容元素组成。 对于演⽰代码,我以简化形式重新创建了架构:
标头的⼤⼩可以忽略。 单个数据元素的重复也很⼩,可以说少于50kB。 由于数据元素重复的次数,XML太⼤了。 要求是:
分割后的XML的每⼀部分都应为语法有效的XML,并且每⼀部分还应针对原始模式进⾏验证
该⼯具应根据架构验证XML,并报告所有验证错误。 验证不得阻塞,并且不可在输出中跳过⾮验证元素或属性
对于标头,决定将其复制到每个新的输出⽂件中,⽽不是将其复制到每个新的输出⽂件中,并使⽤⼀些
处理信息和⼀些默认值来重新⽣成该标头
因此,使⽤诸如Unix Split之类的⼆进制拆分⼯具是不可能的。 在固定数量的字节之后,这将拆分,从⽽确保XML损坏。 我不太确定,但是诸如Split之类的⼯具也不了解编码。 因此,在字节“ x”之后进⾏拆分不仅会导致在XML元素的中间进⾏拆分(例如),⽽且甚⾄会在字符编码序列的中间进⾏拆分(例如,在使⽤经过UTF8编码的Unicode时)。 显然,我们需要更智能的东西。
XSLT作为核⼼技术也是⾏不通的。 乍⼀看,可能会很想尝试:使⽤XSLT2.0,可以从单个输⼊⽂件创建多个输出⽂件。 甚⾄可以在转换时验证输⼊⽂件。 但是,细节始终是魔⿁。 否则,在Java中进⾏简单的操作(例如将验证错误写⼊单独的⽂件或检查当前输出⽂件的⼤⼩)可能需要⾃定义Java代码。 对于Xalan和Saxon来说,当然可以有这样的扩展,但是Xalan不是XSLT2.0实现,因此只剩下Saxon。最后但并⾮最不重要的⼀点是,XSLT1.0 / 2.0是⾮流式的,这意味着它们会将整个源⽂档读⼊内存,因此这显然将XSLT排除在了可能性之外。
剩下的唯⼀选择就是Java XML解析。 当然,在这种情况下,理想的选择是StAX。 我不在这⾥进⾏SAX与StAX的⽐较,事实是StAX能够针对架构的⾝份进⾏验证(⾄少某些解析器可以)并且还可以编写XML。 ⽽且,与SAX相⽐,API的使⽤要容易得多,因为基于pull的API提供了对迭代⽂档的更多控制,并且⽐SAX的推送⽅式更令⼈愉快。 好的,我们需要什么:
能够验证XML的StAX实现
Oracle的JDK默认附带SJSXP作为StAX实现,但是此验证⽆效。
最好具有某种对象/ XML映射技术,⽤于(重新)创建标头,⽽不是⼿动摆弄元素并必须查正确的数据类型/格式显然是JAXB。
该代码有点⼤,⽆法在此处整体显⽰。 可以访问源⽂件,XSD和测试XML
java xml是什么GitHub上。 它具有Maven pom⽂件,因此您应该能够在选择的IDE中将其导⼊。 JAXB绑定编译器将⾃动编译模式,并将⽣成的源放在类路径上。
public void startSplitting() throws Exception {
XMLStreamReader2 xmlStreamReader = ((XMLInputFactory2) wInstance())
.createXMLStreamReader(Resource("/l"));
PrintWriter validationResults = enableValidationHandling(xmlStreamReader);
int fileNumber = 0;
int dataRepetitions = 0;
XMLStreamWriter xmlStreamWriter = openOutputFileAndWriteHeader(++fileNumber); // Prepare first file
第⼀⾏创建了StAX流读取器,这意味着我们正在使⽤游标API。 迭代器API使⽤XMLEventReader类。 类名中还有⼀个奇怪的“ 2”,它表⽰Woodstox的StAX 2功能,其中之⼀可能是对验证的⽀持。 从
:
StAX2 is an experimental API that is intended to extend basic StAX specifications
in a way that allows implementations to experiment with features before they
end up in the actual StAX specification (if they do). As such, it is intended
to be freely implementable by all StAX implementations same way as StAX, but
without going through a formal JCP process. Currently Woodstox is the only
known implementation.
可以在“ enableValidationHandling”中看到
如果需要)。 我将重点介绍重要的部分。 ⾸先,加载XML模式:
XMLValidationSchema xmlValidationSchema = ateSchema(BigXmlTest.class
.getResource("/BigXmlTest.xsd"));
⽤于将可能的验证结果写⼊输出⽂件的回调;
public void reportProblem(XMLValidationProblem validationError) throws XMLValidationException {
validationResults.Message()
+ "Location:"
+ Location(),
ToStringStyle.SHORT_PREFIX_STYLE) + "\r\n");
}
“ openOutputFileAndWriteHeader”将创建⼀个XMLStreamWriter(它⼜是游标API的⼀部分,迭代器API具有XMLEventWriter),我们可以将其输出或原始XML⽂件的⼀部分。 它还将使⽤JAXB创建我们的标头,并将其写⼊输出。 默认情况下,使⽤Schema编译器(xjc)⽣成JAXB对象。
private XMLStreamWriter openOutputFileAndWriteHeader(int fileNumber) throws Exception {
XMLOutputFactory xmlOutputFactory = wInstance();
xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
XMLStreamWriter writer = ateXMLStreamWriter(new FileOutputStream(new File(System
.getProperty("pdir"), "BigXmlTest." + fileNumber + ".xml")));
writer.setDefaultNamespace(DOCUMENT_NS);
writer.writeStartDocument();
writer.writeStartElement(DOCUMENT_NS, BIGXMLTEST_ROOT_ELEMENT);
writer.writeDefaultNamespace(DOCUMENT_NS);
HeaderType header = ateHeaderType();
header.setSomeHeaderElement("Something something darkside");
marshaller.marshal(new JAXBElement<HeaderType>(new QName(DOCUMENT_NS, HEADER_ELEMENT, ""), HeaderType.class,
HeaderType.class, header), writer);
writer.writeStartElement(CONTENT_ELEMENT);
return writer;
}
在第3⾏,我们启⽤“修复名称空间”。 规格说明如下:
Function: Creates default prefixes and associates them with Namespace URIs.
Type: Boolean
Default Value: False
Required: Yes
我从中了解到,处理默认名称空间是必需的。 事实是,如果未启⽤,则不会以任何⽅式编写默认名称空间。 在第6⾏,我们设置默认名称
空间。 设置它实际上不会将其写⼊流。 因此,需要writeDefaultNamespace(第9⾏),但这只能在写⼊start元素之后才能完成。 因
此,您必须在编写任何元素之前定义默认名称空间,但是您需要在编写第⼀个元素之后编写默认名称空间。 理由是StAX需要知道它是否必
须为要写yes或no的根元素⽣成前缀。
在第8⾏,我们编写了root元素。 指⽰此元素所属的名称空间很重要。 如果您未指定前缀,则会为您⽣成⼀个前缀,或者,在本例中,将
不会⽣成任何前缀,因为StAX知道我们已经设置了默认名称空间。 如果您要删除第6⾏的默认名称空间指⽰,则将为根元素添加前缀(带
有随机前缀),例如:<wstxns1:BigXmlTest xmlns:wstxns1 =“ http:// www ...接下来,我们编写默认名称空间,它将被写⼊先
前开始的元素(顺便说⼀句,为了对此顺序有更深⼊的了解,请参阅这篇不错的 )在第11-14⾏中,我们使⽤JAXB⽣成的模型创建标头,
然后让我们的JAXB marshaller直接将其写到我们的StAX输出流。
重要提⽰: JAXB编组器以⽚段模式初始化,否则它将开始添加XML声明,这对于独⽴⽂档是必需的,当然,在现有⽂档中间是不允许
重要提⽰:
的:
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
附带说明⼀下:在此⽰例中,JAXB集成并不是真正有⽤,它会增加复杂性并占⽤更多代码⾏,然后仅使⽤XMLStreamWriter添加元素即
可。 但是,如果您有⼀个更复杂的结构需要创建并合并到⽂档中,则具有⾃动对象映射⾮常⽅便。
因此,我们有启⽤验证的阅读器。 从我们开始遍历源⽂档的那⼀刻起,它将同时验证和解析。 然后,我们的writer已经编写了⼀个初始化
的⽂档和标头,并准备接受更多数据。 最后,我们必须遍历源代码并将每个部分写⼊输出⽂件。 如果输出⽂件变⼤,我们将换⼀个新⽂
件:
while (xmlStreamReader.hasNext()) {
<();
if (EventType() == XMLEvent.START_ELEMENT
&& LocalName().equals(DATA_ELEMENT)) {
if (dataRepetitions != 0 && dataRepetitions % 2 == 0) { // %2 = just for testing: replace this by for example checking the actual size of the current output file xmlStreamWriter.close(); // Also closes any open Element(s) and the document
xmlStreamWriter = openOutputFileAndWriteHeader(++fileNumber); // Continue with next file
dataRepetitions = 0;
}
// Transform the input stream at current position to the output stream
new FragmentXMLStreamWriterWrapper(new AvoidDefaultNsPrefixStreamWriterWrapper(xmlStreamWriter, DOCUMENT_NS))));
dataRepetitions++;
}
}
重要的⼀点是,我们不断迭代源⽂档,并检查是否存在Data元素的开头。 如果是这样,我们将相应的元素及其同级元素流式传输到输出。
在我们的简单⽰例中,我们没有兄弟妹,只有⽂本值。 但是,如果结构更复杂,则所有基础节点将⾃动复制到输出中。 每隔两个数据元
素,我们将循环输出⽂件。 关闭编写器,并初始化⼀个新的编写器(当然,可以通过检查⽂件⼤⼩⽽不是%2来代替此检查)。 如果作家
是关闭的,它将⾃动处理关闭打开的元素并最终关闭⽂档本⾝,⽽⽆需您⾃⼰这样做。 作为将节点从输⼊流传输到输出的机制,需要注意
以下⼏点:
由于验证,我们不得不使⽤游标API,因此必须使⽤XSLT将节点及其兄弟节点传输到输出。 XSLT具有⼀些默认模板,如果您未专门指定XSL,则将调⽤这些模板。 在这种情况下,它将输⼊转换为给定的输出。
需要⼀个⾃定义的 ,我在JavaDoc中对此进⾏了记录。 再次将这个包装器包装在 。 最后⼀个原因是默认的XSLT模板⽆法识别源⽂档中的默认名称空间。 ⼀分钟内提供更多信息(或搜索避免使⽤DefaultDefaultNsPrefixStreamWriterWrapper)。
您使⽤的转换器必须是Oracle JDK的内部版本。 在初始化转换器的地⽅,我们直接引⽤内部TransformerFactory的实例:
apache.xalan.ax.TransformerFactoryImpl然后创建正确的转换器: Transformer = new apache.xalan.ax.TransformerFactoryImpl
TransformerFactoryImpl()。newTransformer(); 通常,您将使⽤wInstance()并使⽤classpath 上可⽤的转换器。 但是,解析器和转换器可以通过提供META-INF /服务来安装⾃⼰。 如果另⼀个转换器(例如默认的Xalan,⽽不是重新打包的JDK版本)将在类路径上,则转换将失败。 原因是显然只有JDK内部版本才可以从StAXSource转换为StAXResult
转换器实际上将让我们的XMLStreamReader在迭代过程中继续。 因此,在处理完⼀个数据元素之后,理论上阅读器的光标将在下⼀
个数据元素处就绪。 从理论上讲,如果格式化XML,则下⼀个事件类型可能是空格。 因此,在下⼀个Data元素实际准备就绪之前,它仍可能需要在while循环中对()进⾏⼀些迭代。
结果是我们有3个输出⽂件,每个输出⽂件都符合原始架构,每个⽂件都有2个数据元素:
要将⼤约30GB的XML(我在说我的原始⼯作分配XML具有更复杂的结构,⽽不是此处使⽤的演⽰XSD)拆分为⼤约500MB的部分,并花费了⼤约25分钟的时间。 为了测试内存使⽤率,我特意将Xmx设置为32MB。 从图中可以看出,内存消耗⾮常低,并且没有GV开销:⽣活是美好的,但并⾮完全如此。 在那⼉,我发现有些尴尬的事情需要⼩⼼。
在我的实际场景中,输⼊XML没有与之关联的名称空间,我很确定它永远不会。 这就是我坚持使⽤此解决⽅案的原因。 在演⽰中,这⾥只有⼀个名称空间,并且已经开始使设置更加脆弱。 问题不在于StAX:使⽤StAX处理名称空间⾮常简单。 您可以决定具有⼀个与该模式的⽬标名称空间相对应的默认名称空间(假设您的模式为elementFormDefault = qualified),并可以为该模式中导⼊的其他名称空间声明⼀些带前缀的名称空间。 当XSLT开始⼲扰输出流时,问题就开始出现(您可能已经注意到了)。 显然,它不会检查已经定义了哪些名称空间或发⽣其他事情。
结果是,它们通过使⽤其他前缀重新定义现有名称空间或重置默认名称空间和其他不需要的内容,使
⽂档严重混乱。 如果您需要⽐默认模板更多的名称空间操作,则可能需要XSL。 如果输⼊⽂档使⽤默认名称空间,则XSLT也会触发异常。 它将尝试注册名称为“ xmlns”的前缀。 不允许这样做,因为xmlns保留⽤于指⽰默认名称空间,不能⽤作前缀。 我为此测试申请的解决⽅案是忽略任何前缀“ xmlns”,并忽略与xmlns前缀组合的⽬标名称空间的添加(这就是为什么要使⽤避免DefaultNsPrefixStreamWriterWrapper)。 前缀和名称空间都需要在PreventDefaultNsPrefixStreamWriterWrapper中进⾏匹配,因为如果您要使⽤的输⼊⽂档中没有默认名称空间,⽽是带有前缀(例如<bigxml:BigXmlTest xmlns:bigxml =“ http://…。”> <bigxml:Header …。),那么您就不能忽略添加名称空间(该组合将成为带有“ bigxml”前缀的⽬标名称空间),因为这只会产⽣数据元素的前缀⽽没有名称空间绑定,例如:
<?xml version='1.0' encoding='UTF-8'?>
<BigXmlTest xmlns="be/bigxmltest">
<Header>
<SomeHeaderElement>Something something darkside</SomeHeaderElement>
</Header>
<Content>
<bigxml:Data>Data1</bigxml:Data>
<bigxml:Data>Data2</bigxml:Data>
</Content>
</BigXmlTest>
请记住,XML的⽣产者可以⾃由选择(还是在elementFormDefault =合格的情况下)选择使⽤默认命名空间还是为每个元素添加前缀。该代码应该透明地能够处理这两种情况。 为⽅便起见,请使⽤PreventDefaultNsPrefixStreamWriterWrapper代码:
public class AvoidDefaultNsPrefixStreamWriterWrapper extends XMLStreamWriterAdapter {
...
@Override
public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {
if (defaultNs.equals(namespaceURI) && "xmlns".equals(prefix)) {
return;
}
super.writeNamespace(prefix, namespaceURI);
}
@Override
public void setPrefix(String prefix, String uri) throws XMLStreamException {
if (prefix.equals("xmlns")) {
return;
}
super.setPrefix(prefix, uri);
}
最后,我还写了⼀个版本(点击
适⽤于GitHub),但这次使⽤的是StAX迭代器API。 您会注意到,不再需要繁琐的XSLT来流传输到输出。 只需将每个感兴趣的事件添加到输出中即可。 通过⾸先使⽤游标API验证输⼊,然后使⽤Iterator API解析输⼊,可以解决缺少验证的问题。 这将花费更长的时间,但是在⼤多数情况下仍然可以接受。 最重要的是:
while (xmlEventReader.hasNext()) {
XMLEvent event = Event();
if (event.isStartElement() && event.asStartElement().getName().getLocalPart().equals(CONTENT_ELEMENT)) {
event = Event();
while (!(event.isEndElement() && event.asEndElement().getName().getLocalPart()
.equals(CONTENT_ELEMENT))) {
if (dataRepetitions != 0 && event.isStartElement()
&& event.asStartElement().getName().getLocalPart().equals(DATA_ELEMENT)
&& dataRepetitions % 2 == 0) { // %2 = just for testing: replace this by for example checking the actual size of the current
// output file
xmlEventWriter.close(); // Also closes any open Element(s) and the document
xmlEventWriter = openOutputFileAndWriteHeader(++fileNumber); // Continue with next file
dataRepetitions = 0;
}
// Write the current event to output
xmlEventWriter.add(event);
event = Event();
if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(DATA_ELEMENT)) {
dataRepetitions++;
}
}
}
}
在第2⾏,您将看到返回XMLEvent,其中包含有关当前节点的所有信息。 在第4⾏上,您看到使⽤此表单检查元素类型更容易(与其与常量进⾏⽐较,还可以使⽤对象模型)。 在第19⾏,要将元素从输⼊复制到输出,我们只需将Event添加到XMLEventWriter。
参考:来⾃博客的 Koen Serneels 。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论