Samstag, 13. Oktober 2012

Große XML Datei in mehrere kleine Dateien aufteilen

Problembeschreibung


Wer viel mit XML-Dateien zu tun hat, kennt vielleicht das Problem - man hat gerade im Programm (oder aus einer Datenbank) ein XML erzeugt, das witzige 50 MB groß ist. Eigentlich in der heutigen Zeit von Terabyte-Festplatten kein großer Wert, sollte man meinen, aber Visual Studio hängt sich beim Öffnen auf. Auch Notepad++ hat hier selbst mit SSD bedenkliche Probleme. Einzig das gute alte Notepad von Windows kann die Datei zumindest anzeigen - das war's aber dann auch, denn damit kann ich nicht wirklich viel anfangen, immerhin sind 50MB auf einer Zeile nicht wirklich übersichtlich.

Lösung

Ich habe mir ein kleines Programm geschrieben, dass ein Eingabe XML in mehrere kleinere XML aufteilt. Die Herausforderung lag darin die Datei nicht komplett einzulesen, sondern Knoten für Knoten zu verarbeiten. Außerdem musste sich das Porgramm merken, welche Knoten noch geöffnet sind, um diese dann zu schließen, sobald die maximale Anzahl an Bytes erreicht wurde. Dadurch ist gewährleistet, dass die resultierenden XML-Dateien auch valides XML repräsentieren.
 

Implementierung

Im Prinzip verwende ich für das Lesen der XML-Datei einen XmlTextReader , das hat den entscheidenden Vorteil, dass die XML-Elemente einzeln eingelesen werden und man XML spezifische Metainformationen zu jedem gelesenen Element erhält. Ich spar mir im Code das kopieren der Kommentare und der XML-Deklaration, wenn das jemand benötigt, ist das schnell zu ergänzen. Außerdem verzichte ich auf die Berücksichtigung von Namespaces.
 
 Der  Programmablauf ist recht einfach:
- Lese jeden Tag und puffere diesen
- öffnende Tags werden auf einen Stack gelegt
- bei schließenden Tags wird der letzte öffnende Tag vom Stack entfernt
- hat der Puffer die gewünschte Größe, so schließe alle noch offenen Tags und schreibe den Puffer auf Platte
- leere Puffer
- öffne die zuletzt offenen Tags wieder im aktuellen Puffer
 
Und hier nun der Code:
 
 
    class XmlUtility
    {
        #region XmlOpeningTag class
        class XmlOpeningTag
        {
            
            public XmlOpeningTag(string name, string fullOpeningTag)
            {
                TagName = name;
                FullOpeningTag = fullOpeningTag;
            }
            public string TagName { get; set; }
            public string FullOpeningTag { get; set; }
            public int ClosingTagSize
            {
                get 
                {
                    return TagName.Length + 3;
                }
            }
        }
        #endregion
        
        /// <summary>
        /// splits the given xml file into smaller parts
        /// </summary>
        /// <param name="pathToXmlFile">the path to the xml file, that is to be splitted</param>
        /// <param name="maxSizeInBytes">app. maximum size of the resulting parts</param>
        /// <param name="resultPath">where to write (path) the resulting files</param>
        /// <returns>list of filepaths, that were created</returns>
        public static List<string> Split(string pathToXmlFile, int maxSizeInBytes, string resultPath=null)
        {
            if (resultPath == null)
                resultPath=System.IO.Path.GetDirectoryName(pathToXmlFile);
            List<string> res = new List<string>();
            // initialize variables
            int currentFileNumber = 1;
            int sizeOfClosingTags = 0;
            string fileWithoutExtension = System.IO.Path.Combine(
                resultPath,               
                System.IO.Path.GetFileNameWithoutExtension(pathToXmlFile));
            StringBuilder output = new StringBuilder();
            Stack<XmlOpeningTag> _openElements = new Stack<XmlOpeningTag>();

            // open the xml file within a xmlreader
            System.Xml.XmlTextReader xmlReader = new System.Xml.XmlTextReader(pathToXmlFile);
            // read from the file, element by element
            while (xmlReader.Read())
            {
                string currentXmlLine = string.Empty;
                XmlOpeningTag newOpeningTag = null;
                switch (xmlReader.NodeType)
                {
                    case XmlNodeType.Element: //its an xml tag element (opening)
                        string xmlElementName = xmlReader.Name;
                        // start with the tag
                        currentXmlLine = "<" + xmlElementName;
                        bool isEmptyElement = xmlReader.IsEmptyElement;
                        // add all attributes
                        while (xmlReader.MoveToNextAttribute()) 
                            currentXmlLine += " " + xmlReader.Name + "='" + xmlReader.Value + "'";
                        // if the element is empty, close it in line
                        if (isEmptyElement )
                            currentXmlLine += "/>";
                        //otherwise just close the tag
                        else
                        {
                            currentXmlLine += ">";
                            newOpeningTag =
                             new XmlOpeningTag(
                                    xmlElementName,
                                    currentXmlLine
                                    );
                            sizeOfClosingTags += newOpeningTag.ClosingTagSize;
                        }
                        break;
                    case XmlNodeType.Text: // text node
                        currentXmlLine = xmlReader.Value;
                        break;
                    case XmlNodeType.EndElement: //end of element tag
                       // pop the last opening tag
                       XmlOpeningTag openingTag = _openElements.Pop();
                       sizeOfClosingTags -= openingTag.ClosingTagSize;
                       currentXmlLine = "</" + xmlReader.Name + ">";
                       break;
                }
                
                // lets see, if we can add this new line
                if (output.Length + sizeOfClosingTags + currentXmlLine.Length >= maxSizeInBytes
                    && xmlReader.NodeType != XmlNodeType.EndElement 
                    // endelements will be added anyways, we cant close the xml 
                    // before adding the endelement
                    )
                {
                    // na we cannot
                    // close all open tags
                    Stack<XmlOpeningTag> helperStack = new Stack<XmlOpeningTag>();
                    while (_openElements.Count > 0)
                    {
                        XmlOpeningTag ol = _openElements.Pop();
                        output.Append("</" + ol.TagName + ">");
                        helperStack.Push(ol);
                    }
                    
                    string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                    res.Add(filePathToSave);
                    System.IO.File.WriteAllText(filePathToSave, output.ToString());
                    currentFileNumber++;
                    // clear the string builder, the old stuff was saved
                    output.Clear();
                    // open the "old open" tags in the new output
                    while (helperStack.Count > 0)
                    {
                        XmlOpeningTag ol = helperStack.Pop();
                        output.Append(ol.FullOpeningTag);
                        _openElements.Push(ol);
                    }

                }

                if (newOpeningTag != null)
                    _openElements.Push(newOpeningTag);
                output.Append(currentXmlLine);
            }
            if (output.Length > 0)
            {
                string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                res.Add(filePathToSave);
                System.IO.File.WriteAllText(filePathToSave, output.ToString());
                currentFileNumber++;
            }
            
            xmlReader.Close();
            return res;
        }
    }

Kommentare:

  1. Dieser Kommentar wurde vom Autor entfernt.

    AntwortenLöschen
  2. Hallo,

    dieser Beitrag ist zwar schon uralt, aber ich wollte mich trotzdem dafür bedanken. Dieses simple Tool ist genau das, was ich für meine bescheidenen Anforderungen gebraucht habe. Perfekt wäre es, wenn man den Ausgabepfad noch angeben könnte, aber ich will ja nicht undankbar erscheinen ;)
    Ich sehe zwar im Quelltext den Parameter "resultPath", aber irgendwie verstehe ich nicht, wo ich den übergeben muss.

    Grüße

    AntwortenLöschen