Excel занимает очень много времени для вычисления UDF VBA

#excel #vba #user-defined-functions

#excel #vba #определяемые пользователем функции

Вопрос:

пример1 Имя файла, которое я пытаюсь сопоставить, находится в строке A, и я просматриваю строку I, чтобы увидеть, есть ли совпадение пример2 Я нашел этот код, не могу вспомнить где, но я пытаюсь сопоставить строку номеров деталей с строкой имен файлов его изображений. Этот код работает, однако при его запуске возникает проблема: вычисление даже 1 столбца занимает очень много времени, и когда я делаю сотни за раз, мой Excel просто перестает отвечать, и у меня есть тысячи продуктов, которые мне нужно сопоставить. Я действительно новичок в VBA, поэтому я даже не могу понять проблему.

Пожалуйста, помогите, спасибо.

 'Name function and arguments
Function SearchChars(lookup_value As String, tbl_array As Range) As String
'Declare variables and types
Dim i As Integer, str As String, Value As String
Dim a As Integer, b As Integer, cell As Variant
'Iterste through each cell
For Each cell In tbl_array
  'Save cell value to variable
  str = cell
  'Iterate through characters
  For i = 1 To Len(lookup_value)
    'Same character?
    If InStr(cell, Mid(lookup_value, i, 1)) > 0 Then
      'Add 1 to number in array
      a = a   1
      'Remove evaluated character from cell and contine with remaning characters
      cell = Mid(cell, 1, InStr(cell, Mid(lookup_value, i, 1)) - 1) amp; Mid(cell, InStr(cell, Mid(lookup_value, i, 1))   1, 9999)
    End If
  'Next character
  Next i
 
a = a - Len(cell)
'Save value if there are more matching characters than before  
If a > b Then
  b = a
  Value = str
End If
 
a = 0
Next cell
'Return value with the most matching characters
SearchChars = Value
End Function
 

Комментарии:

1. For Each cell In tbl_array — это повторение по ячейкам, что происходит очень медленно. Вместо этого вы хотите прочитать tbl_array.Value в Variant массив, а затем выполнить цикл по массиву. Другими словами, вы думаете, что у вас есть массив, но на самом деле это не так.

2. Привет, спасибо за ответ. Так мне изменить для каждой ячейки в tbl_array на для каждой ячейки в tbl_array.Value? Извините, я действительно не знаю, что я делаю.

3. Возможно, сначала прочитайте массивы и диапазоны .

4. Попробуйте переключить Excel с автоматического вычисления на ручное: Файл-> Параметры-> Формулы. Возможно, что Excel пересчитывает всю книгу после обновления каждой ячейки. Если вы установите для него значение Manual, вы можете просто нажать F9 для ручного пересчета после завершения работы скрипта. Это может не исправить это полностью, но это может иметь большое значение

5. @ANeonTetra спасибо за вашу помощь!

Ответ №1:

РЕДАКТИРОВАТЬ (после просмотра данных): следующее должно быть заметно быстрее (а также заметно проще)

 'Name function and arguments
Function SearchChars(lookup_value As String, tbl_array As Range) As String
    
    'Declare variables and types
    Dim inLenMatched%, vnVal, varLookupValues()
    
    'Puts lookup cell values into a array (to speed things up)
    varLookupValues = tbl_array.Value
    
    'Iterate through each lookup value
    For Each vnVal In varLookupValues
      
        'Ignore empty cells
        If vnVal <> "" Then
            
            'Does part number appear in filename?
            If InStr(lookup_value, vnVal) > 0 Then
                
                'Is this match the most complete match so far?
                If Len(vnVal) > inLenMatched Then
                    inLenMatched = Len(vnVal)
                    SearchChars = vnVal
                End If
             End If
        End If
        
    Next vnVal
    
    'Return match value (or 'No Match' if not matched)
    If SearchChars = "" Then SearchChars = "No Match"

End Function
 

Вышеизложенное — всего лишь один нестандартный подход.
Есть другие (и, возможно, более быстрые) способы приблизиться к этому.

Наиболее очевидным шагом (независимо от метода) для повышения производительности было бы ограничение tbl_array только строками с данными (а не всем столбцом).

Отдельно: не зная всех возможных случаев, невозможно сказать наверняка. Но, по всей вероятности, это можно сделать с помощью собственных функций Excel, и (если это так) это обеспечит наилучшую производительность.

Комментарии:

1. Спасибо @spinner, я попробую ваш код. Я опубликовал скриншот образца моих данных в своем сообщении.

2. Это работает действительно отлично, однако оно по-прежнему частично совпадает со многими, вместо того, чтобы просто заполнять столбец «Нет совпадения».

3. 1) Рад помочь. 2) Не уверен, что понимаю. Можете ли вы привести пример того, что вы имеете в виду? 3) Отдельно: все ли имена файлов в форме ??_PartNumber_??.jpg ?

4. Я поместил скриншот в свой пост с именем «example2».

5. итак, в «all_filenames» указаны его .jpg, и один номер детали может содержать несколько изображений. но в некоторых номерах деталей может даже отсутствовать файл .jpg. Я уже пробовал формулу VLOOKUP, но проблема в том, что один номер детали может содержать 2 или 3 изображения и иметь дополнительную букву или слова в конце, он не будет соответствовать ему.

