從Excel導入數據最令人頭疼的是數據格式的兼容性,特別是日期類型的兼容性。為了能夠無腦導入日期,折騰了一天的NPOI。在經過測試確實可以導入任意格式的合法日期後,寫下這篇小文,與大家共享。完整代碼請移步:https://github.com/xuanbg/Utility
2016-11-13 04:06 修正一個bug。由try DateCellValue改為判斷列數據類型,如類型為DateTiime返回DateCellValue,否則返回NumericCellValue或StringCellValue。
概述:
這個幫助類是一個泛型類,泛型參數對應的實體類還起到模板的作用。如果你的Excel文件使用與實體類不同的列標題的話,可以通過給屬性加上Alias特性,將列標題和屬性進行對應。例如:
Excel格式如圖:

實體類:
1 using System;
2 using Insight.Utils.Common;
3
4 namespace Insight.WS.Server.Common.Entity
5 {
6 public class Logistics
7 {
8 [Alias("訂單號")]
9 public string OrderCode { get; set; }
10
11 [Alias("物流公司")]
12 public string Service { get; set; }
13
14 [Alias("物流單號")]
15 public string Number { get; set; }
16
17 [Alias("發貨時間")]
18 public DateTime DeliveryTime { get; set; }
19 }
20 }
返回的Json:
1 [
2 {
3 "OrderCode": "201611S1200324",
4 "Service": "順豐",
5 "Number": "33012231F54351",
6 "DeliveryTime": "2016-11-10T11:02:44"
7 },
8 {
9 "OrderCode": "2016111200324",
10 "Service": "順豐",
11 "Number": "33012231F54352",
12 "DeliveryTime": "2016-11-12T09:02:44"
13 },
14 {
15 "OrderCode": "2016111200324",
16 "Service": "EMS",
17 "Number": "33012231F54353",
18 "DeliveryTime": "2016-11-12T09:02:44"
19 }
20 ]
1、類主體,負責根據傳入的文件路徑讀取數據,並調用其他私有方法對數據進行處理。最後轉換成List<T>並序列化成Json返回。
1 using System;
2 using System.Collections.Generic;
3 using System.Data;
4 using System.IO;
5 using Insight.Utils.Entity;
6 using NPOI.SS.UserModel;
7
8 namespace Insight.Utils.Common
9 {
10 public class NpoiHelper<T> where T : new()
11 {
12 private readonly Result _Result = new Result();
13
14 /// <summary>
15 /// 導入Excel文件
16 /// </summary>
17 /// <param name="path">文件路徑</param>
18 /// <param name="index">Sheet索引</param>
19 /// <returns>Result</returns>
20 public Result Import(string path, int index = 0)
21 {
22 if (!File.Exists(path))
23 {
24 _Result.FileNotExists();
25 return _Result;
26 }
27
28 IWorkbook book;
29 using (var file = new FileStream(path, FileMode.Open, FileAccess.Read))
30 {
31 book = WorkbookFactory.Create(file);
32 }
33
34 if (index >= book.NumberOfSheets)
35 {
36 _Result.SheetNotExists();
37 return _Result;
38 }
39
40 var sheet = book.GetSheetAt(index);
41 var table = GetSheetData(sheet);
42 var list = Util.ConvertToList<T>(table);
43 _Result.Success(list);
44 return _Result;
45 }
46 }
47 }
2、GetSheetData方法,負責將Sheet中的數據讀取到DataTable。這裡通過實體類屬性的特性值作為列名,屬性類型作為列數據類型來初始化DataTable。當然,首行是例外,因為首行是列標題而非數據。
1 /// <summary>
2 /// 讀取Sheet中的數據到DataTable
3 /// </summary>
4 /// <param name="sheet">當前數據表</param>
5 /// <returns>DataTable</returns>
6 private DataTable GetSheetData(ISheet sheet)
7 {
8 var table = InitTable(sheet);
9 if (table == null) return null;
10
11 var rows = sheet.GetEnumerator();
12 while (rows.MoveNext())
13 {
14 var row = (IRow) rows.Current;
15 if (row.RowNum == 0) continue;
16
17 var dr = table.NewRow();
18 for (var i = 0; i < table.Columns.Count; i++)
19 {
20 try
21 {
22 var type = table.Columns[i].DataType;
23 dr[i] = GetCellData(row.GetCell(i), type);
24 }
25 catch (Exception)
26 {
27 dr[i] = DBNull.Value;
28 }
29
30 }
31 table.Rows.Add(dr);
32 }
33
34 return table;
35 }
初始化DataTable的方法:
1 /// <summary>
2 /// 初始化DataTable
3 /// </summary>
4 /// <param name="sheet">當前數據表</param>
5 /// <returns>DataTable</returns>
6 private DataTable InitTable(ISheet sheet)
7 {
8 var title = sheet.GetRow(0);
9 if (title == null)
10 {
11 _Result.NoRowsRead();
12 return null;
13 }
14
15 try
16 {
17 var dict = GetDictionary();
18 var table = new DataTable();
19 foreach (var cell in title.Cells)
20 {
21 var col_name = cell.StringCellValue;
22 var col_type = dict[col_name];
23 table.Columns.Add(cell.StringCellValue, col_type);
24 }
25
26 return table;
27 }
28 catch
29 {
30 _Result.IncorrectExcelFormat();
31 return null;
32 }
33 }
生成模板字典的方法:
1 /// <summary>
2 /// 獲取指定類型的屬性名稱/類型字典
3 /// </summary>
4 /// <returns>Dictionary</returns>
5 private Dictionary<string, Type> GetDictionary()
6 {
7 var dict = new Dictionary<string, Type>();
8 var propertys = typeof(T).GetProperties();
9 foreach (var p in propertys)
10 {
11 string name;
12 var attributes = p.GetCustomAttributes(typeof(AliasAttribute), false);
13 if (attributes.Length > 0)
14 {
15 var type = (AliasAttribute)attributes[0];
16 name = type.Alias;
17 }
18 else
19 {
20 name = p.Name;
21 }
22
23 dict.Add(name, p.PropertyType);
24 }
25
26 return dict;
27 }
3、重點來了!
因為日期/時間在Excel中可能被表示為文本格式或日期格式(其實是Numeric類型),所以在CellType為String/Numeric的時候,如果列數據類型為DateTime,則取cell的DateCellValue,否則取cell的StringCellValue/NumericCellValue就好了。
這樣,無論日期是文本或日期格式,都可以完美獲取。
1 /// <summary>
2 /// 讀Excel單元格的數據
3 /// </summary>
4 /// <param name="cell">Excel單元格</param>
5 /// <param name="type">列數據類型</param>
6 /// <returns>object 單元格數據</returns>
7 private object GetCellData(ICell cell, Type type)
8 {
9 switch (cell.CellType)
10 {
11 case CellType.Numeric:
12 if (type == typeof(DateTime)) return cell.DateCellValue;
13
14 return cell.NumericCellValue;
15
16 case CellType.String:
17 if (type == typeof(DateTime)) return cell.DateCellValue;
18
19 return cell.StringCellValue;
20
21 case CellType.Boolean:
22 return cell.BooleanCellValue;
23
24 case CellType.Unknown:
25 case CellType.Formula:
26 case CellType.Blank:
27 case CellType.Error:
28 return null;
29 default:
30 return null;
31 }
32 }
4、DataTable轉成List<T>的方法:
1 /// <summary>
2 /// 將DataTable轉為List
3 /// </summary>
4 /// <param name="table">DataTable</param>
5 /// <returns>List</returns>
6 public static List<T> ConvertToList<T>(DataTable table) where T: new()
7 {
8 var list = new List<T>();
9 var propertys = typeof(T).GetProperties();
10 foreach (DataRow row in table.Rows)
11 {
12 var obj = new T();
13 foreach (var p in propertys)
14 {
15 string name;
16 var attributes = p.GetCustomAttributes(typeof(AliasAttribute), false);
17 if (attributes.Length > 0)
18 {
19 var type = (AliasAttribute) attributes[0];
20 name = type.Alias;
21 }
22 else
23 {
24 name = p.Name;
25 }
26
27 if (table.Columns.Contains(name))
28 {
29 if (!p.CanWrite) continue;
30
31 var value = row[name];
32 if (value == DBNull.Value) value = null;
33
34 p.SetValue(obj, value, null);
35 }
36 }
37 list.Add(obj);
38 }
39 return list;
40 }
自定義特性:
1 using System;
2
3 namespace Insight.Utils.Common
4 {
5 [AttributeUsage(AttributeTargets.Property)]
6 public class AliasAttribute : Attribute
7 {
8 /// <summary>
9 /// 屬性別名
10 /// </summary>
11 public string Alias { get; }
12
13 /// <summary>
14 /// 構造方法
15 /// </summary>
16 /// <param name="alias">別名</param>
17 public AliasAttribute(string alias)
18 {
19 Alias = alias;
20 }
21 }
22 }
請大家對此多發表意見和建議,謝謝。