2017-03-04 222 views
7

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

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 

或者,甚至更快(小于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循环中发生,我不知道?是否可以将一个范围加载到数组类型对象中(如我的最后一个示例中),以允许仅迭代值而不是单元对象?

+0

这并不罕见,VBA在进程中运行,但是您的第一个代码段不在运行过程中。跨越流程边界缓慢。使用数组可以减少往返次数或基于OpenXML的进程内解决方案(如EPPlus或ClosedXML)。 –

+0

感谢汉斯 - 那么问题仍然是如何将一个范围加载到数组中以避免往返? – pwwolff

回答

3

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

调整你的代码是不会有帮助的。与封送处理相比,对于循环,字符串构造等,都非常快速。要获得明显更好的性能,唯一的方法是减少必须穿越进程间边界的行程数。逐个提取数据单元格是从来没有去得到你想要的性能。

这里有几个选项:

  1. 写VBA子或函数,你想要做的一切,然后调用通过互操作是分或功能。 Walkthrough

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

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

+0

4.(如其他答案中所暗示的),如果不是,则一次性读取所有值作为[,]需要进行保留格式的更改 5.使用带参数的get_Value()获取可编辑的XML,包括格式 – Jbjstam

1

加速它的一件事是在前一个字符串上使用StringBuilder而不是+=。字符串在C#中是不可变的,因此您在创建最终字符串的过程中会创建大量额外的字符串。

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

下面是一个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(); 
    } 
} 
+0

谢谢你的提示 - 削减2秒(减少到12),所以它似乎是一个因素 - 并且通常是一件好事要记住。 – pwwolff

3

我使用此功能。循环仅用于从索引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; 
} 
2

我同情你pwwolff。循环使用Excel单元格可能很昂贵。安东尼奥和马克斯都是正确的,但John Wu的回答很好。使用字符串生成器可能会加快速度,并从使用的范围制作一个对象数组,恕我直言,就像您将使用interop一样快。我知道还有其他第三方库可能表现更好。如果使用interop的文件很大,则循环遍历每个单元格将花费无法接受的时间量。

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

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

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

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); 
} 
+0

感谢您的相关示例!所以现在我非常清楚使用COM库和编组:)时固有的问题: – pwwolff