Java "Invalid XML charakter" 0x4 beim einlesen von XML

[o.0]

Lt. Commander
Registriert
Apr. 2008
Beiträge
1.056
Hallo,

ich schreibe im Moment einen Parser der mir meine Verläufe von Miranda nach Pidgin rettet. Dazu exportiere ich die Verläufe in Miranda im XML Format und lese sie mit einem Java Programm ein, das daraus die entsprechenden HTML Dokumente erstellt und abspeichert. Das klappt soweit auch alles wunderbar, allerdings habe ich noch mindestens ein kleineres Problem dabei.

Aus unerfindlichen Gründen sind in den XML Dateien allerdings diverse Zeichen enthalten, die in einer XML eigentlich nichts zu suchen haben:

Code:
[Fatal Error] test4.xml:53154:11: An invalid XML character (Unicode: 0x4) was found in the element content of the document.
Google spuckt aus, dass es sich bei diesem Zeichen um "EOT (End of Transmission)" handelt und das wirklich nichts darin verloren hat. Diverse Lösungen gab es natürlich auch, aber Lösungsansätze wie die Ausgabe des XML-erstellenden Programmes zu korrigieren gehen in meinem Fall natürlich nicht.

Dabei schmiert dann natürlich das komplette Programm ab. Klar, ich könnte einfach vor dem Einlesen alle .xml Dateien nach diesem Unicode Zeichen durchsuchen und es ggf löschen, allerdings muss das bei >200MB Dateien nicht unbedingt sein. Und eine elegante Variante wäre mir natürlich lieber :p
Hat jemand eine Idee wie ich das Problem lösen könnte?

Die entsprechende Klasse meines Programmes sieht so aus, die relevante Stelle beim Einlesen der Nachricht beginnt bei Zeile 92.

Code:
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

import java.io.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;

/**
 *
 * @author max
 */
public class parser {
    
    public messages parse(String datei, Statistik Stats)
    {
        // Rückgabe Objekt
        messages Messages = new messages();
        
        try
        {
            // Alle nötigen Objekte erstellen
            File file = new File(datei);            
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbFactory.newDocumentBuilder();
            Document document = db.parse(file);
            document.getDocumentElement().normalize();            
            NodeList nodeL = document.getElementsByTagName("EVENT");
            
            int anzahlEvents = nodeL.getLength();
            
            // Statistik aktualisieren..
            Stats.aktMessagesBearbeitet(anzahlEvents);
            
            // Arrays zum Zwischenspeichern der Daten
            String[] fromA = new String[anzahlEvents];
            String[] timeA = new String[anzahlEvents];
            String[] dateA = new String[anzahlEvents];
            String[] idA = new String[anzahlEvents];
            String[] messageA = new String[anzahlEvents];
            
            for (int i = 0; i < anzahlEvents; i++)
            {
                Node firstNode = nodeL.item(i);
  
                if (firstNode.getNodeType() == Node.ELEMENT_NODE)
                {  
                    Element element = (Element) firstNode;
                    
                    if(i == 0) // Nur beim ersten Event nötig da sich die Werte innerhalb einer Datei nicht ändern
                    {
                        // Name des Kontaktes                    
                        NodeList contactElemntList = element.getElementsByTagName("CONTACT");
                        Element contactElement = (Element) contactElemntList.item(0);
                        NodeList contact = contactElement.getChildNodes();
                        
                        // Protokoll über das die Nachricht verschickt wurde, zB "MSN"
                        NodeList protocolElementList = element.getElementsByTagName("PROTOCOL");
                        Element protocolElement = (Element) 
                        protocolElementList.item(0);
                        NodeList protocol = protocolElement.getChildNodes();
                        
                        Messages.setContact(contact.item(0).getNodeValue());
                        Messages.setProtocol(protocol.item(0).getNodeValue());
                    }
                    
                    // Name des Senders der Nachricht
                    NodeList fromElementList = element.getElementsByTagName("FROM");
                    Element fromElement = (Element) 
                    fromElementList.item(0);
                    NodeList from = fromElement.getChildNodes();
                    
                    // Uhrzeit der Nachricht, zB "21:39:46"
                    NodeList timeElementList = element.getElementsByTagName("TIME");
                    Element timeElement = (Element) 
                    timeElementList.item(0);
                    NodeList time = timeElement.getChildNodes();
                    
                    // Datum der Nachricht, zB "2011-10-13"
                    NodeList dateElementList = element.getElementsByTagName("DATE");
                    Element dateElement = (Element) 
                    dateElementList.item(0);
                    NodeList date = dateElement.getChildNodes();
                    
                    // Adresse des Absenders, zB "beispiel@msn.com"
                    NodeList idElementList = element.getElementsByTagName("ID");
                    Element idElement = (Element) 
                    idElementList.item(0);
                    NodeList id = idElement.getChildNodes();
                    
                    // Die Nachricht selbst
                    NodeList messageElementList = element.getElementsByTagName("MESSAGE");
                    if(messageElementList.getLength() != 0) // Falls MESSAGE-Tag existiert, ansonsten leeren String speichern
                    {
                        Element messageElement = (Element) 
                        messageElementList.item(0);
                        NodeList message = messageElement.getChildNodes();
                        messageA[i] = message.item(0).getNodeValue();
                    }
                    else
                    {
                        messageA[i] = "";
                    }
                    
                    fromA[i] = from.item(0).getNodeValue();
                    timeA[i] = time.item(0).getNodeValue();
                    dateA[i] = date.item(0).getNodeValue();
                    idA[i] = id.item(0).getNodeValue();                   
                } 
            }
            
            // Speichert die eigene ID sowie die des Chatpartners
            boolean fremdIDGespeichert = false;
            boolean eigeneIDGespeichert = false;
            int i = 0;
            
            while(fremdIDGespeichert == false)
            {
                if(fremdIDGespeichert == false && Messages.getContact().equals(fromA[i]))
                {
                    Messages.setfremdID(idA[i]);
                    fremdIDGespeichert = true;
                }
                i++;
            }
            
            i = 0;
            
            while(eigeneIDGespeichert == false)
            {
                if(eigeneIDGespeichert == false && !Messages.getContact().equals(fromA[i]))
                {
                    Messages.seteigeneID(idA[i]);
                    eigeneIDGespeichert = true;
                }
                i++;
            }
            
            // Die fertigen Arrays dem Rückgabe Objekt zuweisen
            Messages.setFrom(fromA);
            Messages.setTime(timeA);
            Messages.setDate(dateA);
            Messages.setId(idA);
            Messages.setMessage(messageA);        
        }
        catch (Exception e)
        {
            System.out.println(e.toString());
        }
        
        return Messages;
    }   
}

