В этой статье я постараюсь рассказать об одном из возможных путей конвертации документов формата xls в xml.
Непосредственно моя задача выглядела примерно так:
Дано:
- 1-2 дня времени на поиск и реализацию решения
- Найденное решение должно быть настолько бесплатным, насколько это возможно
- Известна примерная структура, размер и содержание файла, которые должны быть обработаны, что снимает необходимость читать графики, диаграммы и иные медианные из документа.
- Использовать Interop, равно как и иные библиотеки, которые требуют установки Office нельзя, так как это создает лишнюю нагрузку на сервер и требует дополнительной установки офисного пакета.
Должен заметить, эта задача отнюдь не из простых, и решить ее, также как это было сделано с новыми офисными форматами (docx и xlsx), не получится по следующим причинам:
- Во-первых, Microsoft не предлагает каких-либо решений для чтения указанных форматов (Interopt в расчет не берем, так как его использование требует установки офисного пакета на сервере, а чтение данных из одного файлика будет сжирать необоснованно много оперативы).
- Во-вторых, вся информация в документах xls (равно как и в doc) хранится в бинарном виде и недоступна для свободного чтения.
- В-третьих, в интернете практически нет информации о библиотеках, которые бы решали задачу чтения информации из старых форматов excel и word, равно как и задачу преобразования старых форматов в новые.
Перерыв stackoverflow, Хабр, проконсультировавшись со старшими товарищами, а также обратившись к нижегородскому сообществу .Net разработчиков стало понятно, что наиболее верным способом решения поставленной задачи является конвертация xls в xlsx, ну а как работать с xlsx я уже знал.
Однако, нативной библиотеки от Microsoft, которая бы решала, казалось бы, столь очевидную проблему совместимости старых и новых офисных форматов, в природе не существует. Платные библиотеки требовали обоснования необходимости их покупки и, также, отдаляли момент начала их использования (на срок проведения закупочных процедур), а бесплатные решения, написанные для питона и линукса, требовали внесения существенных изменений в архитектуру проекта. И вот тут, казалось бы, самое время начинать рвать волосы из подмышек, но, как говорится, всегда найдется азиат!
Tony Qu является автором крутой библиотеки, написанной как раз для работы с офисными файлами. Изначально либа была написана для джавы, после чего портирована под .Net, правда с некоторыми ограничениями (в версии под .Net отсутствует возможность работы с документами doc).
Но, возможно, данная статья не появилась бы на свет, если бы не огромная ложка дегтя: пользователи библиотеки NPOI напрочь лишены какой-либо документации, описывающей алгорит��ы ее использования. Разумеется, ничто не мешает получить персональную поддержку от разработчика, уже за материальное вознаграждение.
Поэтому, думаю, представленное ниже решение, можно использовать не только для конвертации xls в xlsx, но и в качестве небольшого примера, демонстрирующего возможности либы.
Давайте кодить
Подготовка
- Устанавливаем через nuget библиотеку NPOI (в nuget лежит две либы: одна официальная от Tony Qu, а вторая не понятно от кого и якобы для netcore — ставим первую. Ничего плохого про вторую сказать не могу, так как я ей не пользовался, но миллионы
мухпользователей не могут ошибаться):

