Преобразование XML файла в CSV в java

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

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

Расскажите, как вы думаете, как бы вы решили этот

Я забыл упомянуть, что это очень большой xml файл до 1gb

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

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

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

Ответ 1

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

Я бы подошел к этой проблеме с двумя проходами с помощью анализатора 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;
        }
    }
}

Ответ 2

Это похоже на хороший пример использования XSL. Учитывая ваши основные требования, может быть проще получить правильные узлы с XSL по сравнению с пользовательскими анализаторами или сериализаторами. Выгода будет заключаться в том, что ваш XSL мог бы нацелиться на "//Item//AverageTime" или на любые нужные вам узлы, не беспокоясь о глубине node.

UPDATE: Ниже приведена 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>

Ответ 3

Я не уверен, что я понимаю, насколько универсальным должно быть решение. Вы действительно хотите разобрать файл 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 пакет предлагает все необходимое для работы с XML файлы как input или вывод (используя StAX и структуру привязки XML по вашему выбору) и плоские файлы в качестве входных данных или output. См:


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


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

Ответ 4

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

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

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

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

Ответ 5

Здесь приведен код, который реализует преобразование 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();
    }    
}

Ответ 6

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

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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