12

@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>

我从谷歌借了这个 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 中。(我在<averages>名为 的标签中添加了这种情况的示例category

4

8 回答 8

14

提供的代码应被视为草图而不是权威文章。我不是 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;
        }
    }
}
于 2010-07-30T00:13:02.400 回答
9

这看起来是使用 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>
于 2010-07-30T00:42:50.223 回答
6

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

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


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

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

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


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


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


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

于 2010-07-30T05:37:46.933 回答
2

根据您描述的要求进行编码的最佳方法是使用 FreeMarker 和 XML 处理的简单功能。请参阅文档

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

另一种方法是XMLGen,但方法非常相似。看看那个图表和例子,你将输出 CSV 而不是 SQL 语句。

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

于 2010-07-20T19:32:43.130 回答
2

这里有一些代码使用 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();
    }    
}
于 2010-07-27T18:25:21.590 回答
1

我不相信 SAX 是最适合您的方法。不过,您可以在此处使用不同的方式使用 SAX。

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

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

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

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

于 2010-07-20T19:06:49.450 回答
1

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

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

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

于 2010-08-01T13:00:44.917 回答
0

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

于 2010-07-20T19:13:19.887 回答