【问题标题】:Importing General formatted cells from Excel files via C#通过 C# 从 Excel 文件中导入通用格式的单元格
【发布时间】:2017-05-13 21:41:04
【问题描述】:

我目前使用:

  • Visual Studio 2015 更新 3
  • Microsoft Office Professional Plus 2013
  • .Net Framework 4.5.1
  • Windows 7 64 位

我正在使用以下代码将 Excel 工作表读入 DataTable:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.OleDb;
using System.Data.SqlClient;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

string filename = "C:\\Users\\myusername\\Documents\\MyFile.xlsx";

DataTable dt = null;

try
{
    string ExcelName = filename.Split(("\\").ToCharArray()[0])[filename.Split(("\\").ToCharArray()[0]).Length - 1].Split('.')[0];

    string ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0;HDR=YES;IMEX=1;TypeGuessRows=0;ImportMixedTypes=Text\";";

    string SheetName = CommonFunctions.GetExcelSheetNames(ConnectionString)[0];

    using (OleDbConnection conn = new OleDbConnection(ConnectionString))
    {
        string query = string.Format("SELECT * FROM [" + SheetName + "]");

        conn.Open();

        using (OleDbCommand cmd = new OleDbCommand(query, conn))
        {
            using (OleDbDataReader rdr = cmd.ExecuteReader())
            {
                if (rdr.HasRows)
                {
                    dt = new DataTable();

                    dt.TableName = ExcelName;

                    for (int i = 0; i < rdr.FieldCount; i++)
                    {
                        dt.Columns.Add(new DataColumn(rdr.GetName(i), typeof(string)));

                    }

                    while (rdr.Read())
                    {
                        DataRow dr = dt.NewRow();

                        for (int i = 0; i < rdr.FieldCount; i++)
                        {
                            dr[i] = rdr[i].ToString();

                        }

                        dt.Rows.Add(dr);

                    }

                    foreach (DataRow row in dt.Rows)
                    {
                        string s = row[0].ToString();

                    }

                }

            }

        }

    }

}
catch (Exception ex)
{
    MessageBox.Show(ex.StackTrace, ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);

}

示例 Excel 工作表数据如下,以 CSV 格式提供,便于在 Excel 中打开:

"store_number", "stock_code", "desired_quantity"
"64004", "BI_KRA_SEL_350065", "1"
"64004", "BI_KRA_SEL_500080", "1"
"86208", "BI_KRA_SEL_350065", "1"
"86208", "BI_KRA_SEL_500080", "1"
"64019", "BI_KRA_SEL_350065", "1"
"64019", "BI_KRA_SEL_500080", "1"
"85858", "BI_KRA_SEL_350065", "1"
"85858", "BI_KRA_SEL_500080", "1"
"72122", "BI_KRA_SEL_350065", "1"
"72122", "BI_KRA_SEL_500080", "1"
"68427", "BI_KRA_SEL_350065", "1"
"68427", "BI_KRA_SEL_500080", "1"
"79031", "BI_KRA_SEL_350065", "1"
"79031", "BI_KRA_SEL_500080", "1"
"67662", "BI_KRA_SEL_350065", "1"
"67662", "BI_KRA_SEL_500080", "1"
"92246", "BI_KRA_SEL_350065", "1"
"92246", "BI_KRA_SEL_500080", "1"
"85432", "BI_KRA_SEL_350065", "1"
"85432", "BI_KRA_SEL_500080", "1"
"87188", "BI_KRA_SEL_350065", "1"
"87188", "BI_KRA_SEL_500080", "1"
"91021", "BI_KRA_SEL_350065", "1"
"91021", "BI_KRA_SEL_500080", "1"
"79022", "BI_KRA_SEL_350065", "1"
"79022", "BI_KRA_SEL_500080", "1"
"86369", "BI_KRA_SEL_350065", "1"
"86369", "BI_KRA_SEL_500080", "1"
"67670", "BI_KRA_SEL_350065", "1"
"67670", "BI_KRA_SEL_500080", "1"
"92605", "BI_KRA_SEL_350065", "1"
"92605", "BI_KRA_SEL_500080", "1"
"92609", "BI_KRA_SEL_350065", "1"
"92609", "BI_KRA_SEL_500080", "1"
"92610", "BI_KRA_SEL_350065", "1"
"92610", "BI_KRA_SEL_500080", "1"
"92611", "BI_KRA_SEL_350065", "1"
"92611", "BI_KRA_SEL_500080", "1"
"92612", "BI_KRA_SEL_350065", "1"
"92612", "BI_KRA_SEL_500080", "1"
"92613", "BI_KRA_SEL_350065", "1"
"92613", "BI_KRA_SEL_500080", "1"
"92614", "BI_KRA_SEL_350065", "1"
"92614", "BI_KRA_SEL_500080", "1"
"92615", "BI_KRA_SEL_350065", "1"
"92615", "BI_KRA_SEL_500080", "1"
"92616", "BI_KRA_SEL_350065", "1"
"92616", "BI_KRA_SEL_500080", "1"
"w090", "BI_KRA_SEL_350065", "1"
"w090", "BI_KRA_SEL_500080", "1"
"C908", "BI_KRA_SEL_350065", "1"
"C908", "BI_KRA_SEL_500080", "1"
"w0901", "BI_KRA_SEL_350065", "1"
"w0901", "BI_KRA_SEL_500080", "1"
"G202", "BI_KRA_SEL_350065", "1"
"G202", "BI_KRA_SEL_500080", "1"

