最近由於工作上用到了讀取Excel操作,完成過程中遇到了各種各樣的問題,最后為了以后讀取Excel時不再如此的麻煩,特意做成了一個小組件,方便日后使用。現在總結一下過程中遇到的問題及相應的解決辦法。
一、Excel讀取方式及問題
第一種方式:通過OleDb連接,把excel文件作為數據源來讀取。
優點:讀取的效率很快。
缺點:1、讀取過程中需要依賴於計算機安裝的office版本,不同的版本讀取過程中用到的代碼會有所不同,這就需要在實現的過程中必須判斷客戶端機器安裝的office版本(如果客戶端未裝任何版本的office軟件,則程序將會報錯);
2、讀取Excel得到的工作表名稱即Sheet名稱會和Excel表格中實際的Sheet名稱有所不一致 ,不過這並不妨礙程序的運行;
3、如果Excel表格中存在這樣的列:列中的數據類型不一致,比如說列中一些行為數字,一些行為字符串,那么將有一種類型的數據部分解析不出來,顯示為空,產生這種問題的根源與Excel ISAM[3](Indexed Sequential Access Method,即索引順序存取方法)驅動程序的限制有關,Excel ISAM 驅動程序通過檢查前幾行中實際值確定一個 Excel 列的類型,然后選擇能夠代表其樣本中大部分值的數據類型[4]。也即Excel ISAM查找某列前幾行(默認情況下是8行),把占多的類型作為其處理類型。例如如果數字占多,那么其它含有字母等文本的數據項就會置空;相反如果文本居多,純數字的數據項就會被置空。 若要修改默認情況下的8行,需要修改注冊表值TypeGuessRows,TypeGuessRows 值決定了ISAM 驅動程序從前幾條數據采樣確定數據類型,默認為“8”。可以通過修改“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\4.0\Engines\Excel”下的該注冊表值來更改采樣行數。但是這種改進還是沒有根本上解決問題。遇到這種情況時應當考慮不再使用這種讀取Excel表格的方式。
第二種方式:引用Com組件讀取Excel文件。
優點:無需考慮客戶端機器安裝的Office版本,讀取的Sheet名稱和Excel表格中完全一致,也不需要考慮Excel表格中列中的數據類型是否不一致,讀取的數據十分准確。
缺點:1、效率比較低,讀取的過程耗時嚴重;
2、需要做好內存的釋放、Excel進程的關閉工作。
二、讀取方式詳解及問題解決辦法總結
第一種方式:通過OleDb連接,把excel文件作為數據源來讀取。
1、方式詳解:該方式主要的部分為兩步:第一步,需要解析Excel表中的所有Sheet名稱,如下面的代碼,我將得到的所有Sheet名稱放到了一個Lsit<string>集合集合中(_workSheets),並在代碼段將該集合綁定到了下拉框cmbxWorkSheets;
string conn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + SourceExcelFile + ";Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1;\"" ; OleDbConnection conn = new OleDbConnection(connString); conn.Open(); DataTable dt = conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null); conn.Close(); foreach (DataRow row in dt.Rows) { string name = row["TABLE_NAME"].ToString(); if (!name.Contains("$")) continue; _workSheets.Add(name); }
string conn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + SourceExcelFile + ";Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1;\"" ; string sql = string.Format("select * from [{0}]", cmbxWorkSheets.Text); OleDbDataAdapter da = new OleDbDataAdapter(sql, conn); DataSet ds = new DataSet(); da.Fill(ds); if (ds != null && ds.Tables.Count > 0) dt = ds.Tables[0];
這樣,讀取過程基本完成。
2、方式問題及解決:
問題1:在上面用到的連接字符串 string conn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + SourceExcelFile + ";Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1;\"" ,其中Provider=Microsoft.ACE.OLEDB.12.0,對於不同的offcie版本,Provider對應的值會有所不同,07及以上版本為Microsoft.ACE.OLEDB.12.0,而03版本的office卻為Microsoft.Jet.OLEDB.4.0,因此需要判斷客戶端安裝的office版本以確定Provider的值,而不能像上面那樣直接寫死。
/// <summary> /// 判斷Excel的版本號。 /// </summary> /// <returns></returns> private static double CheckExcelVer() { Type objExcelType = Type.GetTypeFromProgID("Excel.Application"); if (objExcelType == null) { return 0; } object objApp = Activator.CreateInstance(objExcelType); if (objApp == null) { return 0; } object objVer = objApp.GetType().InvokeMember("Version", BindingFlags.GetProperty, null, objApp, null); double iVer = Convert.ToDouble(objVer.ToString()); objVer = null; objApp = null; objExcelType = null; GC.Collect(); return iVer; } /// <summary> /// 根據Excel版本號獲得Excel的版本名稱。 /// </summary> /// <returns></returns> public static String GetExcelVerStr() { String version; double excelver; excelver = CheckExcelVer();// ExistsExcelRegedit(); version = "Office "; if (excelver == 0) { MessageBox.Show("無法識別Excel的版本", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Information); version = "無法識別 office 版本"; } else if (excelver >= 14) version += "2010或以上"; else if (excelver >= 12) version += "2007"; else if (excelver >= 11) version += "2003"; else if (excelver >= 10) version += "XP"; else if (excelver >= 9) version += "2000"; else if (excelver >= 8) version += "97"; else if (excelver >= 7) version += "95"; return version; } /// <summary> /// 根據Excel的版本名稱獲得Provider。 /// </summary> /// <returns></returns> public static string GetExcelProvider() { string provider = string.Empty; string version = GetExcelVerStr(); switch (version) { case "Office 2010或以上": case "Office 2007": provider = "Microsoft.ACE.OLEDB.12.0"; break; case "Office 2003": case "Office 2000": case "Office XP": case "Office 97": case "Office 95": provider = "Microsoft.Jet.OLEDB.4.0"; break; } return provider; }
這樣就可以根據用戶客戶端安裝的office版本來讀取Excel,只要客戶端安裝了office解析Excel時便不會出錯。
問題二:Excel表格中某些列中數據類型不一致,導致列中后面的數據讀取不出來。這種方式在讀取Excel表的時候,無法定義和修改讀取列的數據類型,string conn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + SourceExcelFile + ";Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1;\"" ,雖然“IMEX = 1”被認為是用來解決數字與字符混合時,識別不正常的情況,但其實它並未真正解決問題,表現為如果某字段前8條記錄中全部為純數字的話,那么在該字段隨后的記錄中含有字母或漢字的項將仍然變為空,但是如果該字段前8條記錄中有一條不為純數字,將能得到預期想要的結果,也就是說它其實只對前8行中不一致的數據類型起作用,8行之后的數據若出現數據不一致,便會出現為空的情況。此時,若要更好的解決問題,需要用第二種方式來讀取Excel表。
第二種方式:引用Com組件讀取Excel文件。
1、方式詳解
這種方式需要添加COM中的程序集Microsoft Excel 11.0 Object Library.dll,修改程序集中的屬性“嵌入互操作類型”為False,不然會提示錯誤: 無法嵌入互操作類型“Microsoft.Office.Interop.Excel.ApplicationClass”。讀取Excel的代碼:
private Microsoft.Office.Interop.Excel.ApplicationClass _excelApp; private Microsoft.Office.Interop.Excel._Workbook _workbook; private Microsoft.Office.Interop.Excel.Sheets _workSheets; _excelApp = new Microsoft.Office.Interop.Excel.ApplicationClass(); _excelApp.Visible = true; _workbook = _excelApp.Workbooks.Open(fileName, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value) as Microsoft.Office.Interop.Excel.WorkbookClass; _workSheets = _workbook.Sheets; private void ReadExcel() { Excel.Range selectedRange = _excelApp.Selection as Excel.Range; try { if (ResultTable != null) { ResultTable.Clear(); ResultTable.Dispose(); } ResultTable = new DataTable(); string colName = string.Empty; if (IncludeHeader) { for (int i = 1; i <= selectedRange.Columns.Count; i++) { Excel.Range cell = selectedRange[1, i] as Excel.Range; if (cell != null && cell.Value != null) colName = cell.Value.ToString().Trim(); else colName = string.Empty; if (string.IsNullOrEmpty(colName)) colName = "列 " + i.ToString(); if (ResultTable.Columns.Contains(colName)) throw new Exception(string.Format("導入數據存在重復的列:{0}。", colName)); ResultTable.Columns.Add(new DataColumn(colName)); } } else // 若不含標題行,則列名自動命名為“列 1”、“列 2”等。 for (int i = 1; i <= selectedRange.Columns.Count; i++) ResultTable.Columns.Add(new DataColumn("列 " + i.ToString())); int firstDataLineNo = IncludeHeader ? 2 : 1; // 若不含標題行,數據從第一行開始。 int count = selectedRange.Rows.Count; for (int i = firstDataLineNo; i <= count; i++) { //if (((Excel.Range)selectedRange[i, 1]).Value == null) // continue; DataRow dr = ResultTable.NewRow(); for (int j = 0; j < selectedRange.Columns.Count; j++) dr[j] = ((Excel.Range)selectedRange[i, j + 1]).Value; ResultTable.Rows.Add(dr); SetProgressValue(count - firstDataLineNo, i - firstDataLineNo); } } catch (Exception exp) { MessageBox.Show(this, string.Format("讀Excel錯誤:{0}", exp.Message), "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); _excelApp.Visible = true; } }
方式問題及解決:
這種方式效率很慢,而且需要做好善后工作。釋放內存:
private void DisposeExcelResource() { try { if (_workbook != null) _workbook.Close(false, Missing.Value, false); if (_excelApp != null) _excelApp.Quit(); System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp); System.Runtime.InteropServices.Marshal.ReleaseComObject(_workbook); //System.Runtime.InteropServices.Marshal.ReleaseComObject( _selectedRange ); GC.Collect(); } catch { } }
但上面這些做不到關閉打開的Excel文件,若需要做到關閉Excel文件:
[DllImport("User32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int ID); public void BeforeClosing() { if (_excelApp != null) { IntPtr t = new IntPtr(_excelApp.Hwnd); int k = 0; GetWindowThreadProcessId(t, out k); System.Diagnostics.Process p = System.Diagnostics.Process.GetProcessById(k); p.Kill(); } }
三、總結
花了一定的時間,工作上想實現的實現了,同時也將問題的解決總結了下來。這個過程自己有很大的收獲,以后遇到問題時也一定要像現在這樣將問題深入全面的了解並解決,多解決問題,多總結解決問題的過程,這樣才能讓自己寒酸的技術和表達能力一點一點的得到提高。
