使用Open XML SDK从xlsx文件中读取日期

41

我在一个xlsx文件的一个单元格中有一个格式为“4/5/2011”(月/日/年)的日期。我正在尝试解析该文件并将这些数据加载到某些类中。

到目前为止,我解析单元格的部分如下所示:

string cellValue = cell.InnerText;
if (cell.DataType != null)
{
    switch (cell.DataType.Value)
    {
        case CellValues.SharedString:
            // get string from shared string table
            cellValue = this.GetStringFromSharedStringTable(int.Parse(cellValue));
            break;
    }
}

我原本希望那个日期会是一个单元格数据类型。但实际上,在解析包含日期“4/5/2011”的单元格时,cell.DataType的值为null,单元格的值为“40638”,并且它不是共享字符串表的索引。(我以前尝试过这样做,结果出现异常。)

8个回答

48

Open XML将日期存储为自1900年1月1日以来的天数。跳过不正确的1900年2月29日作为有效日期。您应该能够找到一些算法来帮助您计算正确的值。我相信一些开发人员使用DateTime.FromOADate()作为辅助工具。

此外,默认情况下,Cell类的DataType属性为数字。因此,如果它为空,那么它是一个数字,这在我们的情况下包括日期。

只有在存储的日期早于时代(在本例中为1900年1月1日)时,才需要访问共享字符串表。在这种情况下,Cell类的CellValue属性保存了共享字符串表的索引。


@Santhos,除了使用FromOADate之外,您需要进行任何调整吗?@VincentTan将其仅称为“辅助方法”。 - Csaba Toth
11
我有一个问题:如何区分行内数字和应该是日期的数字?对我来说,在这两种情况下 cell.DataType都为空,而且我只有数字的字符串表示形式在文本中。 - Csaba Toth
2
我现在已经有了使用EPPlus库和Open SDK的经验,对于所有未来的用户,我想说使用EPPlus是一个非常好的选择(比使用Open SDK更好),除非你想做一些EPPlus不支持的事情。 - Santhos
要区分数字和日期,请参见评论https://dev59.com/3mYs5IYBdhLWcg3wDfp5#6IzjnYgBc1ULPQZFVk3u。 - Marco Luglio

17

似乎日期单元格的cell.DataType未设置。

解决方法是查看单元格是否具有StyleIndex,这是文档中单元格格式数组的索引。

然后使用cellFormat.NumberFormatId来确定这是否为日期数据类型。

