C#Excel Interop在循环单元格时变慢

我试图从C#中的Excel文档中提取所有文本数据,并遇到性能问题。 在下面的代码中,我打开工作簿,遍历所有工作表,并循环使用范围中的所有单元格,从中提取每个单元格中的文本。 问题是,这需要14秒钟才能执行。

public class ExcelFile { public string Path = @"C:\test.xlsx"; private Excel.Application xl = new Excel.Application(); private Excel.Workbook WB; public string FullText; private Excel.Range rng; private Dictionary<string, string> Variables; public ExcelFile() { WB = xl.Workbooks.Open(Path); xl.Visible = true; foreach (Excel.Worksheet CurrentWS in WB.Worksheets) { rng = CurrentWS.UsedRange; for (int i = 1; i < rng.Count; i++) { FullText += rng.Cells[i].Value; } } WB.Close(false); xl.Quit(); } } 

而在VBA中,我会做这样的事情,大约需要1秒钟的时间:

 Sub run() Dim strText As String For Each ws In ActiveWorkbook.Sheets For Each c In ws.UsedRange strText = strText & c.Text Next c Next ws End Sub 

或者更快(less于1秒):

 Sub RunFast() Dim strText As String Dim varCells As Variant For Each ws In ActiveWorkbook.Sheets varCells = ws.UsedRange For i = 1 To UBound(varCells, 1) For j = 1 To UBound(varCells, 2) strText = strText & CStr(varCells(i, j)) Next j Next i Next ws End Sub 

也许有什么事情发生在我不知道的C#for循环? 是否有可能加载到一个数组types的对象(如我的最后一个例子)允许迭代只是值,而不是单元格对象?

Excel和C#完全在不同的环境中运行。 C#在使用托pipe内存的.NET框架中运行,而Excel是本机C ++应用程序,并在非托pipe内存中运行。 在这两者之间翻译数据(称为“封送”的过程)在性能方面是非常昂贵的。

调整你的代码是不会帮助的。 与编组过程相比,对于循环,弦构造等等,都是非常快速的。 要获得更好的性能,唯一的方法是减less必须穿越进程间边界的行程数 。 逐个提取数据单元不会使您获得所需的性能。

这里有几个选项:

  1. 在VBA中编写一个子或函数,完成你想要的任何事情,然后通过互操作调用这个子或函数。 演练 。

  2. 使用interop将工作表保存为CSV格式的临时文件,然后使用C#打开文件。 您将需要循环并parsing文件以将其转换为有用的数据结构,但此循环将更快。

  3. 使用互操作将一系列单元格保存到剪贴板,然后使用C#直接读取剪贴板。

我使用这个function。 循环仅用于从索引0开始转换为数组,主要工作在object[,] tmp = range.Value

 public object[,] GetTable(int row, int col, int width, int height) { object[,] arr = new object[height, width]; Range c1 = (Range)Worksheet.Cells[row + 1, col + 1]; Range c2 = (Range)Worksheet.Cells[row + height, col + width]; Range range = Worksheet.get_Range(c1, c2); object[,] tmp = range.Value; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { arr[i, j] = tmp[i + tmp.GetLowerBound(0), j + tmp.GetLowerBound(1)]; } } return arr; } 

有一件事情可以加速,就是在前一个string中使用StringBuilder而不是+= 。 string在C#中是不可变的,因此在创build最终string的过程中,您将创build大量额外的string。

此外,您可以提高循环行,列位置的性能,而不是循环遍历索引。

以下是使用StringBuilder和行列位置循环更改的代码:

 public class ExcelFile { public string Path = @"C:\test.xlsx"; private Excel.Application xl = new Excel.Application(); private Excel.Workbook WB; public string FullText; private Excel.Range rng; private Dictionary<string, string> Variables; public ExcelFile() { StringBuilder sb = new StringBuilder(); WB = xl.Workbooks.Open(Path); xl.Visible = true; foreach (Excel.Worksheet CurrentWS in WB.Worksheets) { rng = CurrentWS.UsedRange; for (int i = 1; i <= rng.Rows.Count; i++) { for (int j = 1; j <= rng.Columns.Count; j++) { sb.append(rng.Cells[i, j].Value); } } } FullText = sb.ToString(); WB.Close(false); xl.Quit(); } } 

