【问题标题】:Trimming all cells in DataTable修剪 DataTable 中的所有单元格
【发布时间】:2020-07-10 00:19:48
【问题描述】:

我正在使用下面的代码来修剪我的 DataTable 中的所有单元格。

问题是,我是通过一个循环来完成的,并且根据我填充 DataTable 的内容,如果它有 1500 行和 20 列,那么循环需要非常非常长的时间。

DataColumn[] stringColumns = dtDataTable.Columns.Cast<DataColumn>().Where(c => c.DataType == typeof(string)).ToArray();
foreach (DataRow row in dtDataTable.Rows)
{
    foreach (DataColumn col in stringColumns)
    {
        if (row[col] != DBNull.Value)
        {
            row.SetField<string>(col, row.Field<string>(col).Trim());
        }
    }
}

下面是我将 Excel 工作表导入 DataTable 的方式:

using (OpenFileDialog ofd = new OpenFileDialog() { Title = "Select File", Filter = "Excel WorkBook|*.xlsx|Excel WorkBook 97-2003|*.xls|All Files(*.*)|*.*", Multiselect = false, ValidateNames = true })
{               
    if (ofd.ShowDialog() == DialogResult.OK)
    {
        String PathName = ofd.FileName;
        FileName = System.IO.Path.GetFileNameWithoutExtension(ofd.FileName);
            
        strConn = string.Empty;

        FileInfo file = new FileInfo(PathName);
        if (!file.Exists) { throw new Exception("Error, file doesn't exists!"); }
        string extension = file.Extension;
        switch (extension)
        {
            case ".xls":
                strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + PathName + ";Extended Properties='Excel 8.0;HDR=Yes;IMEX=1;'";
            case ".xlsx":
                strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + PathName + ";Extended Properties='Excel 12.0;HDR=Yes;IMEX=1;'";
            default:
                strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + PathName + ";Extended Properties='Excel 8.0;HDR=Yes;IMEX=1;'";
        }
    }
    else
    {
        return;
    }
}
    
using (OleDbConnection cnnxls = new OleDbConnection(strConn))
{
    using (OleDbDataAdapter oda = new OleDbDataAdapter(string.Format("select * from [{0}$]", "Sheet1"), cnnxls))
    {
        oda.Fill(dtDataTableInitial);
    }
}


//Clone dtDataTableInitial so that I can have the new DataTable in String Type
dtDataTable = dtDataImportInitial.Clone();
foreach (DataColumn col in dtDataTable.Columns)
{
    col.DataType = typeof(string);
}
foreach (DataRow row in dtDataImportInitial.Rows)
{
    dtDataTable.ImportRow(row);
}

有没有更有效的方法来实现这一点?





编辑: 根据 JQSOFT 的建议,我现在正在使用 OleDbDataReader,但仍在运行两个问题:

一个: SELECT RTRIM(LTRIM(*)) FROM [Sheet1$] 似乎不起作用。

我知道可以一一选择每一列,但是 excel 表中列的数量和标题是随机的,我不知道如何调整我的 SELECT 字符串来解决这个问题。
二:一列的行主要是数字,但有几行字母似乎省略了那些字母的行。例如:
Col1
1
2
3
4
5
6
一个
b

变成:
Col1
1
2
3
4
5
6

但是,我发现如果我手动进入 excel 表并将整个表格单元格格式转换为“文本”,这个问题就解决了。 但是,这样做会将 Excel 工作表中的任何日期转换为无法识别的数字字符串,因此我希望尽可能避免这样做。
例如:7/2/2020 如果转换为“文本”,则变为 44014。



这是我的新代码:

private void Something()
{
    if (ofd.ShowDialog() == DialogResult.OK)
    {
        PathName = ofd.FileName;
        FileName = System.IO.Path.GetFileNameWithoutExtension(ofd.FileName);

        strConn = string.Empty;

        FileInfo file = new FileInfo(PathName);
        if (!file.Exists) { throw new Exception("Error, file doesn't exists!"); }
    }

    using (OleDbConnection cn = new OleDbConnection { ConnectionString = ConnectionString(PathName, "No") })
    {
        using (OleDbCommand cmd = new OleDbCommand { CommandText = query, Connection = cn })
        {
            cn.Open();
            OleDbDataReader dr = cmd.ExecuteReader();
            dtDataTable.Load(dr);
        }
    }

    dataGridView1.DataSource = dtDataTable;
}               

