使用Open Xml SDK 读取 Excel

c#

首先需要添加包引用: DocumentFormat.OpenXml。

DOM 和 SAX

读取Excel有两种方式,一种是DOM(文档对象模型),另一种是SAX(Simple API for XML )。

DOM 是一次性把文档全部读取到内存中,适用于不太大的文档。

SAX 则是通过流的方式读取,适用于比较大的文档。

DOM 代码如下:

string fileName = "Sample.xlsx";
using (SpreadsheetDocument spreadsheetDocument =
        SpreadsheetDocument.Open(fileName, false))
{
    WorkbookPart workbookPart = spreadsheetDocument.WorkbookPart!;
    WorksheetPart worksheetPart = workbookPart.WorksheetParts.First();
    SheetData sheetData = worksheetPart.Worksheet.Elements<SheetData>().First();

    //逐行读取
    foreach (Row r in sheetData.Elements<Row>())
    {        
        //循环行内所有单元格
        foreach (Cell cell in r.Elements<Cell>())
        {
            //读取单元格
        }
    }
}

SAX 代码如下:

string fileName = "Sample.xlsx";
using (SpreadsheetDocument spreadsheetDocument =
        SpreadsheetDocument.Open(fileName, false))
{
    WorkbookPart workbookPart = spreadsheetDocument.WorkbookPart!;
    WorksheetPart worksheetPart = workbookPart.WorksheetParts.First();

    OpenXmlReader reader = OpenXmlReader.Create(worksheetPart);
    //逐个节点读取
    while (reader.Read())
    {
        if (reader.ElementType == typeof(Row) && reader.IsStartElement)
        {
            //检测到行节点,则加载整行            
            Row? r = reader.LoadCurrentElement() as Row;
            //循环行内所有单元格
            foreach (Cell cell in r!.Elements<Cell>())
            {
                //读取单元格
            }
        }
    }
}

读取单元格的值

通过前面的代码,已经取到了单元格的实例(一个Cell的对象),想象中取值应该很简单了,但其实不然。

通过以下枚举可以知道单元格有不用类型的值

public enum CellValues
{
    Boolean,
    Number,
    Error,
    SharedString,
    String,
    InlineString,
    Date
}

因此需要根据不同类型的值,采取不同的方法,如下所示:

object? GetCellValue(Cell c)
{
    //取到单元格的文本值,如果有公式,则是计算过的值
    //其中 c.CellFormula 里面包含了公式的定义
    string? value = c.CellValue?.Text;

    if (c.DataType != null)
    {
        //根据不同的DataType采取不同的取值方式
        switch (c.DataType.Value)
        {
            case CellValues.SharedString:
                //共享字符串存在单独的文件里面,这里得到的只是一个数字,需要额外的取值方法,将在下面介绍
                return (string?)null;
            case CellValues.Date:
                //日期取到的是个数字,需要转换
                if (!string.IsNullOrEmpty(value) && double.TryParse(value, out double dtValue))
                {
                    return System.DateTime.FromOADate(dtValue);
                }
                return (DateTime?)null;
            case CellValues.Boolean:
                if (!string.IsNullOrEmpty(value))
                {
                    if (value.Length < 2)
                    {
                        return value?.ToUpperInvariant() == "T" || value == "1";
                    }
                    if (bool.TryParse(value, out bool boolValue))
                    {
                        return boolValue;
                    }
                }
                return (bool?)null;
            case CellValues.Number:
                if (!string.IsNullOrEmpty(value) && decimal.TryParse(value, out decimal number))
                {
                    return number;
                }
                return (decimal?)null;
            default: break;
        }
    }

    return value;
}

读取字符串

大部分字符串都是以SharedString的方式保存的,SharedString相当于是一个表,里面保存了所有的字符串,单元格的CellValue保存的只是SharedString里面一个索引值。

因此需要先取到SharedString的表格,然后根据索引去找对应的字符串。

相关代码如下:

