Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1165278
  • 博文数量: 146
  • 博客积分: 6619
  • 博客等级: 准将
  • 技术积分: 1621
  • 用 户 组: 普通用户
  • 注册时间: 2008-02-29 14:06
文章分类

全部博文(146)

文章存档

2020年(1)

2019年(4)

2018年(3)

2017年(5)

2015年(5)

2014年(7)

2013年(5)

2012年(11)

2011年(15)

2010年(13)

2009年(14)

2008年(63)

分类: C#/.net

2015-09-28 22:22:15

从上一篇《PYTHON操作EXCEL》可以看到,Python 操作 Excel 已非常自如方便。但是 Python 和相关库毕竟是一个额外的依赖,若能从 Excel 自身解决此类问题,自然是更为易用。 

1. VBA 中的哈希表

用 Python 的着眼点主要是 VLOOKUP 公式太慢了,所以关键是要找到一种更高效的算法或数据结构定位数据。VLOOKUP 要求对列进行排序,内部应该是对列内数据进行二分查找,算法上不好再优化了,那就只好更换一种数据结构。搜索了一下,VBA 提供了 Scripting.Dictionary 这一词典结构,而且有文章说内部是哈希表实现,那就正是我要的东西了。 
这样,VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) 这一公式就转为下面的词典查找方式来实现: 
  • 使用要从中进行查找的 table_array 内容构建词典。用 table_array 第一列作为 key,table_array 第 col_index_num 列作为 value,插入 Dictionary 中:Dictionary.Add key, value;
  • 查找时只需直接取 Dictionary 内的值 Dictionary.Item(lookup_value),即可完成查找;
若是仅仅 VLOOKUP 一次,倒也不必费劲先建立起一个词典。但当使用同样 VLOOKUP 公式的单元格很多时(比如几万个),就显得其必要了。因为 Dictionary 只需要建立一次,就可以用 O(1) 的复杂度进行多次查找了。 

2. VLOOKUP 慢,主要问题不在算法上

从算法角度,词典查找的确快于二分查找,但优势并不是那么明显。所以在具体执行时,我发现使用词典查找的 VBA 宏运行速度并不比 VLOOKUP 快多少,运行时 Excel 仍然会导致系统假死几个小时。按说如此简单的程序不应该那么慢,问题究竟在哪里呢? 
经过一段摸索,我才发现问题的根源所在: 
  • VBA 往 Excel 表格中填内容时,会引发表格中已有公式的自动计算,非常耗时;
  • Excel 表格内容更新时,会触发屏幕显示内容的自动刷新,代价也很高;
所以提高 VBA 脚本执行性能的关键点,在于计算时关掉公式自动计算和屏幕刷新,这也是我始料未及的。在 VBA 中实现这两点很容易,但由于 VLOOKUP 本身即是公式,我没能想通直接调用 VLOOKUP 时如何避免这两点带来的性能损失。 

3. 示例 VBA 代码

在做了上面提到的两次优化之后,原来 VLOOKUP N 个小时才能完成的任务,只用了 7 秒钟就执行结束了。 

下面是我写的一段示例代码。我不熟悉 VBA 语言,只是照葫芦画瓢。代码规范程度相差甚远,但题意应是体现其中了。有心的朋友可以用作参考。

点击(此处)折叠或打开

  1. Sub 在机器表上生成一级分中心()
  2. '
  3. ' 在机器表上生成一级分中心 Macro
  4. '
  5. Application.Calculation = xlCalculationManual
  6. Application.ScreenUpdating = False
  7.   
  8. t0 = Timer
  9. ' 词典
  10. Set map_dict = CreateObject("Scripting.Dictionary")
  11.   
  12. ' 打开分中心映射表
  13. Set map_sheet = Worksheets("分中心映射表")
  14. ' map_nrows = map_sheet.Range("A:A").End(xlUp).Row
  15. ' Set my_rows = map_sheet.Range("A2" & map_nrows).Rows
  16. Set my_rows = map_sheet.UsedRange.Rows
  17.   
  18. ' 遍历分中心映射表,获得 分中心 对应的一级分中心,插入词典
  19. For Each my_row In my_rows
  20.     center = my_row.Cells(1, 1).Value
  21.     city = my_row.Cells(1, 2).Value
  22.     If Not map_dict.Exists(center) Then
  23.         map_dict.Add center, city
  24.     End If
  25. Next my_row
  26.   
  27. ' 打开机器表
  28. Set dispatch_sheet = Worksheets("机器表")
  29. dispatch_nrows = dispatch_sheet.Range("a99999").End(xlUp).Row
  30. Set my_rows = dispatch_sheet.Range("a1:b" & dispatch_nrows).Rows
  31.   
  32. ' 遍历开通表,通过词典获得 machine_id 对应的一级分中心,插入开通表
  33. For Each o_row In my_rows
  34.    center = o_row.Cells(1, 2).Value
  35.    o_row.Cells(1, 2).Value = map_dict.Item(center)
  36. Next o_row
  37.   
  38. MsgBox "在机器表上生成一级分中心。共处理 " & dispatch_nrows & " 条记录,总耗时" & Timer - t0 & "秒。"
  39.   
  40. ' 销毁建立的词典
  41. Set map_dict = Nothing
  42.   
  43. ' 打开自动计算和屏幕刷新
  44. Application.Calculation = xlCalculationAutomatic
  45. Application.ScreenUpdating = True
  46. '
  47. End Sub

