【问题标题】:Efficient way to create an inverted index in VBA在 VBA 中创建倒排索引的有效方法
【发布时间】:2014-03-03 01:21:29
【问题描述】:

我正在创建一个倒排索引来获取一个单词字典,其中包含该单词出现的行号的关联列表(开始行号和出现在该行内给定单元格中的单词列表)。

我已经设法让一些代码为此工作,但我发现处理添加到数组(字典中的值)有点麻烦,我想知道是否有更有效或更优雅的处理方式这个。

我愿意使用数组、集合或任何其他可以轻松搜索的数据类型来将行号列表存储在字典的值中。我已经粘贴了我的代码的缩减版本来演示下面的核心问题,问题实际上只是关于BuildInvertedIndex 过程,但包含其余部分是为了更容易重新创建场景:

Sub Test()
' minimum included here to demonstrate use of buildInvertedIndex procedure

    Dim vRange As Range
    Dim vDict As Dictionary

    Set vRange = ActiveSheet.Range("F2:F20585")
    Set vDict = New Dictionary

    BuildInvertedIndex vDict, vRange

    ' test values returned in dictionary (word: [line 1, ..., line n])
    Dim k As Variant, vCounter As Long
    vCounter = 0
    For Each k In vDict.Keys
        Debug.Print k & ": " & ArrayToString(vDict.Item(k))
        vCounter = vCounter + 1
        If vCounter >= 10 Then
            Exit For
        End If
    Next


End Sub


Sub BuildInvertedIndex(pDict As Dictionary, pRange As Range)

    Dim cell As Range
    Dim words As Variant, word As Variant, val As Variant
    Dim tmpArr() As Long
    Dim newLen As Long, i As Long

    ' loop through cells (one col wide so same as looping through lines)
    For Each cell In pRange.Cells

        ' loop through words in line
        words = Split(cell.Value)
        For Each word In words

            If Not pDict.exists(word) Then
                ' start line array with first row number
                pDict.Add word, Array(cell.Row())
            Else
                i = 0
                If Not InArray(cell.Row(), pDict.Item(word)) Then
                    newLen = UBound(pDict.Item(word)) + 1
                    ReDim tmpArr(newLen)
                    For Each val In tmpArr
                        If i < newLen Then
                            tmpArr(i) = pDict.Item(word)(i)
                        Else
                            tmpArr(i) = cell.Row()
                        End If
                        i = i + 1
                    Next val
                    pDict.Item(word) = tmpArr
                End If
            End If
        Next word
    Next cell

End Sub


Function ArrayToString(vArray As Variant, _
                       Optional vDelim As String = ",") As String
' only included to support test (be able to see what is in the arrays)

    Dim vDelimString As String
    Dim i As Long

    For i = LBound(vArray) To UBound(vArray)
        vDelimString = vDelimString & CStr(vArray(i)) & _
                       IIf(vCounter < UBound(vArray), vDelim, "")
    Next

    ArrayToString = vDelimString
End Function

要运行它,您需要活动工作表(句子)的 F 列中的值,如果您还没有它,您还需要在 VBA 环境中为字典数据类型添加对 Microsoft Scripting Runtime 的引用可用(工具 -> 参考 -> Microsoft Scripting Runtime)。

正如您从代码中看到的那样,这有点混乱,我必须将新的行号插入现有数组(存储为字典中的值)。由于我不知道扩展此数组的方法(不清除现有值),因此我使用变量 tmpArr 创建了一个适当大小的数组,然后从字典中的现有数组中一一复制这些值然后将当前行号添加到末尾。然后使用临时数组替换该键的现有值(当前单词)。

对此的任何建议将不胜感激。

【问题讨论】:

  • 您不能直接使用存储在字典中的数组 - 通常的方法是将其从字典中拉出,修改它,然后将其重新存储在同一个插槽中。例如:stackoverflow.com/questions/16447088/…

标签: arrays vba excel dictionary inverted-index


【解决方案1】:

我愿意使用数组、集合或任何其他数据类型

如我所见,使用集合代替数组会简单得多:

Sub BuildInvertedIndex(pDict As Dictionary, pRange As Range)
    Dim cell As Range
    Dim words, word
    Dim i As Long    
    ' loop through cells (one col wide so same as looping through lines)
    For Each cell In pRange.Cells    
        ' loop through words in line
        words = Split(cell.Value)
        For Each word In words    
            If Not pDict.Exists(word) Then
                ' initialize collection
                pDict.Add word, New Collection
            End If
            'try to add to collection. If row is already in collecton, nothing happend. Storing key makes you sure there're only unique rows
            On Error Resume Next
            pDict.Item(word).Add Item:=cell.Row, Key:=CStr(cell.Row)
            On Error GoTo 0                
        Next word
    Next cell
End Sub

下一步,将ArrayToString稍微修改为ColToString

Function ColToString(vCol As Collection, _
                   Optional vDelim As String = ",") As String
' only included to support test (be able to see what is in the arrays)

    Dim vDelimString As String
    Dim i As Long

    For i = 1 To vCol.Count
        vDelimString = vDelimString & CStr(vCol.Item(i)) & _
                       IIf(i < vCol.Count, vDelim, "")
    Next

    ColToString = vDelimString
End Function

和测试子例程(仅更改一行 - Debug.Print k &amp; ": " &amp; ColToString(vDict.Item(k)) 和目标范围为 "F2:F5"):

Sub Test()
' minimum included here to demonstrate use of buildInvertedIndex procedure

    Dim vRange As Range
    Dim vDict As Dictionary

    Set vRange = ActiveSheet.Range("F2:F5")
    Set vDict = New Dictionary

    BuildInvertedIndex vDict, vRange

    ' test values returned in dictionary (word: [line 1, ..., line n])
    Dim k As Variant, vCounter As Long
    vCounter = 0
    For Each k In vDict.Keys
        Debug.Print k & ": " & ColToString(vDict.Item(k))
        vCounter = vCounter + 1
        If vCounter >= 10 Then
            Exit For
        End If
    Next

    'clean up memory
    Set vDict = Nothing
End Sub

结果:


更新:

为了提高代码的速度,您可以将范围存储在数组中(下一种方法仅适用于 single-column 范围,但您可以轻松修改它):

测试子:

Sub TestWirhArray()
' minimum included here to demonstrate use of buildInvertedIndex procedure

    Dim vRange As Range
    Dim vDict As Dictionary
    Dim myArr As Variant

    Set vDict = New Dictionary
    Set vRange = ActiveSheet.Range("F2:F20585")
    myArr = vRange.Value
    BuildInvertedIndexWithArr vDict, myArr, vRange.Row

    ' test values returned in dictionary (word: [line 1, ..., line n])
    Dim k As Variant, vCounter As Long
    vCounter = 0
    For Each k In vDict.Keys
        Debug.Print k & ": " & ColToString(vDict.Item(k))
        vCounter = vCounter + 1
        If vCounter >= 10 Then
            Exit For
        End If
    Next

    'clean up memory
    Set vDict = Nothing
End Sub

新版BuildInvertedIndexWithArr

Sub BuildInvertedIndexWithArr(pDict As Dictionary, pArr, firstRow As Long)
    Dim cell, words, word
    Dim i As Long, j As Long

    j = firstRow
    ' loop through cells (one col wide so same as looping through lines)
    For Each cell In pArr

        ' loop through words in line
        words = Split(cell)
        For Each word In words

            If Not pDict.exists(word) Then
                ' initialize collection
                pDict.Add word, New Collection
            End If

            On Error Resume Next
            pDict.Item(word).Add Item:=j, Key:=CStr(j)
            On Error GoTo 0

        Next word
        j = j + 1
    Next cell
End Sub

【讨论】:

  • 感谢您的详细回复,现在就试试吧。
  • 太棒了,谢谢。它不仅看起来更干净,而且速度提高了 10 倍以上。在刚刚超过 20k 的行上,时间是:Built index for 20585 rows in 11327.0713492417 ms using arrays as values in dictionary.Built index for 20585 rows in 727.502338194183 ms using collections as values in dictionary.
  • 太酷了:) 还有一个提示如何加速你的代码:而不是使用范围,你可以将它存储在数组中(它应该会给你大约 10-20 倍的速度提升)。我已经用这种方法更新了我的回答。你能不能试试,现在说时间,只是好奇:)。
  • 还有一个小改进:Built index for 20585 rows in 508.731182722124 ms using collections as values in dictionary and array for range.。我还通过将它们全部写入工作表来比较结果,发现对于出现在很多行中的某些单词,结果并不相同。我现在正在调查,看看哪个是正确的。
  • 看起来这是我的原始版本,数组中偶尔会出现重复值,但除此之外它们似乎相同。感谢您的所有帮助。
猜你喜欢
  • 2010-12-06
  • 2017-02-12
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
  • 1970-01-01
  • 2012-03-21
  • 1970-01-01
  • 2014-03-02
相关资源
最近更新 更多