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

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

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

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

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

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

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

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

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

8 ответов


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

Я бы подошел к этой проблеме с 2 проходами, используя парсер SAX. (Кстати, я бы также использовал библиотеку генерации CSV для создания вывода, поскольку это будет иметь дело со всеми fiddly-символами, избегающими этого 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;
        }
    }
}

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

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>

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

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


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

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

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


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


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


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


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

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

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

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


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

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

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

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

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

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


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

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

потокового в Саксон-е-е: http://www.saxonica.com/documentation/sourcedocs/serial.html


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