Excel Interop - 效率和性能

64

我在思考如何提高Excel自动化的性能,因为如果工作表中有很多操作,它可能会变得非常缓慢...

以下是我发现的一些方法:

  • ExcelApp.ScreenUpdating = false -- 关闭屏幕重绘

  • ExcelApp.Calculation = Excel.XlCalculation.xlCalculationManual -- 关闭计算引擎,这样Excel不会在单元格值更改时自动重新计算(在完成后再打开)

  • 减少对 Worksheet.Cells.Item(row, col)Worksheet.Range 的调用 -- 我需要轮询数百个单元格才能找到我需要的单元格。实现一些单元格位置缓存,将执行时间从约40秒降低到约5秒。

哪些Interop调用会对性能造成严重影响,应该避免?还有什么其他方法可以避免进行不必要的处理?


3
+1 我也遇到了使用Excel Interop时的性能问题,学到了新东西:ExcelApp.Calculation = Excel.XlCalculation.xlCalculationManual. 谢谢!=) - Will Marcouiller
感谢您在问题中分享您目前的发现,非常有用。 - Alex
一些其他可能的选项在这里:http://stackoverflow.com/documentation/excel-vba/1107/vba-best-practices/5925/switch-off-properties-during-macro-execution - Slai
我知道这个方法有点老了,但是这里还有一个选项。将您的数据转换成 CSV 文件,保存为 XLSX 格式,然后对现有工作簿进行所有修改。我曾经在不到 2 秒内处理了 6k 条记录和 131 列数据。 - Train
7个回答

50

在使用C#或VB.Net获取或设置范围时,先确定范围的总大小,然后获取一个大的二维对象数组...

//get values
object[,] objectArray = shtName.get_Range("A1:Z100").Value2;
iFace = Convert.ToInt32(objectArray[1,1]);

//set values
object[,] objectArray = new object[3,1] {{"A"}{"B"}{"C"}};
rngName.Value2 = objectArray;

请注意,重要的是要知道Excel存储的数据类型(文本还是数字),因为当您将类型从对象数组转换回来时,它不会自动这样做。如果不能确定数据类型,请添加测试以验证数据。


使用2D对象数组加1。还有shtName.UsedRange.get_Value(XlRangeValueDataType.XlRangeValueDefault),可以将其强制转换为二维对象数组,并一次检索所有单元格的值。 - Will Marcouiller
2
@Will Marcouiller:是的,但使用UsedRange属性的问题是它会对性能产生负面影响。我不确定它甚至比使用cells/offset方法更快。 - Anonymous Type
太好了,但是如何使用这种优化技术将图像添加到xls中呢?在我的应用程序中,图像插入是一个瓶颈。每个生成的XLSX文件至少有300-400张图片。当前的解决方案调用worksheet.Shapes.AddPicture()方法多次,与图片数量相同。这真的很慢。 - mr_esp
如果您需要其他功能(如格式设置)并且使用COM对象性能差,您可以尝试从此答案中获取EPPlus- https://dev59.com/71jUa4cB1Zd3GeqPVuvY - Jeff Fischer
1
@JeffFischer OP在工作表中有“很多事情要处理”。EPPlus的建议与我们讨论的内容不符。 - Jeremy Thompson

14

这是为那些想知道从数据库结果集中填充Excel表格的最佳方法的人准备的。这并不意味着它是完整的列表,但它确实列出了一些选项。

以下是在旧的Pentium 4 3GHz计算机上尝试使用155个列和4200条记录填充Excel表格时的性能数据,包括数据检索时间,按从慢到快的顺序:

  1. 一次一个单元格 - 将近11分钟

  2. 通过转换为HTML来填充数据集+保存HTML到磁盘+加载HTML到Excel并将工作表保存为XLS/XLSX - 5分钟

  3. 一次一列 - 4分钟

  4. 在SQL 2005中使用已弃用的sp_makewebtask过程创建HTML文件 - 9秒+随后加载HTML文件到Excel并另存为XLS/XLSX - 约2分钟。

  5. 将.NET数据集转换为ADO RecordSet并使用WorkSheet.Range [].CopyFromRecordset函数填充Excel - 45秒!

我最终选择了选项5。希望这可以帮助到你。


嗯,(5)中的混合方法不错。 但是你有没有尝试第六种方法,使用OLEDbConnection连接工作簿并将表格填充为表格?这种方法的限制包括需要预先知道每个列的模式(以防止Excel进行不正确的类型转换)。 - Anonymous Type
1
@AnonymousType - 我必须承认我没有尝试将表格填充为一个表格。我仍然希望微软能够为 .Net 开发人员提供一个“CopyFromDataSet”函数 :-). - Ritesh
那本来会是一个非常棒的VSTO功能。 - Anonymous Type
我知道这已经过时了,但是有一个选项6。将您的数据转换为csv文件,保存为xlsx,然后对现有工作簿进行所有修改。我有6k条记录,131列,在不到2秒的时间内完成。多个工作表意味着每个工作表都是一个csv文件。 - Train