以下是一些代码:

    public class ExcelCellWithType
    {
        public string Value { get; set; }
        public UInt32Value ExcelCellFormat { get; set; }
        public bool IsDateTimeType { get; set; }
    }  

    public class ExcelDocumentData
    {
        public ExcelXmlStatus Status { get; set; }
        public IList<Sheet> Sheets { get; set; }
        public IList<ExcelSheetData> SheetData { get; set; }

        public ExcelDocumentData()
        {
            Status = new ExcelXmlStatus();
            Sheets = new List<Sheet>();
            SheetData = new List<ExcelSheetData>();
        }
    } 

    ...

    public ExcelDocumentData ReadSpreadSheetDocument(SpreadsheetDocument mySpreadsheet, ExcelDocumentData data)
    {
        var workbookPart = mySpreadsheet.WorkbookPart;

        data.Sheets = workbookPart.Workbook.Descendants<Sheet>().ToList();

        foreach (var sheet in data.Sheets)
        {
            var sheetData = new ExcelSheetData { SheetName = sheet.Name };
            var workSheet = ((WorksheetPart)workbookPart.GetPartById(sheet.Id)).Worksheet;

            sheetData.ColumnConfigurations = workSheet.Descendants<Columns>().FirstOrDefault();
            var rows = workSheet.Elements<SheetData>().First().Elements<Row>().ToList();
            if (rows.Count > 1)
            {
                foreach (var row in rows)
                {
                    var dataRow = new List<ExcelCellWithType>();

                    var cellEnumerator = GetExcelCellEnumerator(row);
                    while (cellEnumerator.MoveNext())
                    {
                        var cell = cellEnumerator.Current;
                        var cellWithType = ReadExcelCell(cell, workbookPart);
                        dataRow.Add(cellWithType);
                    }

                    sheetData.DataRows.Add(dataRow);
                }
            }
            data.SheetData.Add(sheetData);
        }

        return data;
    }

    ...

    private ExcelCellWithType ReadExcelCell(Cell cell, WorkbookPart workbookPart)
    {
        var cellValue = cell.CellValue;
        var text = (cellValue == null) ? cell.InnerText : cellValue.Text;
        if (cell.DataType?.Value == CellValues.SharedString)
        {
            text = workbookPart.SharedStringTablePart.SharedStringTable
                .Elements<SharedStringItem>().ElementAt(
                    Convert.ToInt32(cell.CellValue.Text)).InnerText;
        }

        var cellText = (text ?? string.Empty).Trim();

        var cellWithType = new ExcelCellWithType();

        if (cell.StyleIndex != null)
        {
            var cellFormat = workbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements[
                int.Parse(cell.StyleIndex.InnerText)] as CellFormat;

            if (cellFormat != null)
            {
                cellWithType.ExcelCellFormat = cellFormat.NumberFormatId;

                var dateFormat = GetDateTimeFormat(cellFormat.NumberFormatId);
                if (!string.IsNullOrEmpty(dateFormat))
                {
                    cellWithType.IsDateTimeType = true;

                    if (!string.IsNullOrEmpty(cellText))
                    {
                       if (double.TryParse(cellText, out var cellDouble))
                        {
                            var theDate = DateTime.FromOADate(cellDouble);
                            cellText = theDate.ToString(dateFormat);
                        }
                    }
                }
            }
        }

        cellWithType.Value = cellText;

        return cellWithType;
    }

    //// https://msdn.microsoft.com/en-GB/library/documentformat.openxml.spreadsheet.numberingformat(v=office.14).aspx
    private readonly Dictionary<uint, string> DateFormatDictionary = new Dictionary<uint, string>()
    {
        [14] = "dd/MM/yyyy",
        [15] = "d-MMM-yy",
        [16] = "d-MMM",
        [17] = "MMM-yy",
        [18] = "h:mm AM/PM",
        [19] = "h:mm:ss AM/PM",
        [20] = "h:mm",
        [21] = "h:mm:ss",
        [22] = "M/d/yy h:mm",
        [30] = "M/d/yy",
        [34] = "yyyy-MM-dd",
        [45] = "mm:ss",
        [46] = "[h]:mm:ss",
        [47] = "mmss.0",
        [51] = "MM-dd",
        [52] = "yyyy-MM-dd",
        [53] = "yyyy-MM-dd",
        [55] = "yyyy-MM-dd",
        [56] = "yyyy-MM-dd",
        [58] = "MM-dd",
        [165] = "M/d/yy",
        [166] = "dd MMMM yyyy",
        [167] = "dd/MM/yyyy",
        [168] = "dd/MM/yy",
        [169] = "d.M.yy",
        [170] = "yyyy-MM-dd",
        [171] = "dd MMMM yyyy",
        [172] = "d MMMM yyyy",
        [173] = "M/d",
        [174] = "M/d/yy",
        [175] = "MM/dd/yy",
        [176] = "d-MMM",
        [177] = "d-MMM-yy",
        [178] = "dd-MMM-yy",
        [179] = "MMM-yy",
        [180] = "MMMM-yy",
        [181] = "MMMM d, yyyy",
        [182] = "M/d/yy hh:mm t",
        [183] = "M/d/y HH:mm",
        [184] = "MMM",
        [185] = "MMM-dd",
        [186] = "M/d/yyyy",
        [187] = "d-MMM-yyyy"
    };

    private string GetDateTimeFormat(UInt32Value numberFormatId)
    {
        return DateFormatDictionary.ContainsKey(numberFormatId) ? DateFormatDictionary[numberFormatId] : string.Empty;
    }

思考一下,只要确定您文化中的日期格式数字可能就足够了(上面的示例包含我可以从Excel 365中找到的美国和英国日期数字)。也许可以从文档中获取格式,但我猜这不是这个问题。 - Philip Johnson
在我看来,这个答案更正确,因为如果你正在阅读一个随机的XLSX文件,你必须经过这个过程才能确定单元格是否包含日期,而不仅仅是数字。 - TechSavvySam

13

你可以使用 DateTime.FromOADate(41690)。


3

我想要分享我的意见。我正在处理一个模板,所以我知道一个给定的单元格应该是一个日期时间。因此,我最终会在这个方法中得到一个字符串参数excelDateTime,其中包含单元格的值,通常是一个OADate数字,如“42540.041666666664”。

public static bool TryParseExcelDateTime(string excelDateTimeAsString, out DateTime dateTime)
{
    double oaDateAsDouble;
    if (!double.TryParse(excelDateTimeAsString, out oaDateAsDouble)) //this line is Culture dependent!
        return false;
    //[...]
    dateTime = DateTime.FromOADate(oaDateAsDouble);

我的问题是最终用户在德国,因为这是一个网站,我们将Thread.CurrentThread.CurrentCulture和Thread.CurrentThread.CurrentUICulture设置为“DE-de”。当调用double.TryParse时,它使用该文化来解析数字。因此,这行代码:double.TryParse("42540.041666666664", out oaDate)确实可以工作,但它返回42540041666666664,因为在德国,点是组分隔符。DateTime.FromOADate因数字超出范围(minOaDate = -657435.0,maxOaDate = +2958465.99999999)而失败。
这使我想到:
  1. 无论用户机器的语言环境如何,OpenXML文档中包含以默认语言环境(美国?不变?在任何情况下,小数点作为十进制分隔符)格式化的数字。我已经搜索过,但没有找到此规范。
  2. 当对可能是OADate字符串进行double.TryParse时,我们应该使用double.TryParse(excelDateTimeAsString, NumberStyles.Any, CultureInfo.InvariantCulture, out oaDateAsDouble))。我正在使用CultureInfo.InvariantCulture,但应该是第1点的内容,我不确定。