问题是当第一列包含字母和/或空格时。这些单元格在生成的 DataTable (dt) 中显示为空白,即从“w090”到“G202”。

我发现当单元格格式设置为“常规”时会发生这种情况。但是,将这些单元格的格式更改为“文本”似乎可以解决问题。

我现在唯一的问题是我不能依赖我的客户提供将单元格设置为“文本”格式的文件。

有没有人知道解决这个问题的方法,或者可能是克隆具有“文本”格式的 Excel 文件的方法?

也许有人知道将 Excel 文件导入 DataTables/DataSets 的更聪明的方法。

非常感谢任何帮助。

【问题讨论】:

  • 不知道为什么string filename = "C:\Users\myusername\Documents\MyFile.xlsx"; 应该工作。不是必须是string filename = @"C:\Users\myusername\Documents\MyFile.xlsx";string filename = "C:\\Users\\myusername\\Documents\\MyFile.xlsx"; 吗?或者是否有一些通用设置将所有字符串文字都作为逐字字符串文字?
  • @Axel Richter 谢谢。我已经更正了字符串。注意:我可以选择在字符串前面加上 @ 符号。
  • Hambone 的回答促使我将 adapter.fill 替换为 DataReader,以便更好地控制调试。在当前调查点,发现数据在rdr.Read() 之前或之前丢失。

标签: c# .net excel import datatable


【解决方案1】:

由于您将Excel 用作数据库,因此每个字段(列)必须具有其确切的数据类型。 Excel 数据库驱动程序从第一个值中猜测这种类型。在您的情况下,这些第一个值是第一列中的数字。所以数据库在那里猜测数字数据类型。所以后面出现的字符串不适合那种类型。

数据库驱动程序有一个参数IMEX,它导致将所有数据视为文本。见https://www.connectionstrings.com/ace-oledb-12-0/treating-data-as-text/

所以试试

string connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0;IMEX=1\";";

string connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0;HDR=NO;IMEX=1\";";

【讨论】:

  • 这听起来像是完美的逻辑解决方案,但即使 IMEX = 1,单元格仍被视为数字。我替换了行,string connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0; IMEX=1\";";,并通过使用像这样的 foreach 递增,foreach(DataRow row in dt.Rows) { string s = row[0].ToString(); },但最后几个单元格仍然是空白。
  • 我没有使用C# .NET 的经验。但是你能试试完整的连接字符串,包括参数HDR:string connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0; HDR=NO; IMEX=1\";";吗?如果这也不起作用,那么我无能为力。
  • 嗯,在connectionstrings.com/ace-oledb-12-0/treating-data-as-text 中显示似乎具有误导性。 Extended Properties="Excel 12.0 Xml;HDR=YES;IMEX=1" 中的分号和下一个属性之间没有空格。我已经更新了我的答案。
  • 一开始我尝试string connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + ";Extended Properties=\"Excel 12.0;HDR=NO;IMEX=1\";"; 最初,我认为这行得通,但这是由于HDR=NO,使第一个单元格成为文本值。将值更改为HDR=YES 意味着第一行再次用作标题(这验证了 HDR 开关至少可以工作),但最后几个单元格仍然是空白。我不知道为什么,但 IMEX 开关似乎不起作用,或者至少我们使用不正确。
