اضطررت مؤخرًا إلى التعامل مع الحاجة إلى الحصول على نص من المستندات المكتبية ( docx و xlsx و rtf و doc و xls و odt و ods ). كانت المهمة معقدة بسبب متطلبات تقديم النص بتنسيق xml بدون القمامة مع الهيكل الأكثر ملاءمة للتحليل الإضافي.
سقط قرار استخدام Interop على الفور بسبب طبيعته المرهقة ، والتكرار إلى حد كبير ، والحاجة إلى تثبيت MS Office على الخادم . ونتيجة لذلك ، تم إيجاد حل وتطبيقه على مشروع داخلي. ومع ذلك ، اتضح أن البحث معقد للغاية وليس تافهًا بسبب عدم وجود أي كتيبات يمكن الوصول إليها بشكل عام وقررت كتابة مكتبة في وقت فراغي لحل المشكلة المحددة ، وأيضًا إنشاء نوع من التعليمات للكتابة حتى يتمكن المطورون من قراءة كانت قادرة ، على الأقل بشكل سطحي ، على فهم المشكلة.
قبل الانتقال إلى وصف الحل الموجود ، أقترح أن تتعرف على بعض الاستنتاجات التي تم التوصل إليها نتيجة لبحثي:
- بالنسبة إلى منصة .Net ، لا يوجد حل جاهز للعمل مع جميع التنسيقات المدرجة ، مما سيجبرنا على تخصيص حلنا في الأماكن.
- لا تحاول العثور على دليل جيد للعمل مع Microsoft OpenXML على شبكة الإنترنت: للتعامل مع هذه المكتبة ، سيكون عليك أن تدخن StackOverflow ذات العيون الحمراء ، وأن تلعب مع المصحح.
- نعم ، تمكنت من ترويض التنين.
يجب أن أقول على الفور أن المكتبة ليست جاهزة بعد ، ولكن يتم كتابتها بنشاط (بقدر ما يسمح به وقت الفراغ). من المفترض أنه سيتم كتابة مشاركات منفصلة لكل تنسيق وبالتوازي مع نشرها ، سيتم تحديث المستودع على github ، حيث سيكون من الممكن الحصول على المصادر.
العمل مع xlsx و 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) :
- usingexcel . xlsx DocumentFormat.OpenXml SpreadsheetDocument.
 
 
- StringBuilder sb ( 1000 . StringBuilder , . , , . 
 
- shared ( ). , SpreadsheetDocument :
 - SharedStringTable sharedStringTable = doc.WorkbookPart.SharedStringTablePart.SharedStringTable.
 
 
- , 
 - 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>");
    }
 
 
- , :
 - string sheetName = doc.WorkbookPart.Workbook.Descendants<Sheet>().ElementAt(sheetIndex).Name.ToString();
 , , . , . , , shift+F9( ),- doc( )->WorkbookPart->Workbook Descendants(),- Sheet. , ( ). : 
 
 
 
- 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);
                }
            }
        }
 
 
- 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().

قبل أن أبدأ في الترميز ، أريد الانتباه إلى فارق بسيط مهم جدًا (نعم ، كما هو الحال في النكتة حول Petka و Vasily Ivanovich). عند تحليل القوائم ، خاصة عندما يتعلق الأمر بالقوائم المتداخلة ، قد تنشأ حالة عندما يتم فصل عناصر القائمة بنوع من إدراج النص أو الصورة أو أي شيء آخر. ثم يطرح السؤال متى نضع علامة إغلاق القائمة؟ اقتراحي ، رائحة العكازات وبناء الدراجات ، يأتي إلى إضافة قاموس ، مفاتيحه ستكون معرف القوائم ، وستتوافق القيمة مع معرف الفقرة (نعم ، تبين أن كل فقرة في المستند لها معرف فريد خاص بها) ، وهو الأخير أيضًا في القائمة. ربما تكون مكتوبة صعبة للغاية ، ولكن أعتقد أنه عندما تنظر إلى التنفيذ ، سيصبح أكثر وضوحًا إلى حد ما: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;
}
- Dictionary<int, string> listEl = new Dictionary<int, string>();— .
 
 
- using (WordprocessingDocument doc = WordprocessingDocument.Open(memStream, false))—- docWordprocessingDocument, word, ( , OpenXML) .
 
 
- StringBuilder sb = new StringBuilder(1000);— xml.
 
 
- Body docBody = doc.MainDocumentPart.Document.Body;— ,
 
 
- 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 ), . , , , )
 
 
- , , 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( , , ). , - , .
 
 
- , - ListParagraph(sb, (Paragraph)element);:
 
 - void ListParagraph(StringBuilder sb, Paragraph p)
{
var level = p.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingLevelReference>().Val;
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 .
 
 
- , , - SimpleParagraph(sb, (Paragraph)element);:
 
 - void SimpleParagraph(StringBuilder sb, Paragraph p)
{
sb.Append($"<p>{p.InnerText}</p>");
}
 
 - , - <p>
 
 
- تتم معالجة الجدول في الطريقة - 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>");}
 
 - معالجة مثل هذا العنصر تافهة للغاية: نقرأ الخطوط ، ونكسرها إلى خلايا ، ونأخذ قيمًا من الخلايا ، ونلفها في علامات - <cell>، والتي نجمعها في علامات- <row>ونضع كل هذا في الداخل- <table>.
 
 
في هذا الصدد ، أقترح اعتبار المهمة كما تم حلها للوثائق بتنسيق docx و xlsx.
يمكن الاطلاع على كود المصدر في المستودع على الرابط
مقالة تحويل Rtf