在java中将XML文件转换为CSV

@Before可能会有一些重复的问题建议,我不认为是这种情况可能首先阅读,我会尽量做到尽可能简短。 标题给出了基本概念。

这是一个示例XML(案例1):

  4504216603  10:00:10.000Z 10:00:30.000Z http://url ....  

这是一个示例XML(案例2):

   4504216604  10:30:10.000Z   value1 9823 9112 value2   11:00:10.000Z http://url ....   

我从谷歌借用了这个XML,反正我的对象并不总是一样,有时还有像case2这样的额外元素。 现在我想从这两种情况中生成这样的CSV:

 ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice 4504216603,10:00:10.000Z,10:00:30.000Z,http://url 4504216604,10:30:10.000Z,11:00:10.000Z,http://url,value1,value2 

第一行是标题,它也应该包含在csv中。 我今天得到了一些有用的链接到stax,我真的不知道什么是正确的/最佳的方法,我现在正在努力3天,不是真的愿意放弃。

告诉我你的想法你会如何解决这个问题

我忘了提到这是一个非常庞大的xml文件,最高可达1GB

BOUNTY UPDATE:

我正在寻找更多的通用方法,这意味着这应该适用于任何数量的任何深度的节点,有时在示例xml中,可能会发生一个item对象具有比下一个/前一个更多的节点,所以也应该有这种情况(因此所有列和值都以CSV格式匹配)。

