Конвертировать XML файл в CSV в Java - PullRequest
10 голосов
/ 20 июля 2010

@ До того, как, возможно, будут предложены несколько дублирующих вопросов, я не думаю, что в этом случае, возможно, сначала прочитайте это, я постараюсь быть максимально кратким.Заголовок дает основную идею.

Вот пример 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>

Iзаимствовал этот XML у Google, в любом случае мои объекты не всегда одинаковы, иногда есть дополнительные элементы, как в случае 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

Эта 1-я строка является заголовком, ее также следует включить в CSV.У меня есть несколько полезных ссылок на Stax сегодня, я действительно не знаю, какой правильный / оптимальный подход для этого, я борюсь с этим в течение 3 дней, пока не собираюсь сдаваться.

Скажите мне, что вы думаете, как бы вы решили это

Я забыл упомянуть, что это очень большой XML-файл до 1 ГБ

ОБНОВЛЕНИЕ BOUNTY:

Я ищу более общий подход, означающий, что он должен работать для любого количества узлов с любой глубиной, и иногда, как в примере с xml, может случиться так, что один объект item имеетбольшее количество узлов, чем следующий / предыдущий, поэтому должен быть случай для этого (чтобы все столбцы и значения совпадали в CSV).

Также может случиться, что узлы имеют одинаковое имя / localName, но разные значения и атрибуты, если это так, тогда в CSV должен появиться новый столбец с соответствующим значением.(Я добавил пример этого случая внутри тега <averages> под названием category)

Ответы [ 8 ]

11 голосов
/ 30 июля 2010

Предоставленный код следует рассматривать как эскиз, а не как окончательную статью. Я не эксперт по SAX, и его реализация может быть улучшена для повышения производительности, упрощения кода и т. Д. При этом SAX должен справляться с потоковой передачей больших файлов XML.

Я бы подошел к этой проблеме с 2 проходами, используя парсер SAX. (Между прочим, я бы также использовал библиотеку создания CSV для создания выходных данных, так как это было бы связано со всеми непростыми символами, которые экранирует CSV, но я не реализовал это в своем наброске).

Первый проход: Установить количество столбцов заголовка

Второй проход: Выход CSV

Я предполагаю, что файл XML правильно сформирован. Я предполагаю, что у нас нет схемы / DTD с предопределенным порядком.

В первом проходе я предполагал, что столбец CSV будет добавлен для каждого элемента XML, содержащего текстовое содержимое, или для любого атрибута (я предполагал, что атрибуты будут содержать что-то!).

Второй проход, установив количество целевых столбцов, будет выполнять фактический вывод 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;
        }
    }
}
8 голосов
/ 30 июля 2010

Это похоже на хороший пример использования 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>
5 голосов
/ 30 июля 2010

Я не уверен, что понимаю, насколько универсальным должно быть решение. Вы действительно хотите дважды проанализировать файл размером 1 ГБ для общего решения? И если вы хотите что-то общее, почему вы пропустили элемент <category> в своем примере? Сколько другого формата вам нужно обрабатывать? Вы действительно не знаете, какой может быть формат (даже если какой-то элемент может быть пропущен)? Вы можете уточнить?

По моему опыту, обычно предпочтительнее анализировать определенные файлы определенным образом (хотя это не исключает использования универсального API). Мой ответ пойдет в этом направлении (и я уточню его после уточнения).


Если вы не чувствуете себя комфортно с XML, вы можете рассмотреть возможность использования некоторых существующих (коммерческих) библиотек, например, Ricebridge XML Manager и CSV Manager . См. Как преобразовать CSV в XML и XML в CSV с использованием Java для полного примера. Подход довольно прост: вы определяете поля данных с помощью выражений XPath (что идеально в вашем случае, поскольку вы можете иметь «дополнительные» элементы), анализируете файл и затем передаете результат List компоненту CSV для генерации CSV файл API выглядит просто, проверенный код (исходный код тестовых примеров доступен по лицензии в стиле BSD), утверждают, что он поддерживает файлы размером в гигабайт.

Вы можете получить лицензию на одного разработчика за 170 долларов, что не очень дорого по сравнению с дневными тарифами для разработчиков.

Они предлагают 30-дневные пробные версии, посмотрите.


Другой вариант - использовать Spring Batch . Spring batch предлагает все необходимое для работы с XML-файлами в качестве ввода или вывода (с использованием StAX и XML-привязки по вашему выбору) и плоских файлов в качестве ввода или выход . См:


Вы также можете использовать Smooks для преобразования XML в CSV . Смотри также:


Другим вариантом может быть развертывание собственного решения с использованием синтаксического анализатора StAX или, если нет, с использованием VTD-XML и XPath. Посмотрите на:

2 голосов
/ 27 июля 2010

Вот некоторый код, который реализует преобразование XML в CSV с использованием StAX.Хотя приведенный вами 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();
    }    
}
2 голосов
/ 20 июля 2010

Лучший способ написания кода в соответствии с вашими требованиями - использовать простую функцию обработки FreeMarker и XML. См. Документы .

. В этом случае вам потребуется только шаблон, который будет генерировать CSV.

Альтернативой этому является XMLGen ,но очень похоже в подходе.Просто посмотрите на эту диаграмму и примеры, и вместо операторов SQL вы получите CSV.

Эти два похожих подхода не являются "традиционными", но выполняют работу очень быстро для вашей ситуации, и у вас нетизучать XSL (я думаю, довольно сложно освоить).

1 голос
/ 01 августа 2010

Обратите внимание, что это будет основной пример использования XSLT, за исключением того, что большинство процессоров XSLT считывают весь XML-файл в память, что не вариант, поскольку он большой. Однако обратите внимание, что корпоративная версия Saxon может выполнять потоковую обработку XSLT (если скрипт XSLT придерживается ограничений).

Вы также можете использовать внешний процессор XSLT вне вашей JVM, если это применимо. Это открывает еще несколько вариантов.

Потоковая передача в Саксонии-EE: http://www.saxonica.com/documentation/sourcedocs/serial.html

1 голос
/ 20 июля 2010

Я не уверен, что SAX - лучший подход для вас. Однако есть несколько способов использовать SAX здесь.

Если порядок элементов в определенных элементах, таких как ListingDetails, не гарантирован, то вам необходимо проявлять активность.

Когда вы запускаете ListingDetails, инициализируйте карту как переменную-член в обработчике. В каждом подэлементе установите соответствующее значение ключа в этой карте. Когда вы закончите ListingDetails, изучите карту и явно смоделируйте значения, такие как нули, для отсутствующих элементов. Предполагая, что у вас есть один ListingDetails на элемент, сохраните его в переменной-члене в обработчике.

Теперь, когда ваш элемент item закончен, есть функция, которая записывает строку CSV на основе карты в нужном вам порядке.

С этим связан риск повреждения XML. Я бы настоятельно рекомендовал установить все эти переменные в NULL при запуске элемента, а затем проверить наличие ошибок и объявить их, когда элемент заканчивается.

0 голосов
/ 20 июля 2010

Вы можете использовать XStream (http://x -stream.github.io / ) или JOX (http://www.wutka.com/jox.html)), чтобы распознать XML и затем преобразовать его в Java Bean. Я думаю, вы можетеконвертируйте Бобы в CSV автоматически, как только вы получите бин.

...