//从workbook取SharedStringTable
var stringTable = workbookPart.GetPartsOfType<SharedStringTablePart>()
                .FirstOrDefault()?.SharedStringTable;
//根据索引值从SharedStringTable取值
stringTable.ElementAt(strIndex).InnerText

读取日期

通过前面的代码知道单元格包含Date类型的值,但实际用的时候发现,对于一个日期字段,DataType 是null 值,但是CellValue 却是数字。

这就很让人费解了,如果事先知道某一列为日期,那还可以通过DateTime.FromOADate方法来转成日期。

如果不知道列的类型,想做动态转换应该怎么做呢?

说实话笔者搜了一圈都没找到合适的方法,真不知道微软为什么留了这么一个bug。

一个折中的办法是通过单元格的格式来判断是否为日期型,即在Excel里面看到的这一块:

format

通过如下链接
https://github.com/ClosedXML/ClosedXML/wiki/NumberFormatId-Lookup-Table
可以知道14到22为默认的日期格式。

相关代码如下:

//取到样式表
var styleSheet = workbookPart.GetPartsOfType<WorkbookStylesPart>()
    .FirstOrDefault()?.Stylesheet;
//根据单元格的StyleIndex取到Format
var cellFormat = styleSheet.CellFormats
    .ChildElements[int.Parse(c.StyleIndex.InnerText)] as CellFormat;

//通过判断cellFormat.NumberFormatId.Value在14和22之间即可

当然这个方法有很大的局限性,各个国家的日期格式,再加上自定义格式,其数量肯定远远超过14到22这几个数。

最后附上完整取值代码, 也可以在此处查看
https://gitee.com/vdevelopernet/csharppractice/blob/master/OpenXmlPractice/FrmReadExcel.cs

object? GetCellValue(Cell c, SharedStringTable? stringTable, Stylesheet? styleSheet)
{
    string? value = c.CellValue?.Text;

    if (c.DataType != null)
    {
        switch (c.DataType.Value)
        {
            case CellValues.SharedString:
                if (!string.IsNullOrEmpty(value) && int.TryParse(value, out int strIndex))
                {
                    if (stringTable != null && stringTable.Count() > strIndex)
                    {
                        return stringTable.ElementAt(strIndex).InnerText;
                    }
                }
                return (string?)null;
            case CellValues.Date:
                if (!string.IsNullOrEmpty(value) && double.TryParse(value, out double dtValue))
                {
                    return System.DateTime.FromOADate(dtValue);
                }
                return (DateTime?)null;
            case CellValues.Boolean:
                if (!string.IsNullOrEmpty(value))
                {
                    if (value.Length < 2)
                    {
                        return value?.ToUpperInvariant() == "T" || value == "1";
                    }
                    if (bool.TryParse(value, out bool boolValue))
                    {
                        return boolValue;
                    }
                }
                return (bool?)null;
            case CellValues.Number:
                if (!string.IsNullOrEmpty(value) && decimal.TryParse(value, out decimal number))
                {
                    return number;
                }
                return (decimal?)null;
            default: break;
        }
    }

    if (c.StyleIndex?.InnerText != null && styleSheet != null && styleSheet.CellFormats != null)
    {
        var cellFormat = styleSheet.CellFormats.ChildElements[int.Parse(c.StyleIndex.InnerText)] as CellFormat;
        //https://github.com/ClosedXML/ClosedXML/wiki/NumberFormatId-Lookup-Table
        if (cellFormat != null && cellFormat.NumberFormatId!.HasValue && cellFormat.NumberFormatId.Value >= 14 && cellFormat.NumberFormatId.Value <= 22)
        {
            if (!string.IsNullOrEmpty(value))
            {
                if (double.TryParse(value, out var cellDouble))
                {
                    return DateTime.FromOADate(cellDouble);
                }
            }
            return (DateTime?)null;

        }
    }

    return value;
}

引用

Related Posts

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注