最后补充一点:我先实现的词典查找,后发现性能问题根源,所以未能去比较 VLOOKUP 与词典查找两种方式的具体性能差异。我想如果差异可以忍受,那么直接在 VBA 中调用 VLOOKUP 公式或许是一种更为简单的实现。

4. 测试用例

上面的代码如何测试呢?

点击(此处)折叠或打开

  1. id name:分中心映射表,字典表 nickName id:机器表,待匹配表 匹配后结果:
  2. 1 a1 b1 1 b1 a1
  3. 2 a2 b2 2 b2 a2
  4. 3 a3 b3 3 b3 a3
  5. 4 a4 b4 4 b4 a4
  6. 5 a5 b5 5 b5 a5
  7. 6 a6 b6 6 b6 a6
  8. 7 a7 b7 7 b7 a7
  9. 8 a8 b8 8 b8 a8
  10. 9 a9 b9 9 b9 a9
  11. 10 a10 b10 10 b10 a10

?

5. 一些技巧

5.1 获取EXCEL表格中的行数(有效行)和列数

点击(此处)折叠或打开
  1. 方法1:
  2.   ActiveSheet.UsedRange.Rows.Count
  3.   ActiveSheet.UsedRange.Columns.Count
  4.        缺点:有时可能会比实际数大一些,原因是如果你把最后几行(列)数据清除后(非整行或整列删除),用这个命令仍返回未清除前的值。就是说现在虽然是空的,但是你曾经用过也算你的。
  5.  
  6. 方法2:
  7.   ActiveSheet.Range("A65535").End(xlUp).Row
  8.   ActiveSheet.Range("IV1").End(xlToLeft).Column
  9.   可以简写为:
  10.   ActiveSheet.[A65536].End(xlUp).Row
  11.   ActiveSheet.[IV1].End(xlToLeft).Column
  12.       缺点:只能计算出一列(行)的最后一个单元格所在的行(列)数。本例是只返回A列最后一个单元格所占的行数。
  13.  
  14. 方法3:
  15.   ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row
  16.   ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Column
  17.   缺点:在工作表进行对删除或清除操作时也会变得比实际情况大。
  18.  
  19. 方法4:
  20.   ActiveSheet.UsedRange.SpecialCells(xlCellTypeLastCell).Row
  21.   ActiveSheet.UsedRange.SpecialCells(xlCellTypeLastCell).Column
  22.   缺点:在工作表进行对删除或清除操作时也会变得比实际情况大。
  23.  
  24. 方法5:
  25.   Application.CountA(ActiveSheet.Range("A:A"))
  26.   Application.CountA(ActiveSheet.Range("1:1"))
  27.   只能统计一列(行)的实际使用情况,得到的不一定是最后一行(列)的位置。方法2的数值比此方法大时,说明在A列的数据间有空白未填写的单元格。
  28.  
  29. 方法6:
  30.   ActiveSheet.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
  31.   ActiveSheet.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
  32.   效果同方法2
  33.  
  34. 方法7:
  35.     GetSheet(ws).UsedRange.CurrentRegion.Rows.Count
  36.     经过加工修改已经可以读出来的是有效数据行

5.2 获得大型文本文件的行数

点击(此处)折叠或打开

  1. Sub RowCnt()
  2.     Dim Arr, k&
  3.  
  4.     Open "C:\qsc.txt" For Input As #1
  5.     Arr = Split(StrConv(InputB(LOF(1), 1), vbUnicode), vbCrLf)
  6.     k = UBound(Arr) + 1
  7.     Reset
  8.     MsgBox "此文本总行数为" & k
  9. End Sub

6. Refer

[1] Excel 2013 VBA DOC

(v=office.15).aspx

[2] Worksheet对象应用大全(3)-UsedRange属性详解

[3] 如何获得大型文本文件的行数


阅读(5901) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~