6
如果您需要轮询多个单元格的值,可以一次性将范围内所有单元格的值存储在变量数组中:
Dim CellVals() as Variant
CellVals = Range("A1:B1000").Value

在这里存在一种权衡,即在获取数值的范围大小方面。如果你需要一千个或更多单元格数值,则使用此方法可能比仅循环不同单元格并轮询值更快。


5
-1,没有冒犯Jon的意思,但是这个问题怎么得到了这么多赞?这个问题是关于Excel互操作性而不是VBA/VB6的。我确信Variant甚至在COM互操作性中也不存在(使用对象)。 - Anonymous Type
1
Range.Value 也适用于 COM。因此,它在 VBA/.NET 的示例中的作用有点不相关,不会改变答案。我同意如果 Jon 能更改示例会更好。 - Tomer W
更不用说这个答案在被采纳的答案之前就已经存在了一年,而那个答案只是稍微好一点,因为它有相反的操作。 - Chris

5

尽可能使用Excel内置功能,例如:不要在整个列中搜索给定字符串,而是使用GUI中可用的find命令(通过Ctrl-F):

Set Found = Cells.Find(What:=SearchString, LookIn:=xlValues, _
    SearchOrder:=xlByRows, SearchDirection:=xlNext, _
    MatchCase:=False, SearchFormat:=False)

If Not Found Is Nothing Then
    Found.Activate
    (...)
EndIf

如果你想排序某些列表,请使用Excel的sort命令,不要在VBA中手动操作:

Selection.Sort Key1:=Range("A1"), Order1:=xlAscending, Header:=xlGuess, _
    OrderCustom:=1, MatchCase:=False, Orientation:=xlTopToBottom, _
    DataOption1:=xlSortNormal

1
这是VBA,而不是Interop。 - CompanyDroneFromSector7G
@bukko 这个想法很好,而且点子也非常一致。 就像使用 SQL 数据库时,你应该使用查询来执行任务(或存储过程),而不是执行多个查询。 - Tomer W

4

正如匿名类型所说:读写大范围块对性能非常重要。

如果COM-Interop开销仍然过大,您可能希望切换到使用XLL接口,这是最快的Excel接口。

虽然XLL接口主要面向C++用户,但XL DNA和Addin Express都提供了.NET到XLL桥接功能,比COM-Interop快得多。


2

性能也很大程度上取决于您如何自动化Excel。 VBA比COM自动化更快,而COM自动化比.NET自动化更快。通常情况下,早期(编译时)绑定也比晚期绑定更快。

如果您遇到严重的性能问题,可以考虑将代码的关键部分移动到VBA模块中,并从COM/.NET自动化代码中调用该代码。

如果您使用.NET,还应使用Microsoft提供的优化的主要互操作程序集,而不是使用自定义构建的互操作程序集。


4
完全正确。但是如果你遵循不频繁创建小的Range对象来设置Value2属性的建议,而只需传递一个对象数组或获取一个对象数组,那么你实际上就不需要使用VBA了。 - Anonymous Type

0

在VBA中,你可以做的另一件大事是使用Option Explicit并尽可能避免使用变量。虽然在VBA中无法完全避免使用变量,但它们会使解释器在运行时执行更多的工作并浪费内存。

当我开始学习Excel中的VBA时,我发现这篇文章非常有帮助。
http://www.ozgrid.com/VBA/SpeedingUpVBACode.htm

还有这本书。

http://www.amazon.com/VB-VBA-Nutshell-Language-OReilly/dp/1565923588

类似于

 app.ScreenUpdates = false //and
 app.Calculation = xlCalculationManual

你也可以设置

 app.EnableEvents = false //Prevent Excel events
 app.Interactive = false  //Prevent user clicks and keystrokes

虽然它们似乎不像前两个那样有很大的区别。

类似于将范围值设置为数组,如果您正在处理的数据主要是每列都有相同公式的表格,您可以使用R1C1公式符号表示公式,并将整个列设置为公式字符串以在一个调用中设置整个内容。

app.ReferenceStyle = xlR1C1
app.ActiveSheet.Columns(2) = "=SUBSTITUTE(C[-1],"foo","bar")"

此外,使用ExcelDNA和.NET创建XLL插件(或C语言硬编码方式)也是您唯一能够使自定义函数在多个线程上运行的方法。(请参见Excel DNA的ExcelFunction属性的IsThreadSafe属性。)
在完全转换为Excel DNA之前,我还尝试了在.NET中创建COM可见库以在VBA项目中引用。这种方式比VBA要快一些,特别是对于重型文本处理,并且使用包装的.NET List类而不是VBA的Collection也是如此,但Excel DNA更好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接