我同情你pwwolff。 循环使用Excel单元格可能很昂贵。 安东尼奥和马克斯都是正确的,但吴宗宪的回答很好。 使用string生成器可能会加快速度,并从使用的范围恕我直言,使一个对象数组是一样快,你会得到使用互操作。 我知道还有其他第三方库可能performance更好。 如果使用互操作的文件很大,循环遍历每个单元将花费不可接受的时间。

在下面的testing中,我使用了一张工作簿,其中工作表中有11列和100行使用的范围数据。 使用对象数组实现,这花了一点点时间。 735行花了大约40秒。

我在一个多行文本框的窗体上放3个button。 第一个button使用您的发布代码。 第二个button从循环中取出范围。 第三个button使用对象数组方法。 每个人都有显着的performance改善。 我在表单上使用了一个文本框来输出数据,你可以使用一个string,但是如果你必须有一个大的string,使用string生成器会更好。

同样,如果文件很大,您可能需要考虑另一个实现。 希望这可以帮助。

 private void button1_Click(object sender, EventArgs e) { Stopwatch sw = new Stopwatch(); MessageBox.Show("Start DoExcel..."); sw.Start(); DoExcel(); sw.Stop(); MessageBox.Show("End DoExcel...Took: " + sw.Elapsed.Seconds + " seconds and " + sw.Elapsed.Milliseconds + " Milliseconds"); } private void button2_Click(object sender, EventArgs e) { MessageBox.Show("Start DoExcel2..."); Stopwatch sw = new Stopwatch(); sw.Start(); DoExcel2(); sw.Stop(); MessageBox.Show("End DoExcel2...Took: " + sw.Elapsed.Seconds + " seconds and " + sw.Elapsed.Milliseconds + " Milliseconds"); } private void button3_Click(object sender, EventArgs e) { MessageBox.Show("Start DoExcel3..."); Stopwatch sw = new Stopwatch(); sw.Start(); DoExcel3(); sw.Stop(); MessageBox.Show("End DoExcel3...Took: " + sw.Elapsed.Seconds + " seconds and " + sw.Elapsed.Milliseconds + " Milliseconds"); } // object[,] array implementation private void DoExcel3() { textBox1.Text = ""; string Path = @"D:\Test\Book1 - Copy.xls"; Excel.Application xl = new Excel.Application(); Excel.Workbook WB; Excel.Range rng; WB = xl.Workbooks.Open(Path); xl.Visible = true; int totalRows = 0; int totalCols = 0; foreach (Excel.Worksheet CurrentWS in WB.Worksheets) { rng = CurrentWS.UsedRange; totalCols = rng.Columns.Count; totalRows = rng.Rows.Count; object[,] objectArray = (object[,])rng.Cells.Value; for (int row = 1; row < totalRows; row++) { for (int col = 1; col < totalCols; col++) { if (objectArray[row, col] != null) textBox1.Text += objectArray[row,col].ToString(); } textBox1.Text += Environment.NewLine; } } WB.Close(false); xl.Quit(); Marshal.ReleaseComObject(WB); Marshal.ReleaseComObject(xl); } // Range taken out of loops private void DoExcel2() { textBox1.Text = ""; string Path = @"D:\Test\Book1 - Copy.xls"; Excel.Application xl = new Excel.Application(); Excel.Workbook WB; Excel.Range rng; WB = xl.Workbooks.Open(Path); xl.Visible = true; int totalRows = 0; int totalCols = 0; foreach (Excel.Worksheet CurrentWS in WB.Worksheets) { rng = CurrentWS.UsedRange; totalCols = rng.Columns.Count; totalRows = rng.Rows.Count; for (int row = 1; row < totalRows; row++) { for (int col = 1; col < totalCols; col++) { textBox1.Text += rng.Rows[row].Cells[col].Value; } textBox1.Text += Environment.NewLine; } } WB.Close(false); xl.Quit(); Marshal.ReleaseComObject(WB); Marshal.ReleaseComObject(xl); } // original posted code private void DoExcel() { textBox1.Text = ""; string Path = @"D:\Test\Book1 - Copy.xls"; Excel.Application xl = new Excel.Application(); Excel.Workbook WB; Excel.Range rng; WB = xl.Workbooks.Open(Path); xl.Visible = true; foreach (Excel.Worksheet CurrentWS in WB.Worksheets) { rng = CurrentWS.UsedRange; for (int i = 1; i < rng.Count; i++) { textBox1.Text += rng.Cells[i].Value; } } WB.Close(false); xl.Quit(); Marshal.ReleaseComObject(WB); Marshal.ReleaseComObject(xl); }