Converter documentos de texto em xml em c #

Recentemente, tive que lidar com a necessidade de obter texto de documentos do office ( docx, xlsx, rtf , doc, xls, odt e ods ). A tarefa foi complicada pelo requisito de apresentar o texto no formato xml sem lixo, com a estrutura mais conveniente para análise posterior.


A decisão de usar o Interop desapareceu imediatamente devido à sua natureza complicada, principalmente redundância e à necessidade de instalar o MS Office no servidor . Como resultado, uma solução foi encontrada e implementada em um projeto interno. No entanto, a pesquisa acabou sendo tão complicada e não trivial devido à falta de manuais geralmente acessíveis que eu decidi escrever uma biblioteca no meu tempo livre que resolvesse a tarefa especificada, além de criar um tipo de instrução para escrever para que os desenvolvedores leiam ela foi capaz, pelo menos superficialmente, de entender o problema.


Antes de prosseguir com a descrição da solução encontrada, sugiro que você se familiarize com algumas das conclusões que foram feitas como resultado de minha pesquisa:


  1. Para a plataforma .Net, não existe uma solução pronta para trabalhar com todos os formatos listados, o que nos forçará a castilizar nossa solução em alguns locais.
  2. Não tente encontrar um bom manual sobre como trabalhar com o Microsoft OpenXML na rede: para lidar com esta biblioteca, você terá que ficar com os olhos muito vermelhos, fumar o StackOverflow e brincar com o depurador.
  3. Sim, eu ainda consegui domar o dragão.

Farei imediatamente uma reserva de que, no momento, a biblioteca ainda não está pronta, mas está sendo ativamente escrita (tanto quanto o tempo livre permitir). Supõe-se que as postagens separadas para cada formato serão escritas e, paralelamente, juntamente com a publicação, o repositório no github será atualizado, de onde será possível obter as fontes.


Trabalhar com xlsx e 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 começar a codificar, quero prestar atenção a uma nuance muito importante (sim, como na piada sobre Petka e Vasily Ivanovich). Ao analisar listas, especialmente quando se trata de listas aninhadas, pode ocorrer uma situação em que os itens da lista são separados por algum tipo de inserção de texto, imagem ou qualquer outra coisa. Então surge a pergunta: quando colocamos a tag de fechamento da lista? Minha sugestão, o cheiro de muletas e construção de bicicletas, se resume a adicionar um dicionário, cujas chaves serão o ID das listas e o valor corresponderá ao ID do parágrafo (sim, verifica-se que cada parágrafo do documento tem seu próprio ID exclusivo), que também é o último de uma lista. Talvez esteja escrito bastante difícil, mas acho que quando você olha para a implementação, ela se torna um pouco mais clara:
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. A tabela é processada no 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>");}

    O processamento de um elemento desse tipo é bastante trivial: lemos as linhas, dividimos em células, pegamos valores das células, envolvemos em tags <cell>, as quais empacotamos em tags <row>e colocamos tudo isso dentro <table>.



Sobre isso, proponho considerar a tarefa como resolvida para documentos nos formatos docx e xlsx.


O código fonte pode ser visualizado no repositório no link


Artigo de conversão de rtf


All Articles