【解决方案2】:

这比仅仅使用adapter.Fill() 需要更多的工作,但是如果你想对数据类型有更多的控制权,你可以预先声明它们,然后一次填充一行数据表。因为它是 Excel,而且 Excel 很乐意在一列中混合和匹配数据类型,所以我认为 OleDb 几乎不可能预先确定正确的数据类型。

这是一个示例,说明如何显式声明数据类型,然后手动将它们插入数据表:

OleDbCommand cmd = new OleDbCommand(query, conn);
OleDbDataReader reader = cmd.ExecuteReader();

object[] fields = new object[reader.FieldCount];
for (int i = 0; i < fields.Length; i++)
    dt.Columns.Add(new DataColumn(reader.GetName(i)));

dt.Columns[0].DataType = typeof(string);
dt.Columns[1].DataType = typeof(string);
dt.Columns[2].DataType = typeof(int);

while (reader.Read())
{
    reader.GetValues(fields);
    dt.Rows.Add(fields);
}

reader.Close();

-- 编辑 2017 年 1 月 3 日--

这是一个使用 POCO 的解决方案,我相信它会起作用:

如果你的 POCO 看起来像这样:

public class Stock
{
    public string StoreNumber { get; set; }
    public string StockCode { get; set; }
    public double DesiredQuantity { get; set; }
}

此代码应从 Excel 中读取数据并将其放入域对象列表中:

OleDbConnection conn = new OleDbConnection(ConnectionString);
conn.Open();

OleDbCommand cmd = new OleDbCommand(query, conn);
OleDbDataReader reader = cmd.ExecuteReader();

List<Stock> stockData = new List<Stock>();

while (reader.Read())
{
    stockData.Add(new Stock()
    {
        StoreNumber = reader.GetValue(0).ToString(),
        StockCode = reader.GetValue(1).ToString(),
        DesiredQuantity = reader.GetDouble(2)
    });
}

reader.Close();

我认为.GetString(x) 可能会由于您突出显示的问题而引发错误,但是通过使用.GetValue(x).ToString(),您可以暴力破解数据类型,因为它们都应该是字符串。

从这里开始,我认为与数据表相比,使用List&lt;Stock&gt; 将是一种乐趣。最好的部分是您可以完全控制数据。

【讨论】:

  • 这很有趣。不幸的是,它仍然不起作用,但我很欣赏这个解决方案提供的额外控制。感谢这个答案,我发现阅读器中的数据类型(reader.MetaData.[0].columnBinding.ExpectedType)是 System.Double,所以我一直在尝试查询。但是,我发现 OleDb 类不支持 CAST 和 CONVERT。我还发现提示将注册表中名为“TypeGuessRows”的值更改为 0,但这也无济于事。
  • 我想我应该问一下……你嫁给了数据表吗?读取数据后,您在做什么?我偏爱 POCO 而不是数据表,但即使我承认他们也有时间和地点。如果您提前知道内容(看起来您知道),您可以对对象施加更多控制。
  • 对我来说听起来很棒,我欢迎尝试基于 POCO 的解决方案。我使用 DataTables 的原因是因为它们是通用的,并且很容易作为 DataSource 传递给 WinForms 应用程序中的 GridView 控件。我完全不介意使用专门的课程,特别是如果它解决了我的问题并意味着我不再丢失数据。
  • 太棒了!我举了一个使用 POCO 的例子,这次我对它进行了实际测试......不管它的价值如何,它在你提供的样本数据上对我有用。
  • 还是不开心。根本问题似乎是在数据传递给读者之前的某个时刻。我想知道它对您有用的原因是否是因为单元格被格式化为“文本”而不​​是“一般”。我真的很感谢你在这方面的帮助。我觉得我们非常接近解决方案,几乎可以触摸它。我也同意你关于 POCO 的看法。它们非常优雅,并且比 DataTables 更易于调试。