我曾经遇到过完全相同的问题。我通过以下方式解决了它: Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - Marc K

2

我们需要采用不同的策略来解析使用OpenXML的不同类型的列。

要解析字符串和布尔值,我们可以使用单元格的DataType属性,如下所示 -

        switch (cell.DataType.Value)
        {
            case CellValues.SharedString:
                // Fetch value from SharedStrings array
                break;
            case CellValues.Boolean:
                text = cell.InnerText;
                switch (text)
                {
                    case "0": text = "false"; break;
                    default: text = "true"; break;
                }
                break;
        }

要解析日期/时间/日期时间值(无论是应用任何内置格式还是任何自定义格式),DataType属性都将返回null,因此可以像下面这样放置:

    if (cell.DataType == null)
        DateTime.FromOADate(double.Parse(cell.InnerText))

上述返回的值将基于您机器上的区域设置以默认格式返回。 但是,如果您需要获取实际在Excel中呈现的格式的值,并且您不确定格式,则可以访问与此类单元格相关联的StyleIndex属性。
StyleIndex属性将为您提供应用于单元格的样式的索引,该索引可以在styles.xml文件(以下标签)中找到 -
    <cellXfs count="3">
        <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>          
        <xf numFmtId="168" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
        <xf numFmtId="169" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
    </cellXfs>

在上述情况下,StyleIndex值可以是0、1或2,因为有3种应用的样式。具有numFmtId in (0, 163)的样式对应于Excel提供的内置格式,而numFmtId >= 164则对应于自定义格式。
从上面获得的StyleIndex值,您将获得numFmtId,该值映射到<numFmts>部分(在styles.xml文件中)下存在的特定<numFmt>标记,以获取应用于单元格上的实际日期格式。
    <numFmts count="2">
       <numFmt numFmtId="168" formatCode="[$£-809]#,##0.00"/>
       <numFmt numFmtId="169" formatCode="dd\-mmm\-yyyy\ hh:mm:ss"/>
    </numFmts>

单元格中应用的日期格式也可以使用OpenXML API获取 -
      CellFormat cellFmt = cellFormats.ChildElements[int.Parse(cell.StyleIndex.InnerText)] as CellFormat;
      string format = numberingFormats.Elements<NumberingFormat>()
                .Where(i => i.NumberFormatId.Value == cellFmt .NumberFormatId.Value)
                .First().FormatCode;

2

我遇到了同样的问题- 转换到了EPPlus http://epplus.codeplex.com/

需要注意它有LGPL许可证。因此,如果您希望您的代码库免受GPL问题的影响,只需按原样使用该库,您的原始代码库许可证就是安全的。


我宁愿坚持使用开放式XML SDK,但如果找不到解决方案,我会尝试这个。谢谢。 - Santhos
你在OpenXML SDK中有一个解决方案,但是要用几个语句来实现它太过复杂了。这里有一个指针 - 样式文档部分告诉您数字格式。在其中进行迭代,选择匹配的元素。 - CKmum
我不明白。我原以为数字格式应该是像“数字:#.###”或“日期:m/d/y”这样的东西。我从单元格中得到的那个数字一定有什么含义。你知道它是什么意思吗? - Santhos
2
不,你实际上需要获取stylesdocument部分。然后找到单元格的格式条目。它将具有numberformat="1"或类似数字。14到18是日期格式。然后您将需要查找日期格式,然后加载数据。 - CKmum
我最近尝试了这个库,发现它非常易于使用,并且对于某些解决方案可能比原始的Open XML SDK更好。 - Santhos
显示剩余2条评论

0

在检索任何内联字符串后,我会这样做:

    private static object Convert(this DocumentFormat.OpenXml.Spreadsheet.CellValues value, string content)
    {
        switch (value)
        {
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.Boolean:
                if (content.Length < 2)
                {
                    return content?.ToUpperInvariant() == "T" || content == "1";
                }
                return System.Convert.ToBoolean(content);
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.Date:
                if (double.TryParse(content, out double result))
                {
                    return System.DateTime.FromOADate(result);
                }
                return null;
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.Number:
                return System.Convert.ToDecimal(content);
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.Error:
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.String:
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.InlineString:
            case DocumentFormat.OpenXml.Spreadsheet.CellValues.SharedString:
            default:
                return content;
        }
    }

-1
每个单元格都有两个属性r(CellReference)和s(StyleIndex)
数字的StyleIndex为2,日期的StyleIndex为3
日期在ODate中,您可以将其转换为字符串格式
value = DateTime.FromOADate(double.Parse(value)).ToShortDateString();

1
我有一个StyleIndex为11的日期字段。 - Buzzy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接