Konvertieren Sie Textdokumente in C # in XML

Vor kurzem musste ich mich mit der Notwendigkeit befassen, Text aus Office-Dokumenten ( docx, xlsx, rtf , doc, xls, odt und ods ) abzurufen . Die Aufgabe wurde durch die Anforderung erschwert, den Text im XML-Format ohne Müll mit der Struktur darzustellen, die für die weitere Analyse am bequemsten ist.


Die Entscheidung für Interop fiel sofort aufgrund der Umständlichkeit, der weitgehend redundanten Verwendung und der Notwendigkeit, MS Office auf dem Server zu installieren, auf . Als Ergebnis wurde eine Lösung für ein internes Projekt gefunden und implementiert. Die Suche erwies sich jedoch aufgrund des Fehlens allgemein zugänglicher Handbücher als so kompliziert und nicht trivial, dass ich mich entschied, in meiner Freizeit eine Bibliothek zu schreiben, die das angegebene Problem lösen würde, und eine Art Anleitung zum Schreiben zu erstellen, damit die Entwickler lesen können Zumindest oberflächlich konnte sie das Problem verstehen.


Bevor Sie mit der Beschreibung der gefundenen Lösung fortfahren, empfehle ich Ihnen, sich mit einigen der Schlussfolgerungen vertraut zu machen, die aus meinen Recherchen gezogen wurden:


  1. Für die .Net-Plattform gibt es keine vorgefertigte Lösung für die Arbeit mit allen aufgelisteten Formaten, die uns zwingen wird, unsere Lösung an einigen Stellen zu kastilisieren.
  2. Versuchen Sie nicht, ein gutes Handbuch für die Arbeit mit Microsoft OpenXML im Netzwerk zu finden: Um mit dieser Bibliothek fertig zu werden, müssen Sie ziemlich rote Augen haben, StackOverflow rauchen und mit dem Debugger spielen.
  3. Ja, ich habe es trotzdem geschafft, den Drachen zu zähmen.

Ich werde sofort reservieren, dass die Bibliothek derzeit noch nicht fertig ist, aber aktiv geschrieben wird (soweit es die Freizeit erlaubt). Es wird davon ausgegangen, dass für jedes Format separate Beiträge verfasst werden und parallel dazu zusammen mit ihrer Veröffentlichung das Repository auf dem Github aktualisiert wird, von wo aus die Quellen abgerufen werden können.


Arbeiten Sie mit xlsx und docx


.xlsx


, , , docx xlsx zip-, xml. , : zip . , : \xl\worksheets.


excel , , - , :



, , , ( <f>) ( <v>). , shared sharedStrings.xml, \xl.
: .


, -, IConvertable:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace ConverterToXml.Converters
{
    interface IConvertable
    {
        string Convert(Stream stream);
        string ConvertByFile(String path);

    }
}

, : string Convert(Stream stream) ( , - ), string ConvertByFile(String path) .


XlsxToXml, IConvertable Nuget DocumentFormat.OpenXml ( , 2.10.0).


string SpreadsheetProcess(Stream memStream), string Convert(Stream stream).


        public string Convert(Stream memStream)
        {
            return SpreadsheetProcess(memStream);
        }

, *string SpreadsheetProcess(Stream memStream)*:


string SpreadsheetProcess(Stream memStream)
        {
            using (SpreadsheetDocument doc = SpreadsheetDocument.Open(memStream, false))
            {
                memStream.Position = 0;
                StringBuilder sb = new StringBuilder(1000);
                sb.Append("<?xml version=\"1.0\"?><documents><document>");
                SharedStringTable sharedStringTable = doc.WorkbookPart.SharedStringTablePart.SharedStringTable; 
                int sheetIndex = 0;
                foreach (WorksheetPart worksheetPart in doc.WorkbookPart.WorksheetParts)
                {
                    WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);
                    sheetIndex++;
                }
                sb.Append(@"</document></documents>");
                return sb.ToString();
            }
        }