此外,可能会发生节点具有相同名称/ localName但不同的值和属性,如果是这种情况,则新列应显示为具有适当值的CSV。 (我在标签中添加了这个案例的例子,称为category

提供的代码应该被视为草图而不是权威性文章。 我不是SAX的专家,可以改进实现以获得更好的性能,更简单的代码等。那就是说SAX应该能够处理流式传输大型XML文件。

我会使用SAX解析器进行2次传递来解决这个问题。 (顺便说一句,我也会使用CSV生成库来创建输出,因为这将处理CSV涉及的所有繁琐的字符转义,但我没有在我的草图中实现这一点)。

第一遍:建立标题列数

第二遍:输出CSV

我假设XML文件格式正确。 我假设我们没有具有预定义顺序的方案/ DTD。

在第一遍中,我假设将为包含文本内容或任何属性的每个XML元素添加CSV列(我假设属性将包含某些内容!)。

已建立目标列数的第二遍将执行实际的CSV输出。

基于您的示例XML,我的代码草图将产生:

 ItemID,StartTime,EndTime,ViewItemURL,AverageTime,category,category,type,type,AveragePrice 4504216603,10:00:10.000Z,10:00:30.000Z,http://url,,,,,, 4504216604,10:30:10.000Z,11:00:10.000Z,http://url,value1,9823,9112,TX,TY,value2 

请注意我使用了google集合LinkedHashMultimap,因为这在将多个值与单个键相关联时很有用。 希望这个对你有帮助!

 import com.google.common.collect.LinkedHashMultimap; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map.Entry; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; public class App { public static void main(String[] args) throws SAXException, FileNotFoundException, IOException { // First pass - to determine headers XMLReader xr = XMLReaderFactory.createXMLReader(); HeaderHandler handler = new HeaderHandler(); xr.setContentHandler(handler); xr.setErrorHandler(handler); FileReader r = new FileReader("test1.xml"); xr.parse(new InputSource(r)); LinkedHashMap headers = handler.getHeaders(); int totalnumberofcolumns = 0; for (int headercount : headers.values()) { totalnumberofcolumns += headercount; } String[] columnheaders = new String[totalnumberofcolumns]; int i = 0; for (Entry entry : headers.entrySet()) { for (int j = 0; j < entry.getValue(); j++) { columnheaders[i] = entry.getKey(); i++; } } StringBuilder sb = new StringBuilder(); for (String h : columnheaders) { sb.append(h); sb.append(','); } System.out.println(sb.substring(0, sb.length() - 1)); // Second pass - collect and output data xr = XMLReaderFactory.createXMLReader(); DataHandler datahandler = new DataHandler(); datahandler.setHeaderArray(columnheaders); xr.setContentHandler(datahandler); xr.setErrorHandler(datahandler); r = new FileReader("test1.xml"); xr.parse(new InputSource(r)); } public static class HeaderHandler extends DefaultHandler { private String content; private String currentElement; private boolean insideElement = false; private Attributes attribs; private LinkedHashMap itemHeader; private LinkedHashMap accumulativeHeader = new LinkedHashMap(); public HeaderHandler() { super(); } private LinkedHashMap getHeaders() { return accumulativeHeader; } private void addItemHeader(String headerName) { if (itemHeader.containsKey(headerName)) { itemHeader.put(headerName, itemHeader.get(headerName) + 1); } else { itemHeader.put(headerName, 1); } } @Override public void startElement(String uri, String name, String qName, Attributes atts) { if ("item".equalsIgnoreCase(qName)) { itemHeader = new LinkedHashMap(); } currentElement = qName; content = null; insideElement = true; attribs = atts; } @Override public void endElement(String uri, String name, String qName) { if (!"item".equalsIgnoreCase(qName) && !"root".equalsIgnoreCase(qName)) { if (content != null && qName.equals(currentElement) && content.trim().length() > 0) { addItemHeader(qName); } if (attribs != null) { int attsLength = attribs.getLength(); if (attsLength > 0) { for (int i = 0; i < attsLength; i++) { String attName = attribs.getLocalName(i); addItemHeader(attName); } } } } if ("item".equalsIgnoreCase(qName)) { for (Entry entry : itemHeader.entrySet()) { String headerName = entry.getKey(); Integer count = entry.getValue(); //System.out.println(entry.getKey() + ":" + entry.getValue()); if (accumulativeHeader.containsKey(headerName)) { if (count > accumulativeHeader.get(headerName)) { accumulativeHeader.put(headerName, count); } } else { accumulativeHeader.put(headerName, count); } } } insideElement = false; currentElement = null; attribs = null; } @Override public void characters(char ch[], int start, int length) { if (insideElement) { content = new String(ch, start, length); } } } public static class DataHandler extends DefaultHandler { private String content; private String currentElement; private boolean insideElement = false; private Attributes attribs; private LinkedHashMultimap dataMap; private String[] headerArray; public DataHandler() { super(); } @Override public void startElement(String uri, String name, String qName, Attributes atts) { if ("item".equalsIgnoreCase(qName)) { dataMap = LinkedHashMultimap.create(); } currentElement = qName; content = null; insideElement = true; attribs = atts; } @Override public void endElement(String uri, String name, String qName) { if (!"item".equalsIgnoreCase(qName) && !"root".equalsIgnoreCase(qName)) { if (content != null && qName.equals(currentElement) && content.trim().length() > 0) { dataMap.put(qName, content); } if (attribs != null) { int attsLength = attribs.getLength(); if (attsLength > 0) { for (int i = 0; i < attsLength; i++) { String attName = attribs.getLocalName(i); dataMap.put(attName, attribs.getValue(i)); } } } } if ("item".equalsIgnoreCase(qName)) { String data[] = new String[headerArray.length]; int i = 0; for (String h : headerArray) { if (dataMap.containsKey(h)) { Object[] values = dataMap.get(h).toArray(); data[i] = (String) values[0]; if (values.length > 1) { dataMap.removeAll(h); for (int j = 1; j < values.length; j++) { dataMap.put(h, values[j]); } } else { dataMap.removeAll(h); } } else { data[i] = ""; } i++; } StringBuilder sb = new StringBuilder(); for (String d : data) { sb.append(d); sb.append(','); } System.out.println(sb.substring(0, sb.length() - 1)); } insideElement = false; currentElement = null; attribs = null; } @Override public void characters(char ch[], int start, int length) { if (insideElement) { content = new String(ch, start, length); } } public void setHeaderArray(String[] headerArray) { this.headerArray = headerArray; } } } 

这看起来像是使用XSL的好例子。 根据您的基本要求,与自定义解析器或序列化器相比,使用XSL可以更容易地获得正确的节点。 好处是您的XSL可以定位“// Item // AverageTime”或您需要的任何节点,而无需担心节点深度。

更新:以下是我扔在一起的xslt,以确保它按预期工作。

     ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice  ,,,,,     

我不确定我是否理解解决方案的通用性。 您是否真的想要为通用解决方案解析1 GB文件两次? 如果你想要通用的东西,你为什么跳过你的例子中的元素? 你需要处理多少种不同的格式? 你真的不知道格式是什么(即使一些元素可以被省略)? 你能澄清一下吗?

根据我的经验,通常最好以特定方式解析特定文件(但这并不排除使用通用API)。 我的答案将朝这个方向发展(我会在澄清后更新)。


如果您对XML不满意,可以考虑使用一些现有的(商业)库,例如Ricebridge XML Manager和CSV Manager 。 有关完整示例,请参阅如何使用Java将CSV转换为XML和XML转换为CSV 。 这种方法非常简单:使用XPath表达式定义数据字段(在您的情况下这是完美的,因为您可以使用“额外”元素),解析文件,然后将结果List传递给CSV组件以生成CSV文件。 API看起来很简单,测试代码(他们的测试用例的源代码在BSD风格的许可下可用),他们声称支持千兆字节大小的文件。

您可以获得170美元的单一开发人员许可证,与开发者每日费率相比并不是非常昂贵。

他们提供30天试用版,看看。


另一种选择是使用Spring Batch 。 Spring批处理提供了使用XML文件作为输入或输出(使用StAX和您选择的XML绑定框架)和平面文件作为输入或输出所需的一切。 看到:

  • Spring批处理文档
  • 样品 (特别是贸易样品)
  • 首先看一下Spring-Batch,第2部分

您还可以使用Smooks进行XML到CSV 转换 。 也可以看看:

  • 结构化事件流与Smooks

另一个选择是使用StAX解析器来滚动自己的解决方案,或者为什么不使用VTD-XML和XPath。 看一下:

  • VTD简介 – XML
  • 使用VTD-XML进行无架构Java-XML数据绑定

根据您描述的要求进行编码的最佳方法是使用FreeMarker和XML处理的简单function。 查看文档 。

在这种情况下,您只需要生成CSV的模板。

另一种方法是XMLGen ,但方法非常相似。 只需查看该图表和示例,您将输出CSV而不是SQL语句。

这两种类似的方法不是“传统的”,但是可以很快地完成你的工作,而且你不必学习XSL(我认为很难掌握)。

这里有一些代码使用StAX实现XML到CSV的转换。 虽然您提供的XML只是一个示例,但我希望这会向您展示如何处理可选元素。

 import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.*; public class App { public static void main( String[] args ) throws XMLStreamException, FileNotFoundException { new App().convertXMLToCSV(new BufferedInputStream(new FileInputStream(args[0])), new BufferedOutputStream(new FileOutputStream(args[1]))); } static public final String ROOT = "root"; static public final String ITEM = "Item"; static public final String ITEM_ID = "ItemID"; static public final String ITEM_DETAILS = "ListingDetails"; static public final String START_TIME = "StartTime"; static public final String END_TIME = "EndTime"; static public final String ITEM_URL = "ViewItemURL"; static public final String AVERAGES = "averages"; static public final String AVERAGE_TIME = "AverageTime"; static public final String AVERAGE_PRICE = "AveragePrice"; static public final String SEPARATOR = ","; public void convertXMLToCSV(InputStream in, OutputStream out) throws XMLStreamException { PrintWriter writer = new PrintWriter(out); XMLStreamReader xmlStreamReader = XMLInputFactory.newInstance().createXMLStreamReader(in); convertXMLToCSV(xmlStreamReader, writer); } public void convertXMLToCSV(XMLStreamReader xmlStreamReader, PrintWriter writer) throws XMLStreamException { writer.println("ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice"); xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ROOT); while (xmlStreamReader.hasNext()) { xmlStreamReader.nextTag(); if (xmlStreamReader.isEndElement()) break; xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ITEM); String itemID = nextValue(xmlStreamReader, ITEM_ID); xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ITEM_DETAILS); String startTime = nextValue(xmlStreamReader, START_TIME); xmlStreamReader.nextTag(); String averageTime = null; String averagePrice = null; if (xmlStreamReader.getLocalName().equals(AVERAGES)) { averageTime = nextValue(xmlStreamReader, AVERAGE_TIME); averagePrice = nextValue(xmlStreamReader, AVERAGE_PRICE); xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, AVERAGES); xmlStreamReader.nextTag(); } String endTime = currentValue(xmlStreamReader, END_TIME); String url = nextValue(xmlStreamReader,ITEM_URL); xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ITEM_DETAILS); xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ITEM); writer.append(esc(itemID)).append(SEPARATOR) .append(esc(startTime)).append(SEPARATOR) .append(esc(endTime)).append(SEPARATOR) .append(esc(url)); if (averageTime!=null) writer.append(SEPARATOR).append(esc(averageTime)).append(SEPARATOR) .append(esc(averagePrice)); writer.println(); } xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ROOT); writer.close(); } private String esc(String string) { if (string.indexOf(',')!=-1) string = '"'+string+'"'; return string; } private String nextValue(XMLStreamReader xmlStreamReader, String name) throws XMLStreamException { xmlStreamReader.nextTag(); return currentValue(xmlStreamReader, name); } private String currentValue(XMLStreamReader xmlStreamReader, String name) throws XMLStreamException { xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, name); String value = ""; for (;;) { int next = xmlStreamReader.next(); if (next==XMLStreamConstants.CDATA||next==XMLStreamConstants.SPACE||next==XMLStreamConstants.CHARACTERS) value += xmlStreamReader.getText(); else if (next==XMLStreamConstants.END_ELEMENT) break; // ignore comments, PIs, attributes } xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, name); return value.trim(); } } 

我不相信SAX是最适合你的方法。 但是,有不同的方法可以在这里使用SAX。

如果在某些元素(如ListingDetails)中无法保证元素顺序,那么您需要主动。

当您启动ListingDetails时,将地图初始化为处理程序上的成员变量。 在每个子元素中,在该映射中设置适当的键值。 完成ListingDetails后,检查映射并显式模拟缺少元素的值,例如null。 假设每个项目有一个ListingDetails,将其保存到处理程序中的成员变量。

现在,当您的item元素结束时,有一个函数可以按照您想要的顺序根据地图写入CSV行。

如果您的XML已损坏,则存在风险。 我强烈考虑在项目开始时将所有这些变量设置为null,然后检查错误并在项目结束时通知它们。

请注意,这将是使用XSLT的一个主要示例,除了大多数XSLT处理器将整个XML文件读入内存,因为它很大而不是一个选项。 但请注意,Saxon的企业版可以进行流式XSLT处理(如果XSLT脚本遵守限制)。

如果适用,您可能还希望在JVM外部使用外部XSLT处理器。 这开辟了几个选项。

Saxon-EE中的流媒体: http : //www.saxonica.com/documentation/sourcedocs/serial.html

您可以使用XStream( http://x-stream.github.io/ )或JOX( http://www.wutka.com/jox.html )识别xml,然后将其转换为Java Bean。 我认为你可以在获得bean后自动将Beans转换为CSV。