2010-07-20 37 views
10

@Antes de que haya probablemente algunas sugerencias de preguntas duplicadas, no creo que sea el caso, tal vez lea esto primero, intentaré ser lo más breve posible. Título da idea básica.Convertir archivo XML a CSV en java

Aquí se muestra un ejemplo de XML (caso 1):

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

Aquí es un XML ejemplo (caso 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> 

Tomé prestado este XML de Google, de todos modos mis objetos no son siempre lo mismo, a veces hay elementos adicionales como en caso2. Ahora me gustaría para producir CSV como esta de los dos casos:

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 

Esta primera línea de cabecera es que también debe incluirse en csv. Obtuve algunos enlaces útiles a stax hoy, realmente no sé cuál es el enfoque correcto/óptimo para esto, estoy luchando con esto durante 3 días, realmente no estoy dispuesto a darme por vencido todavía.

Dime lo que piensas ¿cómo resolver este

me olvidó mencionar esto es muy enorme de archivo XML de hasta 1 GB

BOUNTY ACTUALIZACIÓN:

Busco para un enfoque más genérico, lo que significa que esto debería funcionar para cualquier número de nodos con cualquier profundidad, y a veces como en el ejemplo xml, puede suceder que un objeto item tenga una mayor cantidad de nodos que el siguiente/anterior, por lo que hay también debería ser el caso (para que todas las columnas y valores coincidan en CSV).

También puede suceder que los nodos tengan el mismo nombre/localName pero diferentes valores y atributos, si ese es el caso, entonces la nueva columna debe aparecer en CSV con el valor apropiado. (He añadido un ejemplo de este caso dentro de la etiqueta <averages> llamada category)

+0

son los valores valor1, ..., siempre valorN hijos inmediatos del elemento '' ? ¿Son 'promedios' el único elemento que podría aparecer? ¿O necesita ser más flexible sobre lo que aparece allí? – erickson

+0

@erickson Actualicé mi pregunta – ant

+1

@cOmrade: acerca de su "actualización": si el primer elemento es el que tiene más columnas, entonces solo necesita dos pases/pasos para la transformación: en el paso uno solo recolecta todo el columnas, y en el paso 2 procesarlas como se describe. Si no se encuentra ningún nodo de valor para un nodo en particular, entonces puede poner el valor que desee (nulo o vacío o cualquier convención que desee, consulte mi descripción en respuesta). No es un problema que los nodos estén anidados ya que para CSV serán rojos. –

Respuesta

11

El código proporcionado debe considerarse un boceto en lugar del artículo definitivo. No soy un experto en SAX y la implementación podría mejorarse para un mejor rendimiento, un código más simple, etc. Dicho esto, SAX debería ser capaz de hacer frente a la transmisión de grandes archivos XML.

Me acercaría a este problema con 2 pases usando el analizador SAX. (Por cierto, también usaría una biblioteca de generación de CSV para crear el resultado ya que esto trataría con todos los caracteres fílmicos que escapan de los que implica el CSV pero no los he implementado en mi boceto).

Primer paso: Establecer número de cabeceras de las columnas

Segundo paso: salida CSV

supongo que el archivo XML está bien formado. Supongo que no tenemos un esquema/DTD con un orden predefinido.

En la primera pasada, he supuesto que se agregará una columna CSV para cada elemento XML que contenga contenido de texto o para cualquier atributo (¡he supuesto que los atributos contendrán algo!).

La segunda pasada, habiendo establecido el número de columnas de destino, hará la salida de CSV real.

Basado en su ejemplo XML mi bosquejo código produciría:

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 

Tenga en cuenta que he utilizado las colecciones de Google LinkedHashMultimap ya que esto es muy útil cuando se asocia valores múltiples con una sola llave. ¡Espero que encuentres esto útil!

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; 
     } 
    } 
} 
+0

¿Sabes cómo hacer que funcione un poco más genéricamente sin definir explícitamente 'elemento' y 'raíz'? es decir. sin líneas como! "item" .equalsIgnoreCase (qName) &&! "root" – toop

+0

Hola @toop, siempre puedes hacerlo en función de la profundidad del árbol, ver por ejemplo: http://stackoverflow.com/questions/6248322/java-how -to-determination-the-depth-level-during-xml-parsing-using-sax –

1

No estoy seguro de que SAX sea el mejor enfoque para usted. Sin embargo, aquí puedes usar SAX de diferentes formas.

Si el orden de los elementos no está garantizado dentro de ciertos elementos, como los detalles de la búsqueda, debe ser proactivo.

Cuando inicia un ListingDetails, inicialice un mapa como una variable miembro en el controlador. En cada subelemento, establezca el valor clave apropiado en ese mapa. Cuando finalice un ListingDetails, examine el mapa y simule explícitamente valores como nulls para los elementos faltantes. Suponiendo que tiene un ListingDetails por artículo, guárdelo en una variable miembro en el controlador.

Ahora, cuando su elemento elemento haya terminado, tenga una función que escriba la línea de CSV basada en el mapa en el orden que desee.

El riesgo es esto si tiene XML dañado. Consideraría encarecidamente establecer todas estas variables como nulas cuando se inicie un elemento, y luego buscar errores y anunciarlos cuando finalice el artículo.

2

La mejor forma de codificar según su requisito descrito es utilizar la característica fácil de FreeMarker y el procesamiento XML. See the docs.

En este caso, solo necesitará la plantilla que producirá un archivo CSV.

Una alternativa a esto es XMLGen, pero muy similar en su enfoque. Basta con mirar ese diagrama y ejemplos, y en lugar de declaraciones SQL, obtendrá CSV.

Estos dos enfoques similares no son "convencionales", pero hacen el trabajo muy rápidamente para su situación, y usted no tiene que aprender XSL (bastante difícil de dominar, creo).

2

Aquí hay un código que implementa la conversión de XML a CSV utilizando StAX. Aunque el XML que proporcionó es solo un ejemplo, espero que esto le muestre cómo manejar los elementos opcionales.

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(); 
    }  
} 
+0

Gracias por su respuesta, estoy buscando más enfoque genérico, lo que significa que debería funcionar para cualquier cantidad de nodos con cualquier profundidad, y a veces como en el ejemplo xml, puede suceder que un objeto tenga más cantidad de nodos que el siguiente así que también debería haber un caso para eso. También puede suceder que los nodos tengan el mismo nombre pero diferentes valores y atributos, como es el caso de la nueva columna en CSV también. – ant

8

Parece un buen caso para usar XSL. Dados sus requisitos básicos, puede ser más fácil llegar a los nodos correctos con XSL en comparación con los analizadores o los serializadores personalizados. El beneficio sería que su XSL podría orientar "// Item // AverageTime" o cualquier nodo que necesite sin preocuparse por la profundidad del nodo.

ACTUALIZACIÓN: El siguiente es el xslt que armé para asegurarme de que funcionó como se esperaba.

<?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> 
+0

Especialmente el requisito de "cualquier cantidad de nodos con cualquier profundidad" debe forzar los pensamientos hacia XSL y "// Item". – f1sh

+2

XSL sería la opción perfecta si se tratara de un archivo pequeño; sin embargo, el DOM para un archivo de 1 gb podría ocupar una gran cantidad de memoria. Así que me imagino que necesitaría usar algún tipo de transmisión XSL especializada (este hilo ya se menciona en Saxonica y VTD-XML). Ver también: http://stackoverflow.com/questions/2301926/xml-process-large-data –

+0

Eso es alguna información interesante. En ese caso, una tecnología de streaming xsl sería útil. Gracias por el enlace Mark. –

5

No estoy seguro de entender qué tan genérica debe ser la solución. ¿Realmente desea analizar un archivo de 1 GB dos veces para una solución genérica? Y si quiere algo genérico, ¿por qué se saltó el elemento <category> en su ejemplo? ¿Cuánto formato diferente necesitas manejar? ¿Realmente no sabes qué formato puede ser (incluso si se puede omitir algún elemento)? ¿Puedes aclarar?

Según mi experiencia, generalmente es preferible analizar archivos específicos de una manera específica (esto no excluye el uso de una API genérica). Mi respuesta irá en esta dirección (y la actualizaré después de la aclaración).


Si usted no se siente cómodo con XML, se podría considerar el uso de algunas bibliotecas existentes (comerciales), por ejemplo Ricebridge XML Manager y CSV Manager. Consulte How to convert CSV into XML and XML into CSV using Java para obtener un ejemplo completo. El enfoque es bastante sencillo: define los campos de datos usando expresiones XPath (que es perfecto en su caso ya que puede tener elementos "extra"), analiza el archivo y luego pasa el resultado List al componente CSV para generar el archivo CSV . La API parece simple, el código probado (el código fuente de su test cases está disponible bajo una licencia de estilo BSD), afirman que admite archivos de tamaño de gigabyte.

Puede obtener una licencia de Desarrollador único por $ 170, que no es muy caro en comparación con las tarifas diarias de desarrollador.

Ofrecen versiones de prueba de 30 días, eche un vistazo.


Otra opción sería utilizar Spring Batch. Spring Batch ofrece todo lo necesario para trabajar con XML files como input o salida (utilizando StAX y el marco de enlace XML de su elección) y flat files como entrada o output. Ve:


También es posible usar Smooks hacer XML a CSV transformations. Ver también:


Otra opción sería la de rodar su propia solución, utilizando un analizador StAX o, por qué no, usando VTD-XML y XPath.Echar un vistazo a:

1

Tenga en cuenta que este sería un buen ejemplo de la utilización de XSLT excepto que la mayoría de los procesadores XSLT leídos en todo el archivo XML en memoria que no es una opción ya que es grande. Tenga en cuenta, sin embargo, que la versión empresarial de Saxon puede hacer el procesamiento XSLT de transmisión (si el script XSLT cumple con las restricciones).

En su lugar, es posible que desee utilizar un procesador XSLT externo fuera de su JVM, si corresponde. Esto abre varias opciones más.

Transmisión en Saxon-EE: http://www.saxonica.com/documentation/sourcedocs/serial.html

+0

También hay Joost/STX http://joost.sourceforge.net/ que es un lenguaje similar a XSLT con algunas restricciones adicionales para la transmisión. Como este problema solo requiere un procesamiento secuencial de la entrada, debería encajar bien en ese modelo. –

+0

¿Por qué simplemente XSLT-_like_ en lugar de un subconjunto XSLT? –