使用OpenXML读取带文本格式Excel单元格时浮点精度异常问题
问题根因
OpenXML直接读取CellValue.InnerText拿到的是xlsx文件中存储的原始值。Excel采用IEEE 754双精度浮点数存储数值,十进制的1.1无法被双精度精确表示,底层存储的就是1.1000000000000001这个近似值。
Excel界面显示正常,是因为它会根据单元格绑定的数字格式规则,对原始值做格式化渲染后再展示,不会直接输出原始存储值。你看到的单元格属性s="2"就是样式索引,指向样式表中对应的数字格式配置,你当前的代码完全跳过了样式解析和格式化步骤,自然会拿到带精度误差的原始值。
实现方案
要拿到和Excel界面一致的显示值,核心是按照Excel的渲染逻辑走:读取原始数值后,根据单元格关联的数字格式对数值做格式化,而不是直接输出原始字符串。
具体实现步骤:
- 加载工作簿样式表,同时内置Excel预定义的数字格式映射(索引小于164的格式为Excel内置格式)
- 遍历单元格时,对数值类型的单元格先将原始值解析为double类型
- 根据单元格的StyleIndex找到对应的数字格式码
- 按照格式码格式化double值,常规格式下使用
G10格式说明符即可自动消除浮点数精度带来的多余小数位,输出和Excel一致的1.1这类结果
修正后的完整读取代码如下:
// Excel内置数字格式映射(索引<164为内置格式,可根据业务场景补充) private static readonly Dictionary<uint, string> BuiltInNumberFormats = new Dictionary<uint, string> { {0, "General"}, {1, "0"}, {2, "0.00"}, {3, "#,##0"}, {4, "#,##0.00"}, {9, "0%"}, {10, "0.00%"}, {11, "0.00E+00"}, {14, "M/d/yyyy"}, {18, "h:mm tt"}, {20, "H:mm"}, {22, "M/d/yyyy H:mm"} }; // 读取逻辑 List<List<string>> rows = new List<List<string>>(); // using块自动释放资源,无需手动调用Close using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(excelFilePath, false)) { WorkbookPart workbookPart = spreadsheetDocument.WorkbookPart; WorksheetPart worksheetPart = workbookPart.WorksheetParts.First(); SheetData sheetData = worksheetPart.Worksheet.Elements<SheetData>().First(); SharedStringTablePart sstpart = workbookPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault(); SharedStringTable sst = sstpart?.SharedStringTable; // 加载样式表和所有数字格式 WorkbookStylesPart stylesPart = workbookPart.WorkbookStylesPart; Stylesheet styles = stylesPart.Stylesheet; Dictionary<uint, string> numberFormats = new Dictionary<uint, string>(BuiltInNumberFormats); // 加载用户自定义数字格式 if (styles.NumberingFormats != null) { foreach (NumberingFormat nf in styles.NumberingFormats.Elements<NumberingFormat>()) { numberFormats.TryAdd(nf.NumberFormatId.Value, nf.FormatCode); } } foreach (Row r in sheetData.Elements<Row>()) { List<string> cols = new List<string>(); foreach (Cell c in r.Elements<Cell>()) { // 处理共享字符串类型 if (c.DataType != null && c.DataType == CellValues.SharedString) { if (sst != null && int.TryParse(c.CellValue?.Text, out int ssid) && ssid < sst.ChildElements.Count) { cols.Add(sst.ChildElements[ssid].InnerText); } else { cols.Add(c.CellValue?.InnerText); } } // 处理数值类型单元格 else if (c.DataType == null || c.DataType == CellValues.Number) { string rawValue = c.CellValue?.InnerText; if (double.TryParse(rawValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double numVal)) { string formatCode = "General"; // 查找单元格绑定的数字格式 if (c.StyleIndex != null && c.StyleIndex.Value < styles.CellFormats.Count) { CellFormat cellFormat = (CellFormat)styles.CellFormats.ElementAt((int)c.StyleIndex.Value); if (cellFormat.NumberFormatId != null && numberFormats.TryGetValue(cellFormat.NumberFormatId.Value, out string foundFormat)) { formatCode = foundFormat; } } string displayVal; if (formatCode == "General") { // G10格式匹配Excel常规数值渲染规则,自动过滤双精度精度误差 displayVal = numVal.ToString("G10", CultureInfo.CurrentCulture); } else { displayVal = numVal.ToString(formatCode, CultureInfo.CurrentCulture); } cols.Add(displayVal); } else { cols.Add(rawValue); } } else { cols.Add(c.CellValue?.InnerText); } } rows.Add(cols); } }
注意事项
G10格式说明符最多保留10位有效数字,正好匹配Excel常规格式的显示逻辑,不需要手动写截断小数位的代码,不会误截断有效数字,也能自动把1.1000000000000001这类误差值修正为1.1。- 代码补充了空值、索引越界判断,修复了原代码中SharedStringTable不存在时抛出空引用异常的问题。
- 如果需要处理特殊自定义格式(比如带前后缀的数值、特殊日期格式),只需要补充Excel格式码到.NET数字格式码的转换逻辑即可,常规数值、百分比、日期场景上述代码可以直接使用。
内容的提问来源于stack exchange,提问作者Arthur
相关产品推荐
相关产品推荐

