我正在尝试从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
或者,甚至更快(不到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 :(得分:4)
我使用这个功能。循环仅用于从索引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;
}
答案 1 :(得分:3)
Excel和C#完全在不同的环境中运行。 C#使用托管内存在.NET框架中运行,而Excel是本机C ++应用程序,在非托管内存中运行。在这两者之间转换数据(称为“编组”的过程)在性能方面非常昂贵。
调整代码不会有帮助。对于循环,字符串构造等与编组过程相比都非常快。您将获得明显更好的性能的唯一方法是减少必须跨越进程间边界的行程数。逐个单元地提取数据永远不会为您提供所需的性能。
以下是几个选项:
在VBA中编写一个子或函数来执行您想要的任何操作,然后通过互操作调用该子函数或函数。 Walkthrough。
使用互操作将工作表保存为CSV格式的临时文件,然后使用C#打开该文件。您将需要循环并解析文件以使其进入有用的数据结构,但此循环将更快。
使用互操作将一系列单元格保存到剪贴板,然后使用C#直接读取剪贴板。
答案 2 :(得分:2)
加快速度的一件事是在前一个字符串上使用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();
}
}
答案 3 :(得分:2)
我同情你pwwolff。循环使用Excel单元格可能很昂贵。安东尼奥和马克斯都是正确的,但约翰吴的答案总结得很好。使用字符串构建器可以加快速度,并且从使用的范围IMHO中创建对象数组的速度与使用互操作的速度一样快。据我所知,还有其他第三方库可能表现更好。如果使用互操作文件很大,循环遍历每个单元格将花费不可接受的时间。
在下面的测试中,我使用了一张单页工作簿,其中工作表有11列和100行使用范围数据。使用对象数组实现这需要花费一秒多的时间。有735行需要大约40秒。
我在带有多行文本框的表单上放了3个按钮。第一个按钮使用您发布的代码。第二个按钮从循环中取出范围。第三个按钮使用对象数组方法。每一个都有显着的性能改进。我在表单上使用了一个文本框来输出数据,您可以按原样使用字符串,但如果必须有一个大字符串,则使用字符串构建器会更好。
同样,如果文件很大,您可能需要考虑其他实现。希望这会有所帮助。
QMAKE_CXXFLAGS += -std=c++11