Convierta documentos de texto a xml en C #

Recientemente, tuve que lidiar con la necesidad de obtener texto de documentos de Office ( docx, xlsx, rtf , doc, xls, odt y ods ). La tarea se complicó por el requisito de presentar el texto en formato xml sin basura con la estructura más conveniente para un análisis posterior.


La decisión de usar Interop inmediatamente se derrumbó debido a su volumen, de muchas maneras redundancia, y también a la necesidad de instalar MS Office en el servidor . Como resultado, se encontró e implementó una solución en un proyecto interno. Sin embargo, la búsqueda resultó ser tan complicada y no trivial debido a la falta de manuales accesibles en general que decidí escribir una biblioteca en mi tiempo libre que resolvería la tarea especificada, y también crear un tipo de instrucción para que los desarrolladores lean ella fue capaz, al menos superficialmente, de entender el problema.


Antes de continuar con la descripción de la solución encontrada, le sugiero que se familiarice con algunas de las conclusiones que se obtuvieron como resultado de mi investigación:


  1. Para la plataforma .Net, no existe una solución preparada para trabajar con todos los formatos enumerados, lo que nos obligará a castilizar nuestra solución en algunos lugares.
  2. No intente encontrar un buen manual sobre cómo trabajar con Microsoft OpenXML en la red: para lidiar con esta biblioteca tendrá que enrojecer, fumar StackOverflow y jugar con el depurador.
  3. Sí, todavía logré domar al dragón.

Inmediatamente haré una reserva de que en este momento la biblioteca aún no está lista, pero se está escribiendo activamente (tanto como lo permita el tiempo libre). Se supone que se escribirán publicaciones separadas para cada formato y en paralelo, junto con su publicación, se actualizará el repositorio en el github, desde donde será posible obtener las fuentes.


Trabaja con xlsx y 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().


Antes de comenzar a codificar, quiero prestar atención a un matiz muy importante (sí, como en el chiste sobre Petka y Vasily Ivanovich). Al analizar listas, especialmente cuando se trata de listas anidadas, puede surgir una situación en la que los elementos de la lista están separados por algún tipo de inserción de texto, imagen o cualquier otra cosa. Entonces surge la pregunta, ¿cuándo ponemos la etiqueta de cierre de la lista? Mi sugerencia, que huele a muletas y construcción de bicicletas, se reduce a agregar un diccionario, cuyas claves serán la identificación de las listas, y el valor corresponderá a la identificación del párrafo (sí, resulta que cada párrafo del documento tiene su propia identificación única), que también es el último de una lista. Quizás esté escrito bastante difícil, pero creo que cuando miras la implementación, se volverá algo más claro:
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. La tabla se procesa en el método 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>");}

    El procesamiento de dicho elemento es bastante trivial: leemos las líneas, las dividimos en celdas, tomamos valores de las celdas, las envolvemos en etiquetas <cell>, que empacamos en etiquetas <row>y ponemos todo esto dentro <table>.



Sobre esto, propongo considerar la tarea como resuelta para documentos en formato docx y xlsx.


El código fuente se puede ver en el repositorio en el enlace


Artículo de conversión de RTF


All Articles