[edit]

Die Struktur der XML, falls benötigt:
Code:
<IMHISTORY>
<EVENT>
	<CONTACT></CONTACT>
	<FROM></FROM>
	<TIME></TIME>
	<DATE></DATE>
	<PROTOCOL></PROTOCOL>
	<ID></ID>
	<TYPE></TYPE>
	<MESSAGE></MESSAGE>
</EVENT>
[...]
</IMHISTORY>

[/edit]

Ich wäre für jede Hilfe dankbar.. und ja, die Methode ist glaube ich suboptimal geschrieben, allerdings ist es das erste Mal das ich etwas mit XML mache und ich bin schon froh das es überhaupt läuft :freak:
 
Zuletzt bearbeitet:
Wenn du die Erzeugung des Outputs nicht beeinflussen kannst, wird dir wohl nichts anderes übrig bleiben, als die Daten vor der XML-Verarbeitung zu filtern. Dazu kannst du in deinem Programm die Datei in einen String oder Stringbuffer lesen, und dabei die 0x4 herausfiltern, dann kannst du mittels StringReader den String weiterverarbeiten, also im Pseudocode etwa parse(StringReader(dein_Stringbuffer)) aufrufen.
 
Mh, das hab ich schon befürchtet. Ich werde das einfach mal so probieren wie du es vorschlägst, die ~30MB, die eine einzelne XML Datei bei mir maximal hat, sollten ja in einen String passen.
Danke für die Hilfe, falls ich es nicht hinbekomme oder es aus irgend einem Grund nicht klappen will meld ich mich in den nächsten Tagen nochmal ;)

Edit: Argh, diese Steuerzeichen machen mich jetzt echt fertig. Selbst wenn ich mit aus dem Internet geklauten Ansätzen wie "dateiInhalt.replaceAll("\\p{Cc}", "");" an die Sache rangehe, löscht es die Zeichen nicht. Testweiße habe ich die Zeichen manuell aus den Verläufen gelöscht und dabei festgestellt das es noch an viel mehr Stellen 0x3, 0x10 und ähnliche Zeichen gibt. :/
 
Zuletzt bearbeitet:
[o.0] schrieb:
Hat jemand eine Idee wie ich das Problem lösen könnte?

Code:
DocumentBuilder db = dbFactory.newDocumentBuilder();
Document document = db.parse(file);

DocumentBuilder#parse(java.io.File) ist nicht die einzige Methode, die zum Einlesen genommen werden kann. Ich würde mir einen eigenen Reader schreiben, der die ungültigen Zeichen verwirft und diesen mittels parse#(org.xml.sax.InputSource) setzen. Dann muss nicht erst die ganze Datei in den Speicher gelesen werden.

Etwa so (ist nicht vollständig getestet, nur um mal eine Idee zu geben):

Code:
InputSource source = new InputSource(new BufferedReader(
            new SkipReader(new FileReader(file), '\u0004')));

Document document = DocumentBuilderFactory.newInstance()
                                          .newDocumentBuilder()
                                          .parse(source);

Und hier der Reader. Ich bin mir allerdings nicht ganz sicher, ob sich das so 100% mit den Parser-Constraints verträgt. Validierung sollte man ev. abschalten.

Code:
public class SkipReader
    extends FilterReader
{
    private final char[] ignored;

    public SkipReader(
        final Reader  rInput,
        final char... rIgnored)
    {
        super(rInput);

        ignored = rIgnored;

        Arrays.sort(ignored);
    }

    @Override
    public int read(
        final char[] rBuffer,
        final int    rOffset,
        final int    rLength)
        throws IOException
    {
        int length = super.read(rBuffer, rOffset, rLength);

        if (length == -1)
        {
            return -1;
        }

        int skipped = 0;

        for (int i = 0, read = -1, write = -1; i < length; i++)
        {
            read++;

            if (Arrays.binarySearch(ignored, rBuffer[i]) > -1)
            {
                skipped++;

                continue;
            }

            write++;

            if (write < read)
            {
                rBuffer[write] = rBuffer[read];
            }
        }

        return length - skipped;
    }
}

Dieser Ansatz wäre auf jeden Fall nicht so speicher-intensiv und außerdem sehr flexibel, da Du sehr einfach bestimmen kannst, welche Zeichen ignoriert werden sollen. Wobei wenn Du ohnehin DOM verwendest, sollte Speicher nicht das Problem sein ;)
 
Mh, ja, das ist auch ein interessanter Ansatz, muss ich wohl mal ein wenig mit herumprobieren. Der RAM Verbrauch ist mir recht egal, daran sollte es nicht mangeln. Die Flexibilität bei der Auswahl der zu filternden Zeichen ist in deiner Version aber wirklich deutlich besser.. bin weder ein Fan von RegEx noch von endlosen replaceAll-Ketten, da ist die Arrayvariante schon deutlich eleganter. Ich meld mich wieder falls es selbst so nicht klappt, werd das aber erstmal in Ruhe ausprobieren müssen.

Vielen Dank für die Idee und den Code.
 
Naja, die Lösung mit regulären Ausdrücken funktioniert sicherlich auch. Dein Regex war falsch gewählt (replaceAll("\u0004", "") sollte klappen). Und für die Verkettung könnte man sich auch eine Hilfsmethode schreiben.

Sonderlich effizient ist dieser Ansatz halt nicht. Aber oft spielt das natürlich keine Rolle. Von daher sicher die naheliegende Herangehensweise, wenn man sich denn mit den Ausdrücken auskennt.
 
Der Vollständigkeit halber sei gesagt, dass du deine ursprüngliche Aufgabe so schön mit XSLT hättest lösen können, XML nach HTML schreit fast danach :)
 
@soares:
replaceAll("\u0004", "") hatte ich zuerst versucht, das hat aus unerfindlichen Gründen keinerlei Einfluss.. ich vermute fast das hängt mit der Funktion dieses Zeichens zusammen :/ Oder irgendwas anderes läuft komplett schief. Effizienz ist egal, das rattert eh in drei Sekunden durch.


@dcobra:
Ehrlich gesagt habe ich von XSLT jetzt zum ersten Mal gehört, sieht aber interessant aus. Ist jetzt aber eben schon zu 99% in Java fertig :/
 
Es sollte auf jeden Fall möglich sein, die Zeichen mittels Suchen und Ersetzen zu entfernen. Vielleicht stimmt etwas mit dem Encoding nicht.

XSLT ist sehr nützlich. Ich generiere meine komplette Doku damit. Bedarf aber auch etwas Lernaufwand. Von daher ohne Vorkenntnisse Overkill für dieses Problem.
 
Fehler gefunden.. und er ist an Doofheit kaum zu überbieten <.<

Code:
public messages parse(String datei, Statistik Stats){
[...]
String dateiInhalt = datei.toString().replaceAll("\u0004", "");

Ich habe die ganze Zeit den Dateinamen nach den Steuerzeichen durchsucht und nicht ihren Inhalt. Jetzt läuft es ganz simpel per replaceAll..

Vielen Dank für eure Hilfe, wieder was gelernt. XSLT werd ich mir auch definitiv mal in den Semesterferien anschauen.
 
Zurück
Oben