public string ConnectionString(string FileName, string Header)
{
    OleDbConnectionStringBuilder Builder = new OleDbConnectionStringBuilder();
    if (Path.GetExtension(FileName).ToUpper() == ".XLS")
    {
        Builder.Provider = "Microsoft.Jet.OLEDB.4.0";
        Builder.Add("Extended Properties", string.Format("Excel 8.0;IMEX=1;HDR=Yes;", Header));
    }
    else
    {
        Builder.Provider = "Microsoft.ACE.OLEDB.12.0";
        Builder.Add("Extended Properties", string.Format("Excel 12.0;IMEX=1;HDR=Yes;", Header));
    }

    Builder.DataSource = FileName;

    return Builder.ConnectionString;
}

【问题讨论】:

  • 你为什么要修剪它们
  • 我会在 SQL 中实现修剪。基于集合的操作总是比循环快。您正在循环中进行循环,因此性能会随着行数/列数而几何下降。
  • @LegacyCode 好吧,我正在将一个 Excel 工作表导入我的应用程序,有时会有不应该存在的前导和尾随空格。
  • @JQSOFT 谢谢!我会读一读:)。非常感谢!
  • 更新了原帖。

标签: c# .net


【解决方案1】:

OleDb 对象


实际上我的意思是,要从 Excel 工作表中获取格式化/修剪过的字符串值并创建一个仅包含字符串类型的 DataColumn 对象的 DataTable,请使用仅转发 OleDbDataReader 来创建 DataColumn 和 DataRow 对象读。这样做,数据将在一个步骤中被修改和填充,因此无需调用另一个例程再次循环并浪费更多时间。此外,请考虑使用异步调用来加快进程并避免在执行冗长任务时冻结 UI。

有些事情可能会帮助你去:

private async void TheCaller()
{
    using (var ofd = new OpenFileDialog
    {
        Title = "Select File",
        Filter = "Excel WorkBook|*.xlsx|Excel WorkBook 97 - 2003|*.xls|All Files(*.*)|*.*",
        AutoUpgradeEnabled = true,
    })
    {
        if (ofd.ShowDialog() != DialogResult.OK) return;

        var conString = string.Empty;
        var msg = "Loading... Please wait.";

        try
        {
            switch (ofd.FilterIndex)
            {
                case 1: //xlsx
                    conString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={ofd.FileName};Extended Properties='Excel 12.0;HDR=Yes;IMEX=1;'";                            
                    break;
                case 2: //xls
                    conString = $"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={ofd.FileName};Extended Properties='Excel 8.0;HDR=Yes;IMEX=1;'";
                    break;
                default:
                    throw new FileFormatException();
            }

            var sheetName = "sheet1";
            var dt = new DataTable();

            //Optional: a label to show the current status
            //or maybe show a ProgressBar with ProgressBarStyle = Marquee
            lblStatus.Text = msg;

            await Task.Run(() =>
            {
                using (var con = new OleDbConnection(conString))
                using (var cmd = new OleDbCommand($"SELECT * From [{sheetName}$]", con))
                {
                    con.Open();

                    using (var r = cmd.ExecuteReader())
                        while (r.Read())
                        {
                            if (dt.Columns.Count == 0)
                                for (var i = 0; i < r.FieldCount; i++)
                                    dt.Columns.Add(r.GetName(i).Trim(), typeof(string));

                            object[] values = new object[r.FieldCount];

                            r.GetValues(values);
                            dt.Rows.Add(values.Select(x => x?.ToString().Trim()).ToArray());
                        }
                }
            });

            //If you want...
            dataGridView1.DataSource = null;
            dataGridView1.DataSource = dt;

            msg = "Loading Completed";
        }
        catch (FileFormatException)
        {
            msg = "Unknown Excel file!";
        }
        catch (Exception ex)
        {
            msg = ex.Message;
        }
        finally
        {
            lblStatus.Text = msg;
        }
    }
}

这是一个演示,从 xlsxlsx 文件中读取包含 8 列和 5000 行的表格:

不到一秒钟。还不错。

但是,如果工作表具有混合类型的列,例如第三列在不同行中具有 stringint 值的情况,这将无法正常工作。这是因为默认情况下,通过检查前 8 行,在 Excel 中猜测列的数据类型。更改此行为需要将 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\x.0\Engines\Excel 中的 TypeGuessRows 的注册表值从 8 更改为 0,以强制检查所有行而不是仅检查前 8 行。此操作将显着降低性能。

