java文件和xml文件_用Java分割大型XML文件
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都可以訪問
了這里 GitHub上。 它具有Maven pom文件,因此您應該能夠在選擇的IDE中將其導入。 JAXB綁定編譯器將自動編譯模式,并將生成的源放在類路徑上。
第一行創建了StAX流讀取器,這意味著我們正在使用游標API。 迭代器API使用XMLEventReader類。 類名中還有一個奇怪的“ 2”,它表示Woodstox的StAX 2功能,其中之一可能是對驗證的支持。 從
在這里 :
可以在“ enableValidationHandling”中看到
源文件(如果需要)。 我將重點介紹重要的部分。 首先,加載XML模式:
用于將可能的驗證結果寫入輸出文件的回調;
public void reportProblem(XMLValidationProblem validationError) throws XMLValidationException {validationResults.write(validationError.getMessage()+ "Location:"+ ToStringBuilder.reflectionToString(validationError.getLocation(),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 = 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行,我們啟用“修復名稱空間”。 規格說明如下:
javax.xml.stream.isRepairingNamespaces: 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編組器直接將其寫到StAX輸出流。
重要提示: JAXB編組器以片段模式初始化,否則它將開始添加XML聲明,這對于獨立文檔是必需的,當然,在現有文檔的中間是不允許的:
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);附帶說明一下:在此示例中,JAXB集成并不是真正有用的,它會增加復雜性并占用更多代碼行,然后僅使用XMLStreamWriter添加元素即可。 但是,如果您有一個更復雜的結構需要創建并合并到文檔中,則具有自動對象映射非常方便。
因此,我們有啟用驗證的閱讀器。 從我們開始遍歷源文檔的那一刻起,它將同時驗證和解析。 然后,我們的writer已經編寫了一個初始化的文檔和標頭,并準備接受更多數據。 最后,我們必須遍歷源代碼并將每個部分寫入輸出文件。 如果輸出文件變大,我們將換一個新文件:
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元素的開頭。 如果是這樣,我們將相應的元素及其同級元素流式傳輸到輸出。 在我們的簡單示例中,我們沒有兄弟姐妹,只有文本值。 但是,如果結構更復雜,則所有基礎節點將自動復制到輸出中。 每隔兩個數據元素,我們將循環輸出文件。 關閉編寫器,并初始化一個新的編寫器(當然可以通過檢查文件大小而不是%2代替此檢查)。 如果關閉了編寫器,它將自動處理關閉打開的元素并最終關閉文檔本身,而無需您自己執行。 作為將節點從輸入流傳輸到輸出的機制,需要注意以下幾點:
- 由于驗證,我們不得不使用游標API,因此必須使用XSLT將節點及其兄弟節點傳輸到輸出。 XSLT具有一些默認模板,如果您未專門指定XSL,則將調用這些模板。 在這種情況下,它將輸入轉換為給定的輸出。
- 需要一個自定義的FragmentXMLStreamWriterWrapper ,我在JavaDoc中對此進行了記錄。 再次將這個包裝器包裝在PreventDefaultNsPrefixStreamWriterWrapper中 。 最后一個原因是默認的XSLT模板無法識別源文檔中的默認名稱空間。 一分鐘內可以找到更多信息(或搜索避免使用DefaultDefaultNsPrefixStreamWriterWrapper)。
- 您使用的轉換器必須是Oracle JDK的內部版本。 在初始化轉換器的地方,我們直接引用內部TransformerFactory的實例: com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl然后創建正確的轉換器: Transformer = new TransformerFactoryImpl()。newTransformer(); 通常,您將使用TransformerFactory.newInstance()并使用classpath上可用的轉換器。 但是,解析器和轉換器可以通過提供META-INF /服務來自行安裝。 如果另一個轉換器(例如默認的Xalan,而不是重新打包的JDK版本)將在類路徑上,則轉換將失敗。 原因是顯然只有JDK內部版本才可以從StAXSource轉換為StAXResult
- 轉換器實際上將讓我們的XMLStreamReader在迭代過程中繼續。 因此,在處理完一個Data元素之后,理論上閱讀器的光標將在下一個Data元素就緒。 從理論上講,如果格式化您的XML,則下一個事件類型可能是空格。 因此,在下一個Data元素實際準備就緒之前,它仍可能需要在while循環中的xmlStreamReader.next()上進行一些迭代。
結果是我們有3個輸出文件,每個輸出文件都符合原始架構,每個文件都有2個數據元素:
要將大約30GB的XML(我在說我的原始工作分配XML具有更復雜的結構,而不是此處使用的演示XSD)拆分為大約500MB的部分,并進行了大約25分鐘的驗證。 為了測試內存使用率,我特意將Xmx設置為32MB。 從圖中可以看出,內存消耗非常低,并且沒有GV開銷: 生活是美好的,但并非完全如此。 在那兒,我發現有些尷尬的事情需要謹慎對待。
在我的實際場景中,輸入XML沒有與之關聯的名稱空間,我很確定它永遠不會。 這就是我堅持使用此解決方案的原因。 在演示中,這里只有一個名稱空間,并且已經開始使設置更加脆弱。 問題不在于StAX:使用StAX處理名稱空間非常簡單。 您可以決定具有一個與該模式的目標名稱空間相對應的默認名稱空間(假設您的模式為elementFormDefault = qualified),并可以為該模式中導入的其他名稱空間聲明一些帶前綴的名稱空間。 當XSLT開始干擾輸出流時,問題就開始出現(您可能已經注意到了)。 顯然,它不會檢查已經定義了哪些名稱空間或發生其他事情。
結果是,它們通過使用其他前綴重新定義現有名稱空間或重置默認名稱空間和其他不需要的內容,使文檔嚴重混亂。 如果您需要比默認模板更多的名稱空間操作,則可能需要XSL。 如果輸入文檔使用默認名稱空間,則XSLT也會觸發異常。 它將嘗試注冊名稱為“ xmlns”的前綴。 不允許這樣做,因為xmlns保留用于指示它不能用作前綴的默認名稱空間。 我為此測試應用的解決方案是忽略任何前綴“ xmlns”,并忽略與xmlns前綴組合的目標名稱空間的添加(這就是為什么我們要避免使用DefaultDefaultNsPrefixStreamWriterWrapper)。 前綴和名稱空間都需要在PreventDefaultNsPrefixStreamWriterWrapper中進行匹配,因為如果您要輸入的文檔沒有默認名稱空間,而是帶有前綴(例如<bigxml:BigXmlTest xmlns:bigxml =“ http://…。”> <bigxml:Header …。),那么您就不能忽略添加名稱空間(該組合將成為帶有“ bigxml”前綴的目標名稱空間),因為這只會產生數據元素的前綴而沒有名稱空間綁定,例如:
<?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的生產者可以自由選擇(還是在elementFormDefault =合格的情況下)選擇使用defaultnamespace還是為每個元素添加前綴。 該代碼應該透明地能夠處理這兩種情況。 為方便起見,避免使用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來流傳輸到輸出。 只需將每個感興趣的事件添加到輸出中即可。 通過首先使用游標API驗證輸入,然后使用Iterator API解析輸入,可以解決缺少驗證的問題。 這將花費更長的時間,但是在大多數情況下仍然可以接受。 最重要的部分:
在第2行,您將看到返回XMLEvent,其中包含有關當前節點的所有信息。 在第4行上,您看到使用此表單檢查元素類型更容易(可以使用對象模型來代替與常量的比較)。 在第19行,要將元素從輸入復制到輸出,我們只需將Event添加到XMLEventWriter。
翻譯自: https://www.javacodegeeks.com/2013/08/splitting-large-xml-files-in-java.html
java文件和xml文件
總結
以上是生活随笔為你收集整理的java文件和xml文件_用Java分割大型XML文件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为g520移动版(华为g520手机参数
- 下一篇: 三星note7爆炸原因(三星note7会