- Создаем класс XlsToXlsx и метод
public MemoryStream Convert(Stream sourceStream):
public MemoryStream Convert(Stream sourceStream) { // Открытие xls var source = new HSSFWorkbook(sourceStream); // Создание объекта для будущего xlsx var destination = new XSSFWorkbook(); // Копируем листы из xls и добавляем в xlsx for (int i = 0; i < source.NumberOfSheets; i++) { var xssfSheet = (XSSFSheet)destination.CreateSheet(source.GetSheetAt(i).SheetName); var hssfSheet = (HSSFSheet)source.GetSheetAt(i); CopyStyles(hssfSheet, xssfSheet); CopySheet(hssfSheet, xssfSheet); } // Возвращаем сконвертированный результат using (var ms = new MemoryStream()) { destination.Write(ms); return ms; } }
Как видите, основной метод конвертации работает со стримами. Не переживайте, для результата выполнения метода мы создадим обертки, которые позволят нам сохранять результат конвертации в файл (причем, как из файла, так и из потока).
За работу с файлами xls в библиотеке NPOI отвечает класс HSSFWorkbook, конструктор которого может принимать как стрим, так и путь до файла.
Наш будущий сконвертированный xlsx будет находиться в переменной типа XSSFWorkbook.
Далее запускаем цикл, в котором будет происходить копирование данных из xls в xlsx. Как мы видим, на каждой итерации цикла создаются по два объекта. Обратите внимание, что при создании объекта xssfSheet сразу же происходит копирование имени листа из объекта класса HSSFSheet.
В последствии, указанные объекты используются в качестве аргументов в функциях, копирующих стили листов и их содержимое.
Результат конвертации возвращается в виде потока типа MemoryStream
Разберем метод, отвечающий за копирование стилей
private void CopyStyles(HSSFSheet from, XSSFSheet to)
{
for (short i = 0; i <= from.Workbook.NumberOfFonts; i++)
{
CopyFont(to.Workbook.CreateFont(), from.Workbook.GetFontAt(i));
}
for (short i = 0; i < from.Workbook.NumCellStyles; i++)
{
CopyStyle(to.Workbook.CreateCellStyle(), from.Workbook.GetCellStyleAt(i), to.Workbook, from.Workbook);
}
}Как вы видите, для того, чтобы сохранить аутентичное внешнее представление, нам необходимо произвести копирование шрифтов из одного файла в другой, а также применить стили старого файла к соответствующим ячейкам нового.
За копирование шрифтов отвечает метод void CopyFont(IFont toFront, IFont fontFrom)
private void CopyFont(IFont toFront, IFont fontFrom)
{
toFront.Boldweight = fontFrom.Boldweight;
toFront.Charset = fontFrom.Charset;
toFront.Color = fontFrom.Color;
toFront.FontHeightInPoints = fontFrom.FontHeightInPoints;
toFront.FontName = fontFrom.FontName;
toFront.IsBold = fontFrom.IsBold;
toFront.IsItalic = fontFrom.IsItalic;
toFront.IsStrikeout = fontFrom.IsStrikeout;
}Думаю, из имен свойств можно сделать вывод об их назначении, поэтому комментировать я это не буду — вроде бы итак все предельно понятно.
Копирование и применение соответствующих стилей для каждой ячейки осуществляется в этом методе:
private void CopyStyle(ICellStyle toCellStyle, ICellStyle fromCellStyle, IWorkbook toWorkbook, IWorkbook fromWorkbook)
{
toCellStyle.Alignment = fromCellStyle.Alignment;
toCellStyle.BorderBottom = fromCellStyle.BorderBottom;
toCellStyle.BorderDiagonal = fromCellStyle.BorderDiagonal;
toCellStyle.BorderDiagonalColor = fromCellStyle.BorderDiagonalColor;
toCellStyle.BorderDiagonalLineStyle = fromCellStyle.BorderDiagonalLineStyle;
toCellStyle.BorderLeft = fromCellStyle.BorderLeft;
toCellStyle.BorderRight = fromCellStyle.BorderRight;
toCellStyle.BorderTop = fromCellStyle.BorderTop;
toCellStyle.BottomBorderColor = fromCellStyle.BottomBorderColor;
toCellStyle.DataFormat = fromCellStyle.DataFormat;
toCellStyle.FillBackgroundColor = fromCellStyle.FillBackgroundColor;
toCellStyle.FillForegroundColor = fromCellStyle.FillForegroundColor;
toCellStyle.FillPattern = fromCellStyle.FillPattern;
toCellStyle.Indention = fromCellStyle.Indention;
toCellStyle.IsHidden = fromCellStyle.IsHidden;
toCellStyle.IsLocked = fromCellStyle.IsLocked;
toCellStyle.LeftBorderColor = fromCellStyle.LeftBorderColor;
toCellStyle.RightBorderColor = fromCellStyle.RightBorderColor;
toCellStyle.Rotation = fromCellStyle.Rotation;
toCellStyle.ShrinkToFit = fromCellStyle.ShrinkToFit;
toCellStyle.TopBorderColor = fromCellStyle.TopBorderColor;
toCellStyle.VerticalAlignment = fromCellStyle.VerticalAlignment;
toCellStyle.WrapText = fromCellStyle.WrapText;
toCellStyle.SetFont(toWorkbook.GetFontAt((short)(fromCellStyle.GetFont(fromWorkbook).Index + 1)));
}Опять же, из названия свойств можно сделать вывод о том, что и куда копируется. Единственное, обратите внимание на то, что применение конкретных семейств шрифтов происходит через вызов метода SetFont, а не путем прямого присвоения значения через свойства класса.
Копирование данных из файла xls в новый файл xlsx
Перейдем к процессу копирования содержимого из файла xls в новый файл xlsx.
private void CopySheet(HSSFSheet source, XSSFSheet destination)
{
var maxColumnNum = 0;
var mergedRegions = new List<CellRangeAddress>();
for (int i = source.FirstRowNum; i <= source.LastRowNum; i++)
{
var srcRow = (HSSFRow)source.GetRow(i);
var destRow = (XSSFRow)destination.CreateRow(i);
if (srcRow != null)
{
CopyRow(source, destination, srcRow, destRow, mergedRegions);
// поиск максимального номера ячейки в строке
if (srcRow.LastCellNum > maxColumnNum)
{
maxColumnNum = srcRow.LastCellNum;
}
}
}
// копируем ширину столбцов исходного документа
for (int i = 0; i <= maxColumnNum; i++)
{
destination.SetColumnWidth(i, source.GetColumnWidth(i));
}
}Логика данного метода сводится к вызову метода CopyRow(source, destination, srcRow, destRow, mergedRegions) — что происходит в нем расскажу чуть ниже. Помимо вызова указанного метода, в CopySheet происходит поиск номер последней заполненной ячейки в строке. Это необходимо для того, чтобы верно скопировать ширину каждого столбца (да, конвертация xls в xlsx предусматривает даже такой нюанс).
Разберем метод void CopyRow(HSSFSheet srcSheet, XSSFSheet destSheet, HSSFRow srcRow, XSSFRow destRow, List<CellRangeAddress> mergedRegions):
private void CopyRow(HSSFSheet srcSheet, XSSFSheet destSheet, HSSFRow srcRow, XSSFRow destRow, List<CellRangeAddress> mergedRegions)
{
// Копирование высоты строки
destRow.Height = srcRow.Height;
for (int j = srcRow.FirstCellNum; srcRow.LastCellNum >= 0 && j <= srcRow.LastCellNum; j++)
{
var oldCell = (HSSFCell)srcRow.GetCell(j);
var newCell = (XSSFCell)destRow.GetCell(j);
if (oldCell != null)
{
// создание новой ячейки в новой таблице
if (newCell == null)
{
newCell = (XSSFCell)destRow.CreateCell(j);
}
CopyCell(oldCell, newCell);
// Ниже идет обработка объединенных ячеек
// Проверка на вхождение текущей ячейки в число объединенных
var mergedRegion = GetMergedRegion(srcSheet, srcRow.RowNum,
(short)oldCell.ColumnIndex);
// Если ячейка является объединенной
if (mergedRegion != null)
{
// Проверяем обрабатывали ли мы уже группу объединенных ячеек или нет
var newMergedRegion = new CellRangeAddress(mergedRegion.FirstRow,
mergedRegion.LastRow, mergedRegion.FirstColumn, mergedRegion.LastColumn);
// Если не обрабатывали, то добавляем в текущий диапазон объединенных ячеек текущую ячейку
if (IsNewMergedRegion(newMergedRegion, mergedRegions))
{
mergedRegions.Add(newMergedRegion);
destSheet.AddMergedRegion(newMergedRegion);
}
}
}
}
}Этот метод выполняет две функции:
- Во-первых, метод копирует данные из "простых" ячеек файла с расширением xls в "простые" ячейки файла с расширением xlsx
- Во-вторых, метод обрабатывает случаи, когда ячейка находится внутри группы объединенных ячеек. При этом, проверяется, относится ли данная ячейка к уже существующей группе объединенных ячеек, или же является первой выявленной ячейкой новой группы.
Метод копирования данных и стилей для каждой ячейки:
private void CopyCell(HSSFCell oldCell, XSSFCell newCell)
{
CopyCellStyle(oldCell, newCell);
CopyCellValue(oldCell, newCell);
}В этом методе происходит следующее:
- Во-первых, копируются стили:
private void CopyCellStyle(HSSFCell oldCell, XSSFCell newCell) { if (oldCell.CellStyle == null) return; newCell.CellStyle = newCell.Sheet.Workbook.GetCellStyleAt((short)(oldCell.CellStyle.Index + 1)); } - Во-вторых, копируется содержание каждой ячейки:
void CopyCellValue(HSSFCell oldCell, XSSFCell newCell) { switch (oldCell.CellType) { case CellType.String: newCell.SetCellValue(oldCell.StringCellValue); break; case CellType.Numeric: newCell.SetCellValue(oldCell.NumericCellValue); break; case CellType.Blank: newCell.SetCellType(CellType.Blank); break; case CellType.Boolean: newCell.SetCellValue(oldCell.BooleanCellValue); break; case CellType.Error: newCell.SetCellErrorValue(oldCell.ErrorCellValue); break; case CellType.Formula: newCell.SetCellFormula(oldCell.CellFormula); break; default: break; } }
Как можно видеть, в указанном методе происходит не слепое копирование содержимого ячеек, а с сохранением типов данных и исходного функционала (когда, ячейка содержит функцию).
Обработка объединенных ячеек
Вернемся к обработке ячеек, включенных в группу объединенных.
Как было показано выше, за это отвечают методы CellRangeAddress GetMergedRegion(HSSFSheet sheet, int rowNum, short cellNum) и bool IsNewMergedRegion(CellRangeAddress newMergedRegion, List<CellRangeAddress> mergedRegions):
CellRangeAddress GetMergedRegion(HSSFSheet sheet, int rowNum, short cellNum)
{
for (var i = 0; i < sheet.NumMergedRegions; i++)
{
var merged = sheet.GetMergedRegion(i);
if (merged.IsInRange(rowNum, cellNum))
{
return merged;
}
}
return null;
}Так, мы просто получаем все области объединенных ячеек для конкретного листа, после чего проверяем, входит ли наша ячейка хоть в какой-нибудь из диапазонов объединенных ячеек. Если входит, то то метод возвращает тот диапазон объединенных ячеек, к которому относится проверяемая ячейка. В противно случае, вернется null.
bool IsNewMergedRegion(CellRangeAddress newMergedRegion,List<CellRangeAddress> mergedRegions)
{
return !mergedRegions.Any(r =>
r.FirstColumn == newMergedRegion.FirstColumn &&
r.LastColumn == newMergedRegion.LastColumn &&
r.FirstRow == newMergedRegion.FirstRow &&
r.LastRow == newMergedRegion.LastRow);
}Указанный метод сопоставляет текущую область объединенных ячеек с той областью, к которой относится текущая ячейка, которая входит в состав нескольких объединенных ячеек. Как можно увидеть, сравнение происходит по 4-м крайним точкам каждой из областей (первая и последняя колонка, первая и последняя строчка проверяемой области).
На этом процесс преобразования xls в xlsx можно считать законченным.
С моей стороны будет свинством не сказать, что данный код был нагло сплагиачен со stackoverflow, который в свою очередь представляет собой переработанное решение по конвертации xlsx в xls, которое также лежит на stackoverflow.
Дополнительный функционал
Предлагаю добавить возможность конвертации с записью полученного результата в файл:
- из потока
public void ConvertToXlsxFile(MemoryStream stream, string path) { var result = Convert(stream); using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate)) { fs.Write(result.ToArray()); } } - из файла:
public void ConvertToXlsxFile(string xlsPath, string destPath) { MemoryStream result; using (FileStream fs = new FileStream(xlsPath, FileMode.Open)) { result = Convert(fs); } using (FileStream fs = new FileStream(destPath, FileMode.OpenOrCreate)) { fs.Write(result.ToArray()); } }
Также, не будем забывать, что цикл статей посвящен конвертации разных текстовых файлов в xml:
public class XlsToXml : IConvertable
{
public string Convert(Stream stream)
{
XlsToXlsx excelConvert = new XlsToXlsx();
Stream str = excelConvert.Convert(stream);
XlsxToXml converter = new XlsToXml();
return converter.Convert(str);
}
public string ConvertByFile(string path)
{
using (FileStream fs = File.OpenRead(path))
{
return Convert(fs);
}
}
}Как видите, в методе данного класса используется объект класса XlsxToXml, который реализует логику конвертации xlsx в xml, описанную в статье по этой ссылке.
С полным кодом программы Вы можете ознакомиться здесь