, string SpreadsheetProcess(Stream memStream) :


  1. using excel . xlsx DocumentFormat.OpenXml SpreadsheetDocument.


  2. StringBuilder sb ( 1000 . StringBuilder , . , , .


  3. shared ( ). , SpreadsheetDocument :
    SharedStringTable sharedStringTable = doc.WorkbookPart.SharedStringTablePart.SharedStringTable.


  4. ,


    foreach (WorksheetPart worksheetPart in doc.WorkbookPart.WorksheetParts)
                {
                    WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);
                    sheetIndex++;
                }


    WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);:


    private void WorkSheetProcess(StringBuilder sb, SharedStringTable sharedStringTable, WorksheetPart worksheetPart, SpreadsheetDocument doc,
            int sheetIndex)
        {
            string sheetName = doc.WorkbookPart.Workbook.Descendants<Sheet>().ElementAt(sheetIndex).Name.ToString();
            sb.Append($"<sheet name=\"{sheetName}\">");
            foreach (SheetData sheetData in worksheetPart.Worksheet.Elements<SheetData>())
            {
                if (sheetData.HasChildren)
                {
                    foreach (Row row in sheetData.Elements<Row>())
                    {
                        RowProcess(row, sb, sharedStringTable);
                    }
                }
            }
            sb.Append($"</sheet>");
        }

  5. , :
    string sheetName = doc.WorkbookPart.Workbook.Descendants<Sheet>().ElementAt(sheetIndex).Name.ToString();
    , , . , . , , shift+F9( ), doc( )->WorkbookPart->Workbook Descendants(), Sheet. , ( ). :


  6. foreach , . sheetData - , , RowProcess:


    foreach (SheetData sheetData in worksheetPart.Worksheet.Elements<SheetData>())
            {
                if (sheetData.HasChildren)
                {
                    foreach (Row row in sheetData.Elements<Row>())
                    {
                        RowProcess(row, sb, sharedStringTable);
                    }
                }
            }

  7. void RowProcess(Row row, StringBuilder sb, SharedStringTable sharedStringTable) :


    void RowProcess(Row row, StringBuilder sb, SharedStringTable sharedStringTable)
        {
            sb.Append("<row>");
            foreach (Cell cell in row.Elements<Cell>())
            {
                string cellValue = string.Empty;
                sb.Append("<cell>");
                if (cell.CellFormula != null)
                {
                    cellValue = cell.CellValue.InnerText;
                    sb.Append(cellValue);
                    sb.Append("</cell>");
                    continue;
                }
                cellValue = cell.InnerText;
                if (cell.DataType != null && cell.DataType == CellValues.SharedString)
                {
                    sb.Append(sharedStringTable.ElementAt(Int32.Parse(cellValue)).InnerText);
                }
                else
                {
                    sb.Append(cellValue);
                }
                sb.Append("</cell>");
            }
            sb.Append("</row>");
        }

    foreach (Cell cell in row.Elements<Cell>()) :


    if (cell.CellFormula != null)
                {
                    cellValue = cell.CellValue.InnerText;
                    sb.Append(cellValue);
                    sb.Append("</cell>");
                    continue;
                }

    , , (cellValue = cell.CellValue.InnerText;) .
    , , shared: , :


    if (cell.DataType != null && cell.DataType == CellValues.SharedString)
                {
                    sb.Append(sharedStringTable.ElementAt(Int32.Parse(cellValue)).InnerText);
                }

    , .




.docx


, word excel-.
, , , , , , . , , .., , , , - , , .


, . zip . . word document. , , , , . , : - .


, w:t, w:r, w:p. , docx, . : , w:numPr, (w:ilvl) id , (w:numId).

, , , , ( , ), , id , , .
, , :

, . w:tr () w:tc().


