Конвертация текстовых документов в xml на С#

Недавно мне пришлось столкнуться с необходимостью достать текст из офисных документов (docx, xlsx, rtf, doc, xls, odt и ods). Задача осложнялась требованием представить текст в формате xml без мусора с максимально удобной для дальнейшего парсинга структурой.


Решение использовать Interop сразу отпало по причине его громоздкости, во многом избыточности, а также необходимости устанавливать на сервер MS Office. В результате, решение было найдено и воплощено на внутреннем проекте. Однако, поиск оказался настолько сложен и не тривиален в силу отсутствия каких-либо общедоступных мануалов, что мной было принято решение написать в свободное от работы время библиотеку, которая решала бы указанную задачу, а также создать написать что-то вроде инструкции, чтобы разработчики прочитав ее смогли, хотя бы поверхностно, разобраться в вопросе.


Прежде, чем перейти к описанию найденного решения, предлагаю ознакомиться с некоторыми выводами, которые были сделаны в результате моих изысканий:


  1. Для платформы .Net не существует какого-либо готового решения для работы со всеми перечисленными форматами, что заставит нас местами кастылизовывать наш солюшн.
  2. Не пытайтесь в сети найти хороший мануал по работе с Microsoft OpenXML: чтобы разобраться с этой библиотекой придется изрядно покрасноглазить, покурить StackOverflow и поиграться с отладчиком.
  3. Да, мне все таки, удалось приручить дракона.

Сразу оговорюсь, что в настоящий момент библиотека еще не готова, но она активно пишется (на столько, на сколько это позволяет свободное время). Предполагается, что будут написаны отдельные посты для каждого формата и параллельно, вместе с их публикацией, будет обновляться репозиторий на гитхабе, откуда можно будет получить исходники.


Работа с 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) происходит следующее:


  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(ячейка).