【解决方案3】:

这是一个使用 Interop 服务的可靠解决方案。

添加参考:Microsoft.Office.Interop.Excel 版本 12.0.0.0,可通过 Nuget 获得。

注意:请务必将完整路径作为文件名参数传递。

public static DataTable LoadExcelFile(string fileName, string worksheetName, int headerRowNumber, int firstDataRowNumber)
{
    DataTable dt = new DataTable();

    Microsoft.Office.Interop.Excel.Application ExcelApplication = new Microsoft.Office.Interop.Excel.Application();

    Microsoft.Office.Interop.Excel.Workbook ExcelWorkbook = ExcelApplication.Workbooks.Open(fileName, 0, true, 5, "", "", true, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "\t", false, false, 0, true, 1, 0);

    Microsoft.Office.Interop.Excel.Worksheet ExcelWorksheet = null;

    string WorksheetName = worksheetName;

    if (string.IsNullOrWhiteSpace(worksheetName))
    {
        WorksheetName = ExcelWorkbook.ActiveSheet.Name;

    }

    ExcelWorksheet = (Microsoft.Office.Interop.Excel.Worksheet)ExcelWorkbook.Worksheets[WorksheetName];

    dt.TableName = WorksheetName;

    // Add the columns

    Dictionary<string, int> Columns = new Dictionary<string, int>();

    for (int i = 0; i < ExcelWorksheet.UsedRange.Columns.Count; i++)
    {
        string ColumnHeading = Convert.ToString(((Microsoft.Office.Interop.Excel.Range)ExcelWorksheet.Cells[headerRowNumber, i + 1]).Value2);

        if (!String.IsNullOrWhiteSpace(ColumnHeading) && !dt.Columns.Contains(ColumnHeading))
        {
            Columns.Add(ColumnHeading, i + 1);

            dt.Columns.Add(ColumnHeading);

        }

    }

    // Add the rows

    for (int i = 0; i < ExcelWorksheet.UsedRange.Rows.Count - firstDataRowNumber + 1; i++)
    {
        try
        {
            int ColumnCount = 0;

            DataRow Row = dt.NewRow();

            bool RowHasContent = false;

            foreach (KeyValuePair<string, int> kvp in Columns)
            {
                string CellContent = Convert.ToString(((Microsoft.Office.Interop.Excel.Range)ExcelWorksheet.Cells[i + firstDataRowNumber, kvp.Value]).Value2);

                Row[ColumnCount] = CellContent;

                ColumnCount++;

                if (!string.IsNullOrWhiteSpace(CellContent))
                {
                    RowHasContent = true;

                }

            }

            if (RowHasContent)
            {
                dt.Rows.Add(Row); ;

            }

        }
        catch
        {

        }

    }

    // Clean up

    try { ExcelWorksheet = null; } catch { }

    try { ExcelWorkbook.Close(); } catch { }

    try { ExcelWorkbook = null; } catch { }

    try { ExcelApplication = null; } catch { }

    return dt;
}

为什么要使用互操作服务?

互操作服务解决了在使用基于 JetAce 数据库引擎的解决方案时遇到的类型猜测和格式化错误。

错误解决方案示例如下:

  • ExcelDataReader
  • LinqToExcel
  • OleDb

如果您目前使用上述解决方案之一,则值得测试错误,并将成功的解决方案发布到此页面。

愉快的惊喜

使用 Interop 解决方案时,我预计会在 Excel 中打开一个与程序中打开的文件同名的文件时出现问题,但在测试期间我没有遇到此类问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-28
    • 1970-01-01
    • 2012-10-28
    • 2013-11-11
    • 1970-01-01
    相关资源
    最近更新 更多