【问题标题】:Convert XML file to CSV in java在java中将XML文件转换为CSV
【发布时间】:2011-03-18 15:03:20
【问题描述】:

@Before 可能会有一些重复的问题建议,我认为不是这种情况可能先阅读此内容,我会尽量简短。标题给出了基本思路。

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

<root>
      <Item>
        <ItemID>4504216603</ItemID>
        <ListingDetails>
          <StartTime>10:00:10.000Z</StartTime>
          <EndTime>10:00:30.000Z</EndTime>
          <ViewItemURL>http://url</ViewItemURL>
            ....
           </item>      

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

          <Item>
            <ItemID>4504216604</ItemID>
            <ListingDetails>
              <StartTime>10:30:10.000Z</StartTime>
              <!-- Start difference from case 1 -->
              <averages>
              <AverageTime>value1</AverageTime>
              <category type="TX">9823</category>
              <category type="TY">9112</category>
              <AveragePrice>value2</AveragePrice>
              </averages>
              <!-- End difference from case 1 -->
              <EndTime>11:00:10.000Z</EndTime>
              <ViewItemURL>http://url</ViewItemURL>
                ....
               </item>
                </root>

我从 google 借了这个 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

赏金更新:

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

也可能发生节点具有相同的名称/本地名称但不同的值和属性,如果是这种情况,那么新列应该以适当的值出现在 CSV 中。 (我在名为category&lt;averages&gt; 标签内添加了这种情况的示例)

【问题讨论】:

  • 值 value1, ... , valuen 是否总是 &lt;averages/&gt; 元素的直接子元素? averages 是唯一可能出现的元素吗?还是您需要更灵活地处理那里显示的内容?
  • @erickson 我更新了我的问题
  • @cOmrade:关于您的“更新”:如果不是第一个元素是列最多的元素,那么您只需要两次通过/步骤进行转换:在第一步中,您只需收集所有列,并在步骤 2 中按所述处理它们。如果没有找到特定节点的值节点,那么您可以放置​​一个您想要的值(null 或 empty 或您喜欢的任何约定 - 请参阅我在回答中的描述)。节点嵌套不是问题,因为对于 CSV,它们将是红色扁平的。
  • @A. Ionescu 感谢您的回复,我想我已经收集了足够的算法来解决这个问题,我什至已经实现了一个(无法正常工作)但现在我正在寻找一些代码,因此我正在寻找赏金。

标签: java xml stax


【解决方案1】:

所提供的代码应被视为草图而不是权威文章。我不是 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<String, Integer> headers = handler.getHeaders();
        int totalnumberofcolumns = 0;
        for (int headercount : headers.values()) {
            totalnumberofcolumns += headercount;
        }
        String[] columnheaders = new String[totalnumberofcolumns];
        int i = 0;
        for (Entry<String, Integer> 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<String, Integer> itemHeader;
        private LinkedHashMap<String, Integer> accumulativeHeader = new LinkedHashMap<String, Integer>();

        public HeaderHandler() {
            super();
        }

        private LinkedHashMap<String, Integer> 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<String, Integer>();
            }
            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<String, Integer> 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;
        }
    }
}

【讨论】:

  • 您知道如何在不明确定义“item”和“root”的情况下使其更通用一点吗? IE。没有诸如 !"item".equalsIgnoreCase(qName) && !"root" 之类的行
  • 嗨@toop 你总是可以根据树的深度来做,例如:stackoverflow.com/questions/6248322/…
【解决方案2】:

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

更新:以下是我拼凑的 xslt,以确保它按预期工作。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice
<xsl:for-each select="//Item">
<xsl:value-of select="ItemID"/><xsl:text>,</xsl:text><xsl:value-of select="//StartTime"/><xsl:text>,</xsl:text><xsl:value-of select="//EndTime"/><xsl:text>,</xsl:text><xsl:value-of select="//ViewItemURL"/><xsl:text>,</xsl:text><xsl:value-of select="//AverageTime"/><xsl:text>,</xsl:text><xsl:value-of select="//AveragePrice"/><xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>

【讨论】:

  • 尤其是“任意数量的任意深度的节点”的要求应该迫使人们将想法转向 XSL 和“//Item”。
  • 如果这是一个小文件,XSL 将是完美的选择,但是,1gb 文件的 DOM 可能会占用大量内存。所以我想需要使用某种专门的流式 XSL(这个线程已经提到了 Saxonica 和 VTD-XML)另见:stackoverflow.com/questions/2301926/xml-process-large-data
  • 这是一些有趣的信息。在这种情况下,流式 xsl 技术会很有用。感谢马克的链接。
【解决方案3】:

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

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


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

您可以花 170 美元获得单一开发者许可,与开发者每日费率相比,这并不是很贵。

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


另一种选择是使用Spring Batch。 Spring Batch 提供了使用XML files 作为input 或输出(使用StAX 和您选择的XML 绑定框架)和flat files 作为输入或output 所需的一切。见:


您还可以使用 Smooks 将 XML 转换为 CSV transformations。另见:


另一种选择是推出您自己的解决方案,使用 StAX 解析器,或者为什么不使用 VTD-XML 和 XPath。看看:

【讨论】:

    【解决方案4】:

    根据您描述的要求进行编码的最佳方法是使用 FreeMarker 和 XML 处理的简单功能。 See the docs.

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

    对此的替代方法是XMLGen,但方法非常相似。看看那个图表和例子,你会输出 CSV 而不是 SQL 语句。

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

    【讨论】:

      【解决方案5】:

      这里有一些使用 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();
          }    
      }
      

      【讨论】:

      • 感谢您的回复,我正在寻找更通用的方法,这意味着它应该适用于任意数量的任意深度的节点,有时就像在示例 xml 中一样,它可能会发生item 对象的节点数比下一个对象多,因此也应该有这种情况。也可能发生节点名称相同但值和属性不同的情况,CSV 中的新列也是如此。
      【解决方案6】:

      我不相信 SAX 是最适合您的方法。 不过,您可以通过多种方式在这里使用 SAX。

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

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

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

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

      【讨论】:

        【解决方案7】:

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

        如果适用,您可能还想在 JVM 之外使用外部 XSLT 处理器。这为更多选项打开了大门。

        Saxon-EE 中的流式传输:http://www.saxonica.com/documentation/sourcedocs/serial.html

        【讨论】:

        • 还有 Joost/STX joost.sourceforge.net,它是一种类似 XSLT 的语言,对流式传输有一些额外的限制。由于这个问题只需要对输入进行顺序处理,它应该很适合该模型。
        • 为什么只是 XSLT-like 而不是 XSLT 子集?
        【解决方案8】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-02-20
          • 2016-07-21
          • 2011-03-05
          • 2015-10-28
          • 2014-11-22
          • 2017-04-18
          • 2022-01-23
          相关资源
          最近更新 更多