Прежде, чем начать кодить, хочу обратить внимание на один очень важный ньюанс (да-да, как в анекдоте про Петьку и Василия Ивановича). При разборе списков, особенно это касается вложенных, может возникнуть ситуация, когда пункты списка разделены какой-то вставкой текста, изображения или вообще чего угодно. Тогда возникает вопрос, когда же нам ставить закрывающий тег списка? Мое предложение попахивая костылезацией и велосипедостроением сводится к добавлению словаря, ключами которого будут выступать id списков, а значение будет соответствовать id параграфа (да, оказывается каждый параграф в документе имеет свой уникальный id), который одновременно является последним в каком-то списке. Пожалуй, написано довольно сложно, но, думаю, когда посмотрите на реализацию, станет несколько понятнее:
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. Таблица обрабатывается в методе 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

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 32

    +1

    Почему не решали через XSLT?

      0
      Данный выбор обусловлен «традициями компании» — у других сотрудников был опыт работ OpenXML и, когда я разрабатывал данное решение, то знал, что в случае возникновения вопросов могу обратиться к старшим товарищам. Да и в целом, мне показалось логичным использовать нативное решение от Microsoft.
      +5

      Что это? Сборка xml через string builder?


      А если у вас в строке будет кавычка или символ >, что будет в итоге?

        0
        Да, все верно, именно через StringBuilder собирается xml. Если нам необходимо вставить угольные скобки, то вставляем их как любой другой символ, если нам нужно вставить кавычки, то просто экранируем их.
        Однако, как таковой проблемы нет, так как мы заведомо знаем где и какие теги будут вставлены (мы же их сами в коде и расставляем).
        Если есть сомнения, можете попробовать скачать код с репозитория и попробовать потестить у себя. Если вдруг заметите неадекватное поведение программы, буду рад комментариям.
          +2

          sb.Append($"<sheet name=\"{sheetName}\">");


          Вот здесь например, если имя листа будет что-то типа "My parent's accounting", то ваша xml будет невалидна


          Я почему-то думал, xml правильно собирать через XmlDocument, XDocument или сериализуя класс (я предпочитаю использовать 1 и 3 варианты, зависит от задачи)

            0

            Сейчас перепроверил — все успешно собирается. Прилагаю скрин:


            На счет предложенных Вами вариантов сборки xml я ничего возразить не могу. Они вполне могли бы использоваться. Просто в данном случае, мне показалось ограничиться StringBuilder'ом логичным решением в силу его компактности и полного удовлетворения имеющихся потребностей в рамках решения конкретной задачи. Конечно, если бы использовались более разнообразные теги с большим количеством атрибутов, логичным было бы подключение дополнительных библиотек.

              +1

              Собирается-собирается, но потом ни один парсер такое не откроет

                0

                Почему? Аналогичное решение, в настоящий момент, используется на внутреннем продукте компании (как раз парсер) и пока, тьфу-тьфу-тьфу, все работает.
                Если подскажете, с каким парсером можно поиграться и проверить работоспособность приведенного решения, то буду искренне очень признателен.

                  0

                  Ок. перепроверил. Условия чуть изменились — попробуйте назвать лист как "Procter&Gamble"


                  Вот правила:


                  " is replaced with &quot;
                  & is replaced with &amp;
                  < is replaced with &lt;
                  > is replaced with &gt;

                  А саму валидацию можно провести:


                  • в notepad++ плагином xml tools
                  • гугля "xml validate online"
                  • попытавшись загрузив эту xml в c# XmlDocument.
                    +1
                    • в браузере открыть XML.

                    Вот например вполне валидный XML.


                    <test test1=" '>' " test2=' " '> > </test>
              +2
              Вот здесь например, если имя листа будет что-то типа "My parent's accounting", то ваша xml будет невалидна

              Чего это вдруг? Одинарные кавычки валидны внутри двойных и наоборот. Двойные внутри двойных или одинарные внутри одинарных нужно заменять мнемониками.

                0

                ответил выше

          +1

          Есть вот такое платное решение для некоторых из указанных форматов:
          https://www.aspose.com/

            0

            Спасибо! Это решение тоже было рассмотрено, но было найден бесплатный вариант для всех перечисленных форматов. В течении сегодняшнего дня планирую выложить пост о конвертации rtf в xml. Потом расскажу про doc и xls, а дальше про odt и ods (но с ними все довольно тривиально).

              +1
              А есть ещё вот такое бесплатное: www.e-iceblue.com/Introduce/free-xls-component.html
              С ограничениями… Но мне в своё время очень помогло.
                0

                Шикарная и простая библиотека!
                Но, к сожалению, у нее ограничения(
                Соберусь с силами и выложу пост про конвертацию xls и doc — вот там вообще жара, конечно!
                И, самое паршивое, ну очень сложно было найти решение под .Net, зато под линухом на питончике делается одной командной строкой в терминале)

              +1
              Автор молодец! Технических ограничений по объему обрабатываемых документов нет? 5-7 ГБ Экселя потянет?
                0

                Спасибо, если это не сарказм) Посмотрел темы Ваших статей — видно, что вы уже давно и плотно в it! Мне вот эта статья понравилась — даже повторить захотелось)
                Если по теме вопроса, то на самом деле размер файла Excel с большой долей вероятности может быть ограничен спецификацией. Так, для 32-битных систем он не может быть больше 500-700Мб, а для online-версии предельный размер файла и вовсе составляет 250Мб. То что касается 64 разрядных систем, фактически ограничения накладываются только железом. Со спецификацией можно ознакомиться здесь
                Отсюда можно сделать следующие выводы:


                1. Вероятность того, что кто-то создаст файла такого огромного размера крайне мала.
                2. Есть очень высокая вероятность упереться в технические ограничения (банальная нехватка памяти). Поясню. Вся информация в документе xlsx хранится в zip-архиве, что означает, что закрытый файл размером 5-7 Гб занимает ощутимо меньше памяти на дисковом пространстве, нежели этот же файл, но загруженный и открытый в программе, особенно, если осуществляется какая-то обработка хранимой в нем информации. Думаю, тот кто в студенчестве писал какой-нибудь архиватор типа Хафмана, сейчас прекрасно может представить примерную разницу между двумя этими состояниями файлов.

                В общем, я хочу сказать, что, если речь идет о таких огромных файлах, то для работы с ними необходимо иметь как минимум соответствующее железо, а программа, которая будет использовать предложенное решение должна быть скомпилирована под 64-х разрядную систему и тогда, скорее всего, файл будет обработан и сконвертирован.
                Однако, не стоит упускать из виду высокую вероятность неожиданного поведения GB при работе с такими объемами файлов, а также непредсказуемого поведения операционной системы, которая может принудительно кильнуть разожравшийся процесс.
                Вот принимая во внимание все перечисленное, можно сказать, что ограничений на объемы обработки документов нет, и, при наличии соответствующего железа, файл объемом 5-7 Гб (а это около 16-20 Гб оперативы на хосте) должен будет быть обработан.
                Но, скажу честно, такие кейсы я сам не тестил.

                  +1
                  Ну так о том и вопрос. Умеет ли программа обрабатывать файл по частям или надо полностью в память грузить?
                    +1
                    Судя по методу string Convert(Stream stream) как минимум результат будет в памяти.
                      0

                      Функционал обработки файла по частям не реализован. Честно говоря, я, на данный момент, очень слабо себе представляю, как можно разбить xlsx на части. Если с отдельной обработкой листов еще можно что-то придумать, то с разбивкой листов по строкам — все нет так однозначно. Тем более, если задача стоит разбить документ без предварительной его загрузки в оперативную память.

                        +1
                        В этой задаче две проблемы, связанные с чрезмерным потреблением памяти.
                        1. Чтение исходного файла
                        2. Запись результата
                        Что касается второй проблемы, то у вас уже в определении интерфейса IConvertable (а именно метода string Convert(Stream stream)) заложена невозможность работы с большими файлами. Разумнее возращать Stream и не пользоваться StringBuilder`ом, а писать напрямую в поток. Также данная проблема проявится не только при работе с большими файлами, но а еще и с большим количеством средних по размеру. Результат работы будет возвращен в виде большой строки, а отсюда все проблемы с LOH. Другими словами при парсинге большого количества сравнительно небольших файлов (>4KB тескта в нем), память будет стремительно утекать.
                        Что касается 1го пункта, то тут я с вами не соглашусь. OpenXml умеет работать с большими файлами без чрезмерного потребления памяти (как с xslx, так и c docx):
                        OpenXmlReader reader = OpenXmlReader.Create(doc.MainDocumentPart);
                        while (reader.Read())
                        {
                        if (reader.ElementType == typeof(Document))
                        {
                        }
                        }
                        Да сложнее с таким подходом работать, но зато не жрет память.
                    +1
                    Спасибо за ответ. Да, это не сарказм, это реалии 2009-2010 года.
                    Как вы видите в процитированной вами моей статье, сидим мы тут на Music XML 2.0 и плотно занимаемся и другими XML, не связанными с музыкальной промышленностью.
                    Опять же, процитированную вами спецификацию Excel для Office 365, Excel 2019, Excel 2016, Excel 2013 писали люди далекие от реалии наших IT.
                    Excel — самая что ни на есть отечественная база данных, которую сейчас используют минимум 50 миллионов пользователей по всей стране. Более того, о чем не пишут,
                    но надо упомянуть из уважения к ресурсу, есть верхний предел числа ошибок в формулах,
                    после которого его открыть фактически нельзя никакими стандартными средствами.
                    Более того, это число — степень двойки — 2^12 = 4096 ошибок. )

                    Вот поэтому мой вопрос немного глубже. Есть ли гарантия, что все используемые вами
                    библиотеки под Windows 7 или 10 потянут промышленные объемы данных, нет
                    ли импортов библиотек из серии .Net for MS Office 2010-2013 или С# для этих же целей,
                    версии патчей MS XML Parser 6 или 4.

                    Я понимаю, что для ваших целей может быть ограничения не достигнуты, но широким массам будет приятно осознавать, что купленные мощности есть куда девать.

                    128GB на рабочей станции — это уже реалии нашего дня, может быть 2-3 следующих лет.
                    Почему это так — тоже есть в моей статье. Жду ответа.

                      0

                      Очень интересная информация на счет предела в количестве ошибок — я, честно говоря, даже не знал, что такие есть. И то, что такое количество пользователей у Excel я как-то тоже не задумывался)
                      На счет дополнительно установленных пакетов — их нет. Использую только OpenXML и все.
                      Мне вот тоже сейчас стало интересно, а какой максимальный размер файла удастся обработать, но у меня сейчас под рукой только ноут с 16 Гб оперативы.
                      Кстати, на счет обработки огромных файлов xlsx со сложным содержимым, возможно, имеет смысл как раз использовать Interop, так как файл в таком случае будет открываться и обрабатываться непосредственно в среде Excel, что, теоретически, поможет избежать каких-нибудь не очевидных ошибок.

                      +1
                      Статья правильная. 2 года назад решал похожую задачу через OpenXml.

                      Вероятность того, что кто-то создаст файла такого огромного размера крайне мала.


                      Увы, вывод неправильный. Мой сценарий был очень простой. Многостраничный опросник по степени удовлетворенности сотрудников в компании численностью 32 000 человек, под 100 вопросов. Вывод в эксельку.

                      Есть очень высокая вероятность упереться в технические ограничения


                      Так и было. Погуглил и нашел, как стримить zip. Все ограничения пропали.
                    0

                    Не очень удачные названия методов.


                    1. XxxxProcess() — должен быть ProcessXxxxx()
                    2. XxxParagraph(), Table() — это названия классов/конструкторов, а не методов. Где глагол?
                      +1
                      По работе часто приходится обрабатывать файлы XLSX.
                      В последнее время довелось работать с огромными таблицами (порядка 200 столбцов на несколько тысяч строк), и если открывать их только на чтение, то все нормально, но вот запись файла могла происходить несколько минут.
                      Начал пилить свой велосипед на Python, но к сожалению (или к счастью), дело не зашло далеко (обошелся другими хаками вроде уменьшения таблиц, где это возможно):
                      github.com/ChronoMonochrome/xlsxutil
                        0
                        С doc, docx, xls, xlsx умеет работать эта библиотека. Зависимостей не требует. Кросплатформенна с некоторыми ограничениями.
                          0

                          Как раз про опыт ее использования планирую рассказать в следующей статье. Действительно, хорошая, еще и кросплатформенная, библиотека, но со своими приколами)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.