Ответ №2:

Как уже было сказано, минимизация взаимодействия с листом путем присвоения диапазона массиву структурно ускорит ваши макросы. Не проверено, но эти незначительные изменения в вашем коде должны помочь вам на правильном пути:

     Option Explicit
    'Name function and arguments
    Function SearchChars2(lookup_value As String, tbl_array As Range) As String
    'Declare variables and types
    Dim i As Integer, str As String, Value As String
    Dim a As Integer, b As Integer, cell As Variant
    'Iterste through each cell => replace with array
    'adapt to correct sheet
    Dim arr
    arr = tbl_array
    
    For Each cell In arr 'tbl_array
        'Save cell value to variable
        str = cell
        'Iterate through characters
        For i = 1 To Len(lookup_value)
          'Same character?
          If InStr(cell, Mid(lookup_value, i, 1)) > 0 Then
            'Add 1 to number in array
            a = a   1
            'Remove evaluated character from cell and contine with remaning characters
            cell = Mid(cell, 1, InStr(cell, Mid(lookup_value, i, 1)) - 1) amp; Mid(cell, InStr(cell, Mid(lookup_value, i, 1))   1, 9999)
          End If
        'Next character
        Next i
     
        a = a - Len(cell)
        'Save value if there are more matching characters than before
        If a > b Then
          b = a
          Value = str
        End If
         
        a = 0
        Next cell
    'Return value with the most matching characters
    SearchChars2 = Value
    End Function
 

Комментарии:

1. Привет @ceci, я скопировал и вставил ваш код в свой VBA. В настоящее время я пытаюсь это сделать, и он обрабатывается уже довольно давно.

2. Мне все еще не нравится Instr(cell,... -> cell = цикл. Похоже, это один из наименее эффективных способов подсчета совпадающих символов.

3. Привет @ceci, это немного быстрее, если я делаю это с шагом.

4. хорошо, просто изменил часть массива, не могли бы вы привести пример некоторых исходных данных?

5. Новое и здесь, я должен просто поместить сюда один из файлов Excel?

Ответ №3:

Я пытался изменить ваш существующий код, но мне было проще просто переписать его, используя то, что я считаю лучшей структурой. И после запуска кода по 26 столбцам и 432 строкам потребовалось всего 0,2 секунды, чтобы найти ближайшую соответствующую строку.

Я переместил каждое значение в массив. Я преобразовал lookup_value и «значения ячеек» в массив байтов. Я сравнил массивы байтов для подсчета совпадающих «символов». И затем я возвращаю строку, в которой было наибольшее количество совпадающих «символов».

 Sub Example()
    Dim StartTime As Double
    StartTime = Timer * 1000
    Debug.Print SearchChars3("Toddleson", Range("A1:Z432"))
    Debug.Print "Time Elapsed: " amp; Timer * 1000 - StartTime amp; " ms"
    'Time Elapsed: 171.875 ms
End Sub

Function SearchChars3(lookup_value As String, tbl_array As Range) As String
    Dim ClosestMatch As String, HighestMatchCount As Integer
    
    Dim tbl_values() As Variant
    tbl_values = tbl_array.Value
    
    Dim LkUpVal_Bytes() As Byte
    LkUpVal_Bytes = ToBytes(lookup_value)
    
    Dim Val As Variant
    For Each Val In tbl_values
        If Val = "" Then GoTo nextVal
        
        Dim Val_Bytes() As Byte
        Val_Bytes = ToBytes(CStr(Val))
        
        Dim MatchCount As Integer
        MatchCount = CountMatchingElements(LkUpVal_Bytes, Val_Bytes)
        
        If MatchCount > HighestMatchCount Then
            HighestMatchCount = MatchCount
            ClosestMatch = Val
        End If
nextVal:
    Next
    SearchChars3 = ClosestMatch
End Function

Function ToBytes(InputStr As String) As Byte()
    Dim ByteArr() As Byte
    ReDim ByteArr(Len(InputStr) - 1)
    Dim i As Long
    For i = 0 To Len(InputStr) - 1
        ByteArr(i) = AscW(Mid(InputStr, i   1, 1))
    Next
    ToBytes = ByteArr
End Function

Function CountMatchingElements(Arr1 As Variant, Arr2 As Variant) As Integer
    'As elements from Arr1 are found in Arr2, those elements are removed from Arr2, to prevent re-matching with the same elements
    'To enable this feature, Arr2 is turned into a Collection
    Dim Col2 As New Collection
    Dim v As Variant
    For Each v In Arr2
        Col2.Add v
    Next
    
    Dim MatchCount As Integer, i As Long
    For Each v In Arr1
        For i = 1 To Col2.Count
            If Col2.Item(i) = v Then
                MatchCount = MatchCount   1
                Col2.Remove (i)
                Exit For
            End If
        Next
    Next
    CountMatchingElements = MatchCount
End Function
 

Дальнейшая оптимизация может заключаться в том, чтобы иметь вторую версию ToBytes функции, которая напрямую выводит значения в Collection . Затем вы можете изменить CountMatchingElements , чтобы принять коллекцию, и ей не нужно будет преобразовывать второй массив в коллекцию.

Я оставлю это как идею для вас, чтобы поэкспериментировать.

Комментарии:

1. Большое вам спасибо @Toddleson, я буду пробовать ваш код!