C#Excel文件讀取問題及解決辦法


       最近由於工作上用到了讀取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行,需要修改注冊表值TypeGuessRowsTypeGuessRows 值決定了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);
     }

 

         第二步,根據選擇的Sheet,讀取excel表格中相應sheet名稱的數據
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();
            }

        }

                                                                                                   三、總結
        花了一定的時間,工作上想實現的實現了,同時也將問題的解決總結了下來。這個過程自己有很大的收獲,以后遇到問題時也一定要像現在這樣將問題深入全面的了解並解決,多解決問題,多總結解決問題的過程,這樣才能讓自己寒酸的技術和表達能力一點一點的得到提高。

       


  


      

   

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM