为了完整起见,我将在这里给出一个大锤到破解坚果的答案,因为问题'这两个范围是否相同?正在成为其他人“比较我的范围,然后做这个复杂的事情......”问题的未经审查的组成部分。
你的问题是一个关于小范围的简单问题。我的答案是大的;但这个问题是一个很好的问题,也是一个更一般性答案的好地方,因为它简单明了:“这些范围有什么不同吗?” 和“有人篡改了我的数据吗?” 与大多数商业 Excel 用户相关。
典型的“比较我的行”问题的大多数答案都是 VBA 中的逐个单元格读取和比较。这些答案的简单性值得称赞,但这种方法在大型数据集上执行非常缓慢,因为:
- 一次读取一个单元格的范围非常慢;
- 逐对比较值效率低下,特别是对于字符串,当值的数量达到数万时,
Point(1) 是重要的一点:VBA 拾取单个单元格所花费的时间与
var = Range("A1")
使用
var = Range("A1:Z1024")
...
...并且与工作表的每次交互所花费的时间是 VBA 中字符串比较的四倍,是浮点小数比较的二十倍;反过来,这比整数比较长三倍。
Range.Value2
因此,如果您一次读取整个范围,并在 VBA 中处理数组,您的代码可能会快四倍,甚至可能快一百倍。
那是在 Office 2010 和 2013 中(我测试过它们);对于旧版本的 Excel,对于与单元格或单元格区域的每个 VBA 交互,您将看到 1/50 秒到 1/500 秒之间的引用时间。这会慢很多,因为在新旧版本的 Excel 中,VBA 操作仍将是个位数的微秒:如果您的代码运行速度至少快一百倍,甚至可能快数千倍,如果您可以避免在旧版本的 Excel 中逐个单元格地读取工作表。
arr1 = Range1.Values
arr2 = Range2.Values
' Consider checking that the two ranges are the same size
' And definitely check that they aren't single-cell ranges,
' which return a scalar variable, not an array, from .Value2
' WARNING: THIS CODE WILL FAIL IF YOUR RANGE CONTAINS AN ERROR VALUE
For i = LBound(arr1, 1) To Ubound(arr1, 2)
For j = LBound(arr1, 2) To Ubound(arr1, 2)
If arr1(i, j) <> arr2(i, j) Then
bMatchFail = True
Exit For
End If
Next j
If bMatchFail Then Exit For
Next i
Erase arr1
Erase arr2
您会注意到此代码示例是通用的,适用于从任何地方获取的两个相同大小的范围 - 甚至来自不同的工作簿。如果您要比较两个相邻的列,则加载一个包含两列的数组并进行比较IF arrX(i, 1) <> arrX(i,2) Then
将使运行时间减半。
仅当您从大范围中获取数万个值时,您的下一个挑战才有意义:对于任何小于此的扩展答案,都没有性能提升。
我们正在做的是:
使用哈希函数比较两个大范围的值
这个想法非常简单,尽管基础数学对于非数学家来说非常具有挑战性:我们不是一次比较一个值,而是运行一个数学函数,将这些值“散列”成一个简短的标识符,以便于比较。
如果您反复将范围与“参考”副本进行比较,则可以存储“参考”哈希,这样可以将工作量减半。
那里有一些快速可靠的散列函数,它们在 Windows 中作为安全和加密 API 的一部分提供。有一个小问题是它们在字符串上运行,我们有一个数组要处理;但是您可以轻松找到一个快速的“Join2D”函数,该函数从范围.Value2
属性返回的二维数组中获取字符串。
因此,两个大范围的快速比较函数将如下所示:
Public Function RangeCompare(Range1 as Excel.Range, Range2 As Excel.Range) AS Boolean
' Returns TRUE if the ranges are identical.
' This function is case-sensitive.
' For ranges with fewer than ~1000 cells, cell-by-cell comparison is faster
' WARNING: This function will fail if your range contains error values.
RangeCompare = False
If Range1.Cells.Count <> Range2.Cells.Count Then
RangeCompare = False
ElseIf Range1.Cells.Count = 1 then
RangeCompare = Range1.Value2 = Range2.Value2
Else
RangeCompare = MD5(Join2D(Range1.Value2)) = MD5(Join2D(Range2.Value2))
Endif
End Function
我在这个 VBA 函数中包装了 Windows System.Security MD5 哈希:
Public Function MD5(arrBytes() As Byte) As String
' Return an MD5 hash for any string
' Author: Nigel Heffernan Excellerando.Blogspot.com
' Note the type pun: you can pass in a string, there's no type conversion or cast
' because a string is stored as a Byte array and VBA recognises this.
oMD5 As Object 'Set a reference to mscorlib 4.0 to use early binding
Dim HashBytes() As Byte
Dim i As Integer
Set oMD5 = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
HashBytes = oMD5.ComputeHash_2((arrBytes))
For i = LBound(HashBytes) To UBound(HashBytes)
MD5 = MD5 & Right("00" & Hex(HashBytes(i)), 2)
Next i
Set oMD5 = Nothing ' if you're doing this repeatedly, declare at module level and persist
Erase HashBytes
End Function
还有其他 VBA 实现,但似乎没有人知道 Byte Array / String 类型双关语——它们不
等价,它们是
相同的——所以每个人都编写了不必要的类型转换。
Dick Kusleika 于 2015 年在 Daily Dose of Excel 上发布了一个快速简单的 Join2D 函数:
Public Function Join2D(ByVal vArray As Variant, Optional ByVal sWordDelim As String = " ", Optional ByVal sLineDelim As String = vbNewLine) As String
Dim i As Long, j As Long
Dim aReturn() As String
Dim aLine() As String
ReDim aReturn(LBound(vArray, 1) To UBound(vArray, 1))
ReDim aLine(LBound(vArray, 2) To UBound(vArray, 2))
For i = LBound(vArray, 1) To UBound(vArray, 1)
For j = LBound(vArray, 2) To UBound(vArray, 2)
'Put the current line into a 1d array
aLine(j) = vArray(i, j)
Next j
'Join the current line into a 1d array
aReturn(i) = Join(aLine, sWordDelim)
Next i
Join2D = Join(aReturn, sLineDelim)
End Function
如果您需要在进行比较之前删除空白行,您将需要我在 2012 年在 StackOverflow 中发布的 Join2D 函数。
这种类型的哈希比较最常见的应用是电子表格控制 -更改监控- 你会看到Range1.Formula
used 而不是Range1.Value2
: 但你的问题是关于比较值,而不是公式。
脚注:我在其他地方发布了一个非常相似的答案。如果我早些时候看到这个问题,我会先在这里发布。