Office 互操作对象


或者,您可以使用 Microsoft.Office.Interop.Excel 对象来读取 Excel 工作表,获取和格式化单元格的值,无论其类型如何。

using Excel = Microsoft.Office.Interop.Excel;
//...

private async void TheCaller()
{
    using (var ofd = new OpenFileDialog
    {
        Title = "Select File",
        Filter = "Excel WorkBook|*.xlsx|Excel WorkBook 97 - 2003|*.xls|All Files(*.*)|*.*",
        AutoUpgradeEnabled = true,
    })
    {
        if (ofd.ShowDialog() != DialogResult.OK) return;

        var msg = "Loading... Please wait.";
        Excel.Application xlApp = null;
        Excel.Workbook xlWorkBook = null;

        try
        {
            var dt = new DataTable();

            lblStatus.Text = msg;

            await Task.Run(() =>
            {
                xlApp = new Excel.Application();
                xlWorkBook = xlApp.Workbooks.Open(ofd.FileName, Type.Missing, true);

                var xlSheet = xlWorkBook.Sheets[1] as Excel.Worksheet;
                var xlRange = xlSheet.UsedRange;

                dt.Columns.AddRange((xlRange.Rows[xlRange.Row] as Excel.Range)
                .Cells.Cast<Excel.Range>()
                .Where(h => h.Value2 != null)
                .Select(h => new DataColumn(h.Value2.ToString()
                .Trim(), typeof(string))).ToArray());

                foreach (var r in xlRange.Rows.Cast<Excel.Range>().Skip(1))
                    dt.Rows.Add(r.Cells.Cast<Excel.Range>()
                        .Take(dt.Columns.Count)
                        .Select(v => v.Value2 is null
                        ? string.Empty
                        : v.Value2.ToString().Trim()).ToArray());
            });

            (dataGridView1.DataSource as DataTable)?.Dispose();
            dataGridView1.DataSource = null;
            dataGridView1.DataSource = dt;

            msg = "Loading Completed";
        }
        catch (FileFormatException)
        {
            msg = "Unknown Excel file!";
        }
        catch (Exception ex)
        {
            msg = ex.Message;
        }
        finally
        {
            xlWorkBook?.Close(false);
            xlApp?.Quit();

            Marshal.FinalReleaseComObject(xlWorkBook);
            Marshal.FinalReleaseComObject(xlApp);

            xlWorkBook = null;
            xlApp = null;

            GC.Collect();
            GC.WaitForPendingFinalizers();

            lblStatus.Text = msg;
        }
    }
}

注意:你需要add reference到提到的图书馆。

速度不快,尤其是在有大量单元格的情况下,但它可以获得所需的输出。

【讨论】:

  • 啊 - 这就像魔术一样!但是,我注意到如果 Excel 表中的第一行(或者可能是列)包含单选按钮之类的对象,则代码不会处理,只会挂起。如果有对象,有没有办法调整该代码以忽略第一行?
  • 有时 Excel 工作表中的第一个单元格包含“垃圾数据”字样,如果有帮助的话 - 并且该行包含单选按钮等对象。
  • @lolikols 无法重现该问题。该代码对我有用,除非它是启用宏的工作表。 (xlsm)。您可以将文件与一些虚假数据共享以进行测试吗?
  • 没关系,很抱歉 - 原来是我忘记注释掉的一行代码。我仍然遇到一个问题,如果一列主要是数字,并且有几个字母 - 这些字母会被完全忽略。这是一个示例 - 在最后 3 行中,第 3 列中的字母将被导入过程省略:dropbox.com/s/nxey2zl6jyvmrz3/SS%20TEST.xlsx?dl=0
  • @lolikols 问题是你有一个混合类型的列,字符/字符串和整数类型。 Provider 使用前 8 行来猜测列数据类型。因此,在我们这里的例子中,提供者将Col3 的类型定义为整数,因此省略了带有字符串值的最后一个值。您需要编辑一些注册表项以更改此行为并强制提供程序检查所有行以识别类型。阅读this帖子了解详情。
猜你喜欢
  • 2012-09-09
  • 1970-01-01
  • 2018-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-27
  • 2020-01-20
相关资源
最近更新 更多