Bevor Sie mit dem Codieren beginnen, möchte ich auf eine sehr wichtige Nuance eingehen (ja, wie im Witz über Petka und Vasily Ivanovich). Beim Parsen von Listen, insbesondere bei verschachtelten Listen, kann es vorkommen, dass die Listenelemente durch das Einfügen von Text, Bildern oder anderen Elementen getrennt werden. Dann stellt sich die Frage, wann wir das abschließende Tag der Liste setzen. Mein Vorschlag, der nach Krücken und Fahrradbau riecht, besteht darin, ein Wörterbuch hinzuzufügen, dessen Schlüssel die ID der Listen sind und dessen Wert der ID des Absatzes entspricht (ja, es stellt sich heraus, dass jeder Absatz im Dokument eine eigene eindeutige ID hat), die auch die letzte in einer Liste ist. Vielleicht ist es ziemlich schwierig geschrieben, aber ich denke, wenn Sie sich die Implementierung ansehen, wird es etwas klarer:
public string Convert(Stream memStream)
{
    Dictionary<int, string> listEl = new Dictionary<int, string>();
    string xml = string.Empty;
    memStream.Position = 0;
    using (WordprocessingDocument doc = WordprocessingDocument.Open(memStream, false))
    {
        StringBuilder sb = new StringBuilder(1000); 
        sb.Append("<?xml version=\"1.0\"?><documents><document>");
        Body docBody = doc.MainDocumentPart.Document.Body;
        CreateDictList(listEl, docBody);
        foreach (var element in docBody.ChildElements)
        {
            string type = element.GetType().ToString();
            try
            {
                switch (type)
                {
                    case "DocumentFormat.OpenXml.Wordprocessing.Paragraph":
                        if (element.GetFirstChild<ParagraphProperties>() != null)
                        {
                            if (element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val != CurrentListID)
                            {
                                CurrentListID = element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
                                sb.Append($"<li id=\"{CurrentListID}\">");
                                InList = true;
                                ListParagraph(sb, (Paragraph)element);
                            }
                            else
                            {
                                ListParagraph(sb, (Paragraph)element);
                            }
                            if (listEl.ContainsValue(((Paragraph)element).ParagraphId.Value))
                            {
                                sb.Append($"</li id=\"{element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val}\">");
                            }
                            continue;
                        }
                        else
                        {
                            SimpleParagraph(sb, (Paragraph)element);
                            continue;
                        }
                    case "DocumentFormat.OpenXml.Wordprocessing.Table":
                        Table(sb, (Table)element);
                        continue;
                }
            }
            catch (Exception e)
            {
                continue;
            }
        }
        sb.Append(@"</document></documents>");
        xml = sb.ToString();
    }
    return xml;
}

  1. Dictionary<int, string> listEl = new Dictionary<int, string>(); — .


  2. using (WordprocessingDocument doc = WordprocessingDocument.Open(memStream, false))doc WordprocessingDocument, word, ( , OpenXML) .


  3. StringBuilder sb = new StringBuilder(1000); — xml.


  4. Body docBody = doc.MainDocumentPart.Document.Body; — ,


  5. CreateDictList(listEl, docBody);, foreach , :


    void CreateDictList(Dictionary<int, string> listEl, Body docBody)
    {
    foreach(var el in docBody.ChildElements)
    {
        if(el.GetFirstChild<ParagraphProperties>() != null)
        {
            int key = el.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
            listEl[key] = ((DocumentFormat.OpenXml.Wordprocessing.Paragraph)el).ParagraphId.Value;
        }
    }
    }

    GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val; — ( https://docs.microsoft.com/ru-ru/office/open-xml/open-xml-sdk ), . , , , )


  6. , , foreach . : . , , . , (, ) , . , . :


    string type = element.GetType().ToString();
                   try
    {
    switch (type)
    {
        case "DocumentFormat.OpenXml.Wordprocessing.Paragraph":
    
            if (element.GetFirstChild<ParagraphProperties>() != null) //  /  
            {
                if (element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val != CurrentListID)
                {
                    CurrentListID = element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
                    sb.Append($"<li id=\"{CurrentListID}\">");
                    InList = true;
                    ListParagraph(sb, (Paragraph)element);
                }
                else //  
                {
                    ListParagraph(sb, (Paragraph)element);
                }
                if (listEl.ContainsValue(((Paragraph)element).ParagraphId.Value))
                {
                    sb.Append($"</li id=\"{element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val}\">");
                }
                continue;
            }
            else //  
            {
                SimpleParagraph(sb, (Paragraph)element);
                continue;
            }
        case "DocumentFormat.OpenXml.Wordprocessing.Table":
    
            Table(sb, (Table)element);
            continue;
    }
    }

    try-catch , - , switch-case ( , , ). , - , .


  7. , ListParagraph(sb, (Paragraph)element); :


    void ListParagraph(StringBuilder sb, Paragraph p)
    {
    //  
    var level = p.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingLevelReference>().Val;
    // id 
    var id = p.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
    sb.Append($"<ul id=\"{id}\" level=\"{level}\"><p>{p.InnerText}</p></ul id=\"{id}\" level=\"{level}\">");
    }

    <ul>, id .


  8. , , SimpleParagraph(sb, (Paragraph)element);:


    void SimpleParagraph(StringBuilder sb, Paragraph p)
    {
    sb.Append($"<p>{p.InnerText}</p>");
    }

    , <p>


  9. Die Tabelle wird in der Methode verarbeitet Table(sb, (Table)element);:


    void Table(StringBuilder sb, Table table)
    {
    sb.Append("<table>");
    foreach (var row in table.Elements<TableRow>())
    {
    sb.Append("<row>");
    foreach (var cell in row.Elements<TableCell>())
    {
    sb.Append($"<cell>{cell.InnerText}</cell>");
    }
    sb.Append("</row>");
    }
    sb.Append("</table>");}

    Die Verarbeitung eines solchen Elements ist ziemlich trivial: Wir lesen die Zeilen, teilen sie in Zellen auf, nehmen Werte aus den Zellen, wickeln sie in Tags ein <cell>, die wir in Tags packen <row>und all dies einfügen <table>.



In diesem Zusammenhang schlage ich vor, die Aufgabe als gelöst für Dokumente im Format docx und xlsx zu betrachten.


Der Quellcode kann im Repository unter dem Link angezeigt werden


RTF- Konvertierungsartikel


All Articles