2010-07-20 25 views
10

@Before Probabilmente ci saranno alcuni suggerimenti di domande duplicate, non penso che sia il caso di leggere prima questo, cercherò di essere il più breve possibile. Il titolo dà un'idea di base.Converti file XML in CSV in java

Ecco un esempio 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>  

Ecco un esempio XML (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> 

ho preso in prestito questo XML da parte di Google, in ogni modo i miei oggetti non sono sempre uguale, a volte ci sono elementi extra come nel caso 2. Ora mi piacerebbe produrre CSV come questo da entrambi i casi:

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 

Questa prima linea è di intestazione dovrebbe anche essere incluso in csv. Oggi ho alcuni link utili per stax, non so davvero quale sia l'approccio giusto/ottimale per questo, sto lottando con questo per 3 giorni, non ancora disposta a rinunciare ancora.

Ditemi cosa ne pensate come si possa risolvere questo

Ho dimenticato di dire questo è molto grande file XML fino a 1GB

BOUNTY UPDATE:

sto cercando per un approccio più generico, il che significa che dovrebbe funzionare per qualsiasi numero di nodi con qualsiasi profondità e, a volte come nell'esempio xml, può succedere che un oggetto item abbia un numero maggiore di nodi rispetto a quello successivo/precedente in modo da dovrebbe essere anche un caso per questo (quindi tutte le colonne e i valori corrispondono in CSV).

Inoltre può succedere che i nodi abbiano lo stesso nome/localName ma valori e attributi diversi, se questo è il caso, la nuova colonna dovrebbe apparire in CSV con il valore appropriato. (Ho aggiunto un esempio di questo caso all'interno del tag <averages> chiamato category)

+0

sono i valori valore1, ..., valoreN sempre figli immediati della '' elemento? "Medie" è l'unico elemento che potrebbe apparire? O hai bisogno di essere più flessibile su ciò che appare lì? – erickson

+0

@erickson Ho aggiornato la mia domanda – ant

+1

@cOmrade: riguardo al tuo "aggiornamento": se non il primo elemento è quello con più colonne, allora hai solo bisogno di due passaggi/passaggi per la trasformazione: nel primo passo raccogli tutti colonne e nel passaggio 2 elaborarli come descritto. Se non viene trovato alcun nodo di valore per un particolare nodo, allora puoi inserire un valore che desideri (null o vuoto o qualsiasi convenzione tu voglia - vedi la mia descrizione in risposta). Non è un problema che i nodi siano nidificati poiché per CSV saranno rossi piatti. –

risposta

11

Il codice fornito deve essere considerato uno schizzo piuttosto che l'articolo definitivo. Non sono un esperto di SAX e l'implementazione potrebbe essere migliorata per prestazioni migliori, codice più semplice ecc. Detto questo, SAX dovrebbe essere in grado di gestire grandi file XML in streaming.

Vorrei affrontare questo problema con 2 passaggi utilizzando il parser SAX. (Per inciso, vorrei anche usare una libreria di generazione CSV per creare l'output in quanto questo avrebbe a che fare con tutto il carattere scappato che sfugge al CSV ma non l'ho implementato nel mio sketch).

Primo passaggio: Stabilire il numero di colonne di intestazione

Secondo passaggio: uscita CSV

Si considera che il file XML è ben formato. Presumo che non abbiamo uno schema/DTD con un ordine predefinito.

Nel primo passaggio ho presupposto che una colonna CSV verrà aggiunta per ogni elemento XML contenente il contenuto del testo o per qualsiasi attributo (ho assunto che gli attributi conterranno qualcosa!).

Il secondo passaggio, dopo aver stabilito il numero di colonne di destinazione, eseguirà l'output CSV effettivo.

Basato sul tuo esempio XML mio codice schizzo produrrebbe:

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 

Si prega di notare che ho usato le collezioni google LinkedHashMultimap come questo è utile quando si associa valori multipli con un singolo tasto. Spero che tu trovi questo utile!

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

Sai come farlo funzionare un po 'più genericamente senza definire esplicitamente "item" e "root"? vale a dire. senza righe come! "item" .equalsIgnoreCase (qName) &&! "root" – toop

+0

Ciao @toop potresti sempre farlo in base alla profondità dell'albero, vedi ad esempio: http://stackoverflow.com/questions/6248322/java-how -determinare-il-livello-di-profondità-durante-xml-parsing-using-sax –

1

Non sono convinto che SAX sia l'approccio migliore per te. Ci sono diversi modi in cui puoi usare SAX qui, però.

Se l'ordine degli elementi non è garantito all'interno di determinati elementi, come ListingDetails, è necessario essere proattivi.

Quando si avvia un ListingDetails, inizializzare una mappa come variabile membro sul gestore. In ciascun sottoelemento, imposta il valore-chiave appropriato in quella mappa. Quando finisci un ListingDetails, esamina la mappa e simula esplicitamente valori come null per gli elementi mancanti. Supponendo che tu abbia un ListingDetails per elemento, salvalo in una variabile membro nel gestore.

Ora, quando il tuo elemento elemento è finito, avere una funzione che scrive la linea di CSV in base alla mappa nell'ordine desiderato.

Il rischio con questo è se si è danneggiato XML. Prenderò in considerazione la possibilità di impostare tutte queste variabili su null quando viene avviato un oggetto, quindi verificare la presenza di errori e annunciarli al termine dell'elemento.

2

Il modo migliore per codificare in base ai requisiti descritti è utilizzare la funzionalità semplice di FreeMarker e dell'elaborazione XML. See the docs.

In questo caso sarà necessario solo il modello che produrrà un CSV.

Un'alternativa a questa è XMLGen, ma molto simile in approccio. Basta guardare il diagramma e gli esempi, e invece di istruzioni SQL, si genererà CSV.

Questi due approcci simili non sono "convenzionali", ma eseguono il lavoro molto rapidamente per la tua situazione, e non devi imparare XSL (abbastanza difficile da padroneggiare, credo).

2

Ecco un codice che implementa la conversione dell'XML in CSV utilizzando StAX. Sebbene l'XML che hai fornito sia solo un esempio, spero che questo ti mostri come gestire gli elementi opzionali.

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

Grazie per la vostra risposta, sto cercando un approccio più generico, il che significa che dovrebbe funzionare per qualsiasi numero di nodi con qualsiasi profondità e, a volte come nell'esempio xml, può succedere che un oggetto oggetto abbia un numero maggiore di nodi rispetto al prossimo quindi dovrebbe esserci anche il caso. Inoltre può accadere che i nodi abbiano lo stesso nome ma valori e attributi diversi, come nel caso della nuova colonna in CSV. – ant

8

Questo sembra un buon caso per l'utilizzo di XSL. Dati i requisiti di base, potrebbe essere più semplice ottenere i nodi giusti con XSL rispetto ai parser o ai serializzatori personalizzati. Il vantaggio sarebbe che il tuo XSL poteva scegliere come target "// Item // AverageTime" o qualunque nodo tu necessiti senza preoccuparti della profondità del nodo.

AGGIORNAMENTO: Il seguente è xslt che ho lanciato insieme per assicurarmi che funzionasse come previsto.

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

Specialmente il requisito di "qualsiasi numero di nodi con qualsiasi profondità" dovrebbe forzare i propri pensieri verso XSL e "// Item". – f1sh

+2

XSL sarebbe la scelta perfetta se questo fosse un file di piccole dimensioni, tuttavia, il DOM per un file da 1gb potrebbe occupare un'enorme quantità di memoria. Quindi immagino una specie di streaming specializzato XSL che dovrebbe essere usato (questo thread ha già parlato di Saxonica e VTD-XML) Vedi anche: http://stackoverflow.com/questions/2301926/xml-process-large-data –

+0

Che sono alcune informazioni interessanti. In tal caso, sarebbe utile uno streaming di tecnologia xsl. Grazie per il link Marco. –

5

Non sono sicuro di aver capito quanto generica dovrebbe essere la soluzione. Vuoi veramente analizzare un file da 1 GB due volte per una soluzione generica? E se vuoi qualcosa di generico, perché hai saltato l'elemento <category> nell'esempio? Quanti formati diversi hai bisogno di gestire? Non sai davvero quale possa essere il formato (anche se qualche elemento può essere ommited)? Puoi chiarire?

In base alla mia esperienza, in genere è preferibile analizzare i file specifici in un modo specifico (tuttavia, ciò non esclude l'utilizzo di un'API generica). La mia risposta andrà in questa direzione (e la aggiornerò dopo i chiarimenti).


Se non si sente a proprio agio con XML, si potrebbe considerare l'utilizzo di alcune librerie esistenti (commerciali), per esempio Ricebridge XML Manager e CSV Manager. Vedere How to convert CSV into XML and XML into CSV using Java per un esempio completo. L'approccio è piuttosto semplice: si definiscono i campi dati usando le espressioni XPath (che è perfetto nel tuo caso poiché puoi avere elementi "extra"), analizza il file e poi passa il risultato List al componente CSV per generare il file CSV . L'API sembra semplice, il codice testato (il codice sorgente del loro test cases è disponibile con una licenza in stile BSD), affermano di supportare file di dimensioni gigabyte.

È possibile ottenere una licenza per sviluppatore singolo per $ 170 che non è molto costoso rispetto alle tariffe giornaliere degli sviluppatori.

Offrono versioni di prova per 30 giorni, date un'occhiata.


Un'altra opzione sarebbe quella di utilizzare Spring Batch. Il batch di primavera offre tutto il necessario per lavorare con XML files come input o come output (usando StAX e il framework di binding XML di tua scelta) e flat files come input o output. Vedere:


Si potrebbe anche usare Smooks fare XML in formato CSV transformations. Vedi anche:


Un'altra opzione sarebbe quella di rotolare la propria soluzione, utilizzando un parser StAX o, perché no, utilizzando VTD-XML e XPath.Date un'occhiata a:

1

Si noti che questo sarebbe un primo esempio di utilizzo di XSLT, tranne che maggior parte dei processori XSLT lette in tutto il file XML in memoria che non è un opzione in quanto è grande. Si noti, tuttavia, che la versione enterprise di Saxon può eseguire lo streaming dell'elaborazione XSLT (se lo script XSLT rispetta le restrizioni).

Si potrebbe anche voler utilizzare un processore XSLT esterno al di fuori della JVM, se applicabile. Questo si apre per molte altre opzioni.

streaming in Saxon-EE: http://www.saxonica.com/documentation/sourcedocs/serial.html

+0

C'è anche Joost/STX http://joost.sourceforge.net/ che è un linguaggio simile a XSLT con alcuni vincoli aggiuntivi per lo streaming. Poiché questo problema richiede solo l'elaborazione sequenziale dell'input, dovrebbe adattarsi bene a quel modello. –

+0

Perché solo XSLT-_like_ invece di un sottoinsieme XSLT? –