• 彻底解决Oledb连接Excel数据类型不统一的问题

    by{ guangboo }, published {2010-08-04}, Tag { Excel / }

    在使用Microsoft.Jet.OLEDB.4.0连接Excel,进行读取数据,相对使用传统的COM来读取数据,效率是很高的。但相对传统COM操作Excel来说,及存在数据类型转换的问题。

    因为使用OLEDB连接Excel读取数据时,需要确定数据的类型。默认情况使用连接字符串:

    string connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + excelFile + ";Extended Properties='Excel 8.0;'";

     使用上面的连接字符串连接Excel时,可能会遇到数据类型不一致的问题。所谓数据类型不一致,是指同一列里面数据类型可能出现多种,如浮点数、字符串、日期等;当出现此类情况时,读取出来的数据就为空,甚至会报错,如“非法的日期格式”等异常。出现这种问题,我们大家都会想到把数据全部都按字符数据来读取,但是按什么数据类型来读取不是我们能控制的,是OLEDB控制的,至少暂时我还没有找到能控制输出数据类型的方法。因为我当初也尝试使用convert,cast函数对输出的列进行类型转换,但oledb连接Excel时,使用的SQL不支持这些函数。因此只能从其他角度来解决该问题。我也在网上搜索了很多解决方法,最全面的解决方法是:http://www.douban.com/note/18510346/。下面列出了网上出现解决该问题方法的比较:

    解决方案说明缺点
    COM 使用Excel COM接口访问Excel 非托管、不容易释放资源、效率低下
    连接字符串添加IMEX=1 构造的连接字符串,如:
    string strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + excelFile + ";Extended Properties='Excel 8.0;HDR=YES;IMEX=1'";
    
    其中HDR表示是否将Sheet页的第一行作为字段名,“YES”代表是,“NO”代表不是,当为YES时,将把SHEET页的第一行作为字段名,数据从第二行开始,而如果是NO时,字段名就是要SHEET的列名,如A,B,C等,数据就从第一行开始取;IMEX是用来告诉驱动程序,使用Excel文件的模式,其值有0、1、2三种,分别代表导出、导入、混合模式。当我们设置IMEX=1时将强制混合数据转换为文本,但仅仅这种设置并不可靠,IMEX=1只确保在某列前8行数据至少有一个是文本项的时候才起作用,它只是把查找前8行数据中数据类型占优选择的行为作了略微的改变。例如某列前8行数据全为纯数字,那么它仍然以数字类型作为该列的数据类型,随后行里的含有文本的数据仍然变空。 (摘至:http://www.douban.com/note/18510346/)。
    只根据前8行数据判断是否使用字符类型
    IMEX=1与注册表值TypeGuessRows配合使用 TypeGuessRows 值决定了ISAM 驱动程序从前几条数据采样确定数据类型,默认为“8”。可以通过修改“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\4.0\Engines\Excel”下的该注册表值来更改采样行数。但是这种改进还是没有根本上解决问题,即使我们把IMEX设为“1”, TypeGuessRows设得再大,例如1000,假设数据表有1001行,某列前1000行全为纯数字,该列的第1001行又是一个文本,ISAM驱动的这种机制还是让这列的数据变成空。 (摘至:http://www.douban.com/note/18510346/)。 修改注册表不方便,而且无法事先判读sheet有多少行,因此还是受行数限制。
    将Excel先转换成csv纯文本格式

    (1)在读取Excel的.xls类型的文本数据之前,先将其转换为.csv格式,在Excel中直接另存为这种格式就可以达到转换的目的。CSV文件又称为逗号分隔的文件,是一种纯文本文件,它以“,”分隔数据列。

      

    需要指出的是,CSV文件也可以用Ole DB或ODBC的方式读取,但是如果采用这些方式读取其数据又会回到丢失数据的老路上,ISAM机制同样会发挥作用。

      

    (2)采用普通的读取文本文件的方法打开文件,读取第一行,用“,”作为分隔符获得各字段名,在DataTable中创建对应的各字段,字段的类型可以统一创建成“String”。

    (3)逐行读取数据行, 用“,”作为分隔符获得某行各列的数据并填入DataTable相应的字段中。

    简要代码:

    String line; 
    String [] split = null; 
    DataTable table=new DataTable("auto"); 
    DataRow row=null; 
    StreamReader sr=new StreamReader("c:/auto.csv",System.Text.Encoding.Default); 
    //创建与数据源对应的数据列 
    line = sr.ReadLine(); 
    split=line.Split(','); 
    foreach(String colname in split){ 
      table.Columns.Add(colname,System.Type.GetType("System.String")); 
    } 
    //将数据填入数据表 
    int j=0; 
    while((line=sr.ReadLine())!=null){ 
      j=0; 
      row = table.NewRow(); 
      split=line.Split(','); 
      foreach(String colname in split){ 
          row[j]=colname; 
          j++;
       } 
      table.Rows.Add(row);
    } 
    sr.Close(); 
    //显示数据 
    dataGrid1.DataSource=table.DefaultView; 
    dataGrid1.DataBind(); 
    
    (摘至:http://www.douban.com/note/18510346/)。
    需要事先将excel转换成csv文件

    这里提供一个更加方便的办法,不过前提是第一行必须是作为字段名或者第一行的数据类型就为字符型。这样一说,大家就明白了。首先修改连接字符串为:

    string strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + excelFile + ";Extended Properties='Excel 8.0;HDR=NO;IMEX=1'";

    这里将HDR设为NO,因为我就是将第一行做为数据读取,而IMEX=1就表示根据前8行判断列的数据类型,如果有字符型数据,那么就强制混合数据转换为文本。这里就明白为什么要保证第一行为字符型的原因了。能将列的数据类型强制设为字符型,那么列中出现什么类型的数据都不怕了。需要做的工作就是,在获取完数据后,将字段名重新设置,并删除第一条记录即可。代码如下:

    DataTable dt = new DataTable();
    
    using(OleDbCommand cmd = new OleDbCommand()){
        cmd.Connection = conn;
        cmd.CommandType = CommandType.Text;
        cmd.CommandTimeout = 6;
        cmd.CommandText = string.Format("select * from [{0}$]", sheetName);
    
        OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        adapter.Fill(dt);
    }
    
    if (dt.Rows.Count > 0) {
        DataRow dr = dt.Rows[0];
    
        for (int col = 0; col < dt.Columns.Count; col++) {
            dt.Columns[col].ColumnName = dr[col].ToString();
        }
    
        dt.Rows[0].Delete();
        dt.AcceptChanges();
    }
    

    这样最终得到的数据就是你希望的数据了。

  • Excel打开工作簿事件Appevents_Workbookopeneventhandler

    by{ guangboo }, published {2010-06-09}, Tag { Excel / }

    Excel插件编程,当Excel打开文档时候触发某一动作。

    金融行业中Excel做各种数据分析时,一般都有Excel模板,模板里面包含了很多的VBA函数,当然这些VBA函数里面有很多是自定义的,是需要与服务器交互,达到获取即时数据的目的,如股票当前价格等。这些函数需要通过计算(与服务器通信)更新模板中的内容,然后保存当前快照,从而得到一个分析数据。

    这样的需求一般都需要一个重新计算的按钮,这个按钮的事件就是触发重新计算的,当然Excel中有自动的重算设置,但Excel默认的重算设置在这里是不起作用的,因为只有Excel能够捕获到某些值发生变化时才会启动重算(即使你将重算设置设成了自动重算)。

    因为Excel不知道单元格的值有变化,才不会自动重算的,那么我们可以通过将单元格的值设为Dirty,即表示单元格的值发生了变化时,那么Excel就不在拒绝重算了。如:

    // 保存原重算设置
    Microsoft.Office.Interop.Excel.XlCalculation saveXlCalculation = Globals.ThisAddIn.Application.Calculation;
    // 设为手动重算
    Globals.ThisAddIn.Application.Calculation = Microsoft.Office.Interop.Excel.XlCalculation.xlCalculationManual;
    // 获取当前活动工作表
    Microsoft.Office.Interop.Excel.Worksheet sheet = (Excel.Worksheet)Globals.ThisAddIn.Application.ActiveSheet;
    // 重算当前工作表
    sheet.UsedRange.Dirty();
    // 还原重算设置
    Globals.ThisAddIn.Application.Calculation = saveXlCalculation;
    

    你也可以通过变了工作簿,来重算所有工作表。

    另外你可以通过,下面的代码快速计算:

    // 计算所有打开的工作簿
    Globals.ThisAddIn.Application.Calculate()

    或者:

    // 促使对所有打开工作簿中的数据进行完整计算
    Globals.ThisAddIn.Application.CalculateFull();

    或者:

    // 对于所有打开的工作簿,强制数据的完整计算并重建从属关系。
    Globals.ThisAddIn.Application.CalculateFullRebuild();

    重新计算的工作可以实现了。那么很多研究员还希望在打开Excel模板文件的时候能够自动计算,不需要手动点击重算按钮了。

    这是就需要一个WorkbookOpen事件:Microsoft.Office.Interop.Excel.AppEvents_WorkbookOpenEventHandler,该委托需要绑定到Application.WorkbookOpen事件上,如下代码:

    private void ThisAddIn_Startup(object sender, System.EventArgs e){
        Globals.ThisAddIn.Application.WorkbookOpen += new Microsoft.Office.Interop.Excel.AppEvents_WorkbookOpenEventHandler(FExcelApplication_WorkbookOpen);
        // 省略其他代码
    }
    void FExcelApplication_WorkbookOpen(Microsoft.Office.Interop.Excel.Workbook Wb) {
        if((new ConfigClass()).AutoCalcAfterLogih) {
            // 重新计算Wb
        }
    }

    这样,当研究员打开模板后Excel插件就会自动重算打开的工作簿