java文件和xml文件_用Java分割大型XML文件
java文件和xml文件
上周,我被要求用Java編寫一些東西,該東西能夠?qū)⒁粋€30GB的XML文件拆分為可配置文件大小的較小部分。 文件的使用者將是一個中間件應(yīng)用程序,該應(yīng)用程序在XML的大尺寸方面存在問題。 在幕后,它使用某種DOM解析技術(shù),使它在一段時間后耗盡內(nèi)存。 由于它是基于供應(yīng)商的中間件,因此我們無法自行糾正。 最好的選擇是創(chuàng)建一些預(yù)處理工具,該工具會先將大文件分成多個較小的塊,然后再由中間件處理。
XML文件帶有一個相應(yīng)的W3C模式,該模式由強(qiáng)制性頭部分和緊隨其后嵌套有多個0 .. *數(shù)據(jù)元素的內(nèi)容元素組成。 對于演示代碼,我以簡化形式重新創(chuàng)建了架構(gòu):
標(biāo)頭的大小可以忽略。 單個數(shù)據(jù)元素的重復(fù)也非常小,可以說少于50kB。 由于數(shù)據(jù)元素重復(fù)的次數(shù),XML太大了。 要求是:
- 分割后的XML的每一部分都應(yīng)是語法上有效的XML,并且每一部分還應(yīng)針對原始模式進(jìn)行驗證
- 該工具應(yīng)根據(jù)架構(gòu)驗證XML,并報告所有驗證錯誤。 驗證不得阻塞,且非驗證元素或?qū)傩圆坏迷谳敵鲋刑^
- 對于標(biāo)頭,決定將其復(fù)制到每個新的輸出文件中,而不是將其復(fù)制到每個新的輸出文件中,并為每個新的輸出文件重新生成標(biāo)頭,并提供一些處理信息和一些默認(rèn)設(shè)置
因此,使用諸如Unix Split之類的二進(jìn)制拆分工具是不可能的。 在固定數(shù)量的字節(jié)之后,這將拆分,從而確保XML確實損壞。 我不太確定,但是Split等工具也不了解編碼。 因此,在字節(jié)“ x”之后進(jìn)行拆分不僅會導(dǎo)致在XML元素的中間進(jìn)行拆分(例如),而且甚至?xí)谧址幋a序列的中間進(jìn)行拆分(例如,在使用經(jīng)過UTF8編碼的Unicode時)。 顯然,我們需要更智能的東西。
XSLT作為核心技術(shù)也是行不通的。 乍一看,您可能會很想:使用XSLT2.0,可以從單個輸入文件創(chuàng)建多個輸出文件。 甚至可以在轉(zhuǎn)換時驗證輸入文件。 但是,細(xì)節(jié)始終像魔鬼一樣。 否則,在Java中進(jìn)行簡單的操作(例如將驗證錯誤寫入單獨的文件或檢查當(dāng)前輸出文件的大小)可能需要自定義Java代碼。 對于Xalan和Saxon來說,當(dāng)然可以有這樣的擴(kuò)展,但是Xalan不是XSLT2.0實現(xiàn),因此只剩下Saxon。 最后但并非最不重要的一點是,XSLT1.0 / 2.0是非流式的,這意味著它們會將整個源文檔讀入內(nèi)存,因此這顯然將XSLT排除在了可能性之外。
剩下的唯一選擇就是Java XML解析。 在這種情況下,當(dāng)然理想的選擇是StAX。 我不在這里進(jìn)行SAX與StAX的比較,事實是StAX能夠針對架構(gòu)的身份進(jìn)行驗證(至少某些解析器可以)并且還可以編寫XML。 此外,與SAX相比,API的使用要容易得多,因為基于pull的API提供了更多的迭代文檔控制,并且比SAX的推送方式更令人愉快。 好的,我們需要什么:
- 能夠驗證XML的StAX實現(xiàn)
- 默認(rèn)情況下,Oracle的JDK附帶SJSXP作為StAX實現(xiàn),但是此驗證無效。
- 最好具有某種對象/ XML映射技術(shù),用于(重新)創(chuàng)建標(biāo)頭,而不是手動擺弄元素并必須查找正確的數(shù)據(jù)類型/格式
- 顯然是JAXB。
該代碼有點大,無法在此處完整顯示。 源文件,XSD和測試XML都可以訪問
了這里 GitHub上。 它具有Maven pom文件,因此您應(yīng)該能夠在選擇的IDE中將其導(dǎo)入。 JAXB綁定編譯器將自動編譯模式,并將生成的源放在類路徑上。
第一行創(chuàng)建了StAX流讀取器,這意味著我們正在使用游標(biāo)API。 迭代器API使用XMLEventReader類。 類名中還有一個奇怪的“ 2”,它表示W(wǎng)oodstox的StAX 2功能,其中之一可能是對驗證的支持。 從
在這里 :
可以在“ enableValidationHandling”中看到
源文件(如果需要)。 我將重點介紹重要的部分。 首先,加載XML模式:
用于將可能的驗證結(jié)果寫入輸出文件的回調(diào);
public void reportProblem(XMLValidationProblem validationError) throws XMLValidationException {validationResults.write(validationError.getMessage()+ "Location:"+ ToStringBuilder.reflectionToString(validationError.getLocation(),ToStringStyle.SHORT_PREFIX_STYLE) + "\r\n");}“ openOutputFileAndWriteHeader”將創(chuàng)建一個XMLStreamWriter(它又是游標(biāo)API的一部分,迭代器API具有XMLEventWriter),我們可以將其輸出或原始XML文件的一部分。 它還將使用JAXB創(chuàng)建我們的標(biāo)頭,并將其寫入輸出。 默認(rèn)情況下,使用Schema編譯器(xjc)生成JAXB對象。
private XMLStreamWriter openOutputFileAndWriteHeader(int fileNumber) throws Exception {XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);XMLStreamWriter writer = xmlOutputFactory.createXMLStreamWriter(new FileOutputStream(new File(System.getProperty("java.io.tmpdir"), "BigXmlTest." + fileNumber + ".xml")));writer.setDefaultNamespace(DOCUMENT_NS);writer.writeStartDocument();writer.writeStartElement(DOCUMENT_NS, BIGXMLTEST_ROOT_ELEMENT);writer.writeDefaultNamespace(DOCUMENT_NS);HeaderType header = objectFactory.createHeaderType();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行,我們啟用“修復(fù)名稱空間”。 規(guī)格說明如下:
javax.xml.stream.isRepairingNamespaces: Function: Creates default prefixes and associates them with Namespace URIs. Type: Boolean Default Value: False Required: Yes我從中了解到,處理默認(rèn)名稱空間是必需的。 事實是,如果未啟用,則不會以任何方式編寫默認(rèn)名稱空間。 在第6行,我們設(shè)置默認(rèn)名稱空間。 設(shè)置它實際上不會將其寫入流。 因此,需要writeDefaultNamespace(第9行),但這只能在寫入start元素之后才能完成。 因此,您必須在編寫任何元素之前定義默認(rèn)名稱空間,但是您需要在編寫第一個元素之后編寫默認(rèn)名稱空間。 理由是StAX需要知道它是否必須為要寫yes或no的根元素生成前綴。
在第8行,我們編寫了root元素。 指示此元素所屬的名稱空間很重要。 如果不指定前綴,則會為您生成一個前綴,或者,在本例中,將不會生成任何前綴,因為StAX知道我們已經(jīng)設(shè)置了默認(rèn)名稱空間。 如果您要刪除第6行的默認(rèn)名稱空間的指示,則將為根元素加上前綴(帶有隨機(jī)前綴),例如:<wstxns1:BigXmlTest xmlns:wstxns1 =“ http:// www…接下來,我們編寫默認(rèn)的名稱空間,它將被寫入先前開始的元素(順便說一句,為了對此順序有更深入的了解,請參閱這篇不錯的文章 )在第11-14行中,我們使用JAXB生成的模型創(chuàng)建標(biāo)頭,然后讓我們的JAXB編組器直接將其寫到StAX輸出流。
重要提示: JAXB編組器以片段模式初始化,否則它將開始添加XML聲明,這對于獨立文檔是必需的,當(dāng)然,在現(xiàn)有文檔的中間是不允許的:
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);附帶說明一下:在此示例中,JAXB集成并不是真正有用的,它會增加復(fù)雜性并占用更多代碼行,然后僅使用XMLStreamWriter添加元素即可。 但是,如果您有一個更復(fù)雜的結(jié)構(gòu)需要創(chuàng)建并合并到文檔中,則具有自動對象映射非常方便。
因此,我們有啟用驗證的閱讀器。 從我們開始遍歷源文檔的那一刻起,它將同時驗證和解析。 然后,我們的writer已經(jīng)編寫了一個初始化的文檔和標(biāo)頭,并準(zhǔn)備接受更多數(shù)據(jù)。 最后,我們必須遍歷源代碼并將每個部分寫入輸出文件。 如果輸出文件變大,我們將換一個新文件:
while (xmlStreamReader.hasNext()) {xmlStreamReader.next();if (xmlStreamReader.getEventType() == XMLEvent.START_ELEMENT&& xmlStreamReader.getLocalName().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 filexmlStreamWriter.close(); // Also closes any open Element(s) and the documentxmlStreamWriter = openOutputFileAndWriteHeader(++fileNumber); // Continue with next filedataRepetitions = 0;}// Transform the input stream at current position to the output streamtransformer.transform(new StAXSource(xmlStreamReader), new StAXResult(new FragmentXMLStreamWriterWrapper(new AvoidDefaultNsPrefixStreamWriterWrapper(xmlStreamWriter, DOCUMENT_NS))));dataRepetitions++;} }重要的一點是,我們不斷迭代源文檔,并檢查是否存在Data元素的開頭。 如果是這樣,我們將相應(yīng)的元素及其同級元素流式傳輸?shù)捷敵觥?在我們的簡單示例中,我們沒有兄弟姐妹,只有文本值。 但是,如果結(jié)構(gòu)更復(fù)雜,則所有基礎(chǔ)節(jié)點將自動復(fù)制到輸出中。 每隔兩個數(shù)據(jù)元素,我們將循環(huán)輸出文件。 關(guān)閉編寫器,并初始化一個新的編寫器(當(dāng)然可以通過檢查文件大小而不是%2代替此檢查)。 如果關(guān)閉了編寫器,它將自動處理關(guān)閉打開的元素并最終關(guān)閉文檔本身,而無需您自己執(zhí)行。 作為將節(jié)點從輸入流傳輸?shù)捷敵龅臋C(jī)制,需要注意以下幾點:
- 由于驗證,我們不得不使用游標(biāo)API,因此必須使用XSLT將節(jié)點及其兄弟節(jié)點傳輸?shù)捷敵觥?XSLT具有一些默認(rèn)模板,如果您未專門指定XSL,則將調(diào)用這些模板。 在這種情況下,它將輸入轉(zhuǎn)換為給定的輸出。
- 需要一個自定義的FragmentXMLStreamWriterWrapper ,我在JavaDoc中對此進(jìn)行了記錄。 再次將這個包裝器包裝在PreventDefaultNsPrefixStreamWriterWrapper中 。 最后一個原因是默認(rèn)的XSLT模板無法識別源文檔中的默認(rèn)名稱空間。 一分鐘內(nèi)可以找到更多信息(或搜索避免使用DefaultDefaultNsPrefixStreamWriterWrapper)。
- 您使用的轉(zhuǎn)換器必須是Oracle JDK的內(nèi)部版本。 在初始化轉(zhuǎn)換器的地方,我們直接引用內(nèi)部TransformerFactory的實例: com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl然后創(chuàng)建正確的轉(zhuǎn)換器: Transformer = new TransformerFactoryImpl()。newTransformer(); 通常,您將使用TransformerFactory.newInstance()并使用classpath上可用的轉(zhuǎn)換器。 但是,解析器和轉(zhuǎn)換器可以通過提供META-INF /服務(wù)來自行安裝。 如果另一個轉(zhuǎn)換器(例如默認(rèn)的Xalan,而不是重新打包的JDK版本)將在類路徑上,則轉(zhuǎn)換將失敗。 原因是顯然只有JDK內(nèi)部版本才可以從StAXSource轉(zhuǎn)換為StAXResult
- 轉(zhuǎn)換器實際上將讓我們的XMLStreamReader在迭代過程中繼續(xù)。 因此,在處理完一個Data元素之后,理論上閱讀器的光標(biāo)將在下一個Data元素就緒。 從理論上講,如果格式化您的XML,則下一個事件類型可能是空格。 因此,在下一個Data元素實際準(zhǔn)備就緒之前,它仍可能需要在while循環(huán)中的xmlStreamReader.next()上進(jìn)行一些迭代。
結(jié)果是我們有3個輸出文件,每個輸出文件都符合原始架構(gòu),每個文件都有2個數(shù)據(jù)元素:
要將大約30GB的XML(我在說我的原始工作分配XML具有更復(fù)雜的結(jié)構(gòu),而不是此處使用的演示XSD)拆分為大約500MB的部分,并進(jìn)行了大約25分鐘的驗證。 為了測試內(nèi)存使用率,我特意將Xmx設(shè)置為32MB。 從圖中可以看出,內(nèi)存消耗非常低,并且沒有GV開銷: 生活是美好的,但并非完全如此。 在那兒,我發(fā)現(xiàn)有些尷尬的事情需要謹(jǐn)慎對待。
在我的實際場景中,輸入XML沒有與之關(guān)聯(lián)的名稱空間,我很確定它永遠(yuǎn)不會。 這就是我堅持使用此解決方案的原因。 在演示中,這里只有一個名稱空間,并且已經(jīng)開始使設(shè)置更加脆弱。 問題不在于StAX:使用StAX處理名稱空間非常簡單。 您可以決定具有一個與該模式的目標(biāo)名稱空間相對應(yīng)的默認(rèn)名稱空間(假設(shè)您的模式為elementFormDefault = qualified),并可以為該模式中導(dǎo)入的其他名稱空間聲明一些帶前綴的名稱空間。 當(dāng)XSLT開始干擾輸出流時,問題就開始出現(xiàn)(您可能已經(jīng)注意到了)。 顯然,它不會檢查已經(jīng)定義了哪些名稱空間或發(fā)生其他事情。
結(jié)果是,它們通過使用其他前綴重新定義現(xiàn)有名稱空間或重置默認(rèn)名稱空間和其他不需要的內(nèi)容,使文檔嚴(yán)重混亂。 如果您需要比默認(rèn)模板更多的名稱空間操作,則可能需要XSL。 如果輸入文檔使用默認(rèn)名稱空間,則XSLT也會觸發(fā)異常。 它將嘗試注冊名稱為“ xmlns”的前綴。 不允許這樣做,因為xmlns保留用于指示它不能用作前綴的默認(rèn)名稱空間。 我為此測試應(yīng)用的解決方案是忽略任何前綴“ xmlns”,并忽略與xmlns前綴組合的目標(biāo)名稱空間的添加(這就是為什么我們要避免使用DefaultDefaultNsPrefixStreamWriterWrapper)。 前綴和名稱空間都需要在PreventDefaultNsPrefixStreamWriterWrapper中進(jìn)行匹配,因為如果您要輸入的文檔沒有默認(rèn)名稱空間,而是帶有前綴(例如<bigxml:BigXmlTest xmlns:bigxml =“ http://…。”> <bigxml:Header …。),那么您就不能忽略添加名稱空間(該組合將成為帶有“ bigxml”前綴的目標(biāo)名稱空間),因為這只會產(chǎn)生數(shù)據(jù)元素的前綴而沒有名稱空間綁定,例如:
<?xml version='1.0' encoding='UTF-8'?> <BigXmlTest xmlns="http://www.error.be/bigxmltest"><Header><SomeHeaderElement>Something something darkside</SomeHeaderElement></Header><Content><bigxml:Data>Data1</bigxml:Data><bigxml:Data>Data2</bigxml:Data></Content> </BigXmlTest>請記住,XML的生產(chǎn)者可以自由選擇(還是在elementFormDefault =合格的情況下)選擇使用defaultnamespace還是為每個元素添加前綴。 該代碼應(yīng)該透明地能夠處理這兩種情況。 為方便起見,避免使用DefaultDefaultNsPrefixStreamWriterWrapper代碼:
public class AvoidDefaultNsPrefixStreamWriterWrapper extends XMLStreamWriterAdapter { ...@Overridepublic void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {if (defaultNs.equals(namespaceURI) && "xmlns".equals(prefix)) {return;}super.writeNamespace(prefix, namespaceURI);}@Overridepublic void setPrefix(String prefix, String uri) throws XMLStreamException {if (prefix.equals("xmlns")) {return;}super.setPrefix(prefix, uri);} 最后,我還寫了一個版本(點擊
此處完全相同),但這次使用StAX迭代器API。 您會注意到,不再需要繁瑣的XSLT來流傳輸?shù)捷敵觥?只需將每個感興趣的事件添加到輸出中即可。 通過首先使用游標(biāo)API驗證輸入,然后使用Iterator API解析輸入,可以解決缺少驗證的問題。 這將花費更長的時間,但是在大多數(shù)情況下仍然可以接受。 最重要的部分:
在第2行,您將看到返回XMLEvent,其中包含有關(guān)當(dāng)前節(jié)點的所有信息。 在第4行上,您看到使用此表單檢查元素類型更容易(可以使用對象模型來代替與常量的比較)。 在第19行,要將元素從輸入復(fù)制到輸出,我們只需將Event添加到XMLEventWriter。
翻譯自: https://www.javacodegeeks.com/2013/08/splitting-large-xml-files-in-java.html
java文件和xml文件
總結(jié)
以上是生活随笔為你收集整理的java文件和xml文件_用Java分割大型XML文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为g520移动版(华为g520手机参数
- 下一篇: 创新设计模式:抽象工厂模式