Performance 优化子目录中递归文件搜索的速度?

Performance 优化子目录中递归文件搜索的速度?,performance,vba,excel,Performance,Vba,Excel,我正在寻找使用Excel宏递归搜索文件模式子目录的最快执行方法。Excel VBA在这方面似乎相当缓慢 到目前为止我已经尝试过的事情(一些基于其他stackoverflow建议): 独占使用Dir在子目录中递归,并在每个文件夹中搜索filepattern。(最慢) 使用Folder.Files集合遍历FileSystemObject文件夹,根据filepattern检查每个文件。(更好,但仍然缓慢) 遍历FileSystemObject文件夹,然后使用Dir检查每个文件夹中的文件模式(到目前为

我正在寻找使用Excel宏递归搜索文件模式子目录的最快执行方法。Excel VBA在这方面似乎相当缓慢

到目前为止我已经尝试过的事情(一些基于其他stackoverflow建议):

  • 独占使用Dir在子目录中递归,并在每个文件夹中搜索filepattern。(最慢)
  • 使用Folder.Files集合遍历FileSystemObject文件夹,根据filepattern检查每个文件。(更好,但仍然缓慢)
  • 遍历FileSystemObject文件夹,然后使用Dir检查每个文件夹中的文件模式(到目前为止最快,但每个文件仍需要几秒钟的时间,如果可能的话,我希望进行优化)
我查看了My.Computer.FileSystem.GetFiles,它似乎非常完美(允许您指定通配符模式并使用单个命令搜索子文件夹),但从我所知,Excel VBA中似乎不支持它,仅在VB中支持

我目前正在使用下面的FindFile Sub,它具有迄今为止最好的性能。如果有人对如何进一步改进这一点提出建议,我将不胜感激

Option Explicit
Private Declare Function GetTickCount Lib "kernel32" () As Long


Function Recurse(sPath As String, targetName As String) As String

    Dim FSO As New FileSystemObject
    Dim myFolder As Folder
    Dim mySubFolder As Folder
    Dim myFile As File

    On Error Resume Next
    Set myFolder = FSO.GetFolder(sPath)
    If Err.Number <> 0 Then
        MsgBox "Error accessing " & sPath & ". The macro will abort."
        Err.Clear
        Exit Function
    End If
    On Error GoTo 0

    Dim foundFolderPath As String
    Dim foundFileName As String
    foundFolderPath = ""
    foundFileName = ""

    For Each mySubFolder In myFolder.SubFolders

        foundFileName = Dir(mySubFolder.Path & "\" & targetName & "*")
        If foundFileName <> vbNullString Then
            foundFolderPath = mySubFolder.Path & "\" & foundFileName
        End If

        If foundFolderPath <> vbNullString Then
            Recurse = foundFolderPath
            Exit Function
        End If

        foundFolderPath = Recurse(mySubFolder.Path, targetName)

        If foundFolderPath <> vbNullString Then
            Recurse = foundFolderPath
            Exit Function
        End If
    Next

End Function


Sub FindFile()

    Dim start As Long
    start = GetTickCount()

    Dim targetName As String
    Dim targetPath As String
    targetName = Range("A1").Value 'Target file name without extension
    targetPath = "C:\Example\" & Range("B1").Value 'Subfolder name

    Dim target As String
    target = Recurse(targetPath, targetName)

    Dim finish As Long
    finish = GetTickCount()

    MsgBox "found: " & target & vbNewLine & vbNewLine & (finish - start) & " milliseconds"

End Sub
选项显式
私有声明函数GetTickCount Lib“kernel32”()的长度为
函数递归(sPath作为字符串,targetName作为字符串)作为字符串
将FSO设置为新的FileSystemObject
将myFolder设置为文件夹
将mySubFolder设置为文件夹
将myFile设置为文件
出错时继续下一步
设置myFolder=FSO.GetFolder(sPath)
如果错误号为0,则
MsgBox“访问时出错”&sPath&>。宏将中止
呃,明白了
退出功能
如果结束
错误转到0
将foundFolderPath设置为字符串
将文件名设置为字符串
foundFolderPath=“”
foundFileName=“”
对于myFolder.SubFolders中的每个mySubFolder
foundFileName=Dir(mySubFolder.Path&“\”&targetName&“*”)
如果foundFileName vbNullString,则
foundFolderPath=mySubFolder.Path&“\”和foundFileName
如果结束
如果是foundFolderPath vbNullString,则
Recurse=foundFolderPath
退出功能
如果结束
foundFolderPath=Recurse(mySubFolder.Path,targetName)
如果是foundFolderPath vbNullString,则
Recurse=foundFolderPath
退出功能
如果结束
下一个
端函数
子FindFile()
开始的时间越长越好
start=GetTickCount()
将targetName设置为字符串
将targetPath设置为字符串
targetName=Range(“A1”).Value“无扩展名的目标文件名”
targetPath=“C:\Example\”&Range(“B1”).Value”子文件夹名称
将目标变暗为字符串
目标=递归(targetPath,targetName)
黯淡的余味
finish=GetTickCount()
MsgBox“找到:”&target&vbNewLine&vbNewLine&(finish-start)&“毫秒”
端接头
根据已接受的答案更新文件搜索功能 这个版本的FindFile()执行速度大约是我在上面问题中最初粘贴的方法的两倍。正如在下面的文章中所讨论的,这应该适用于32位或64位版本的Excel 2010及更新版本

Option Explicit

Private Declare PtrSafe Function FindClose Lib "kernel32" (ByVal hFindFile As LongPtr) As Long
Private Declare PtrSafe Function FindFirstFileW Lib "kernel32" (ByVal lpFileName As LongPtr, ByVal lpFindFileData As LongPtr) As LongPtr
Private Declare PtrSafe Function FindNextFileW Lib "kernel32" (ByVal hFindFile As LongPtr, ByVal lpFindFileData As LongPtr) As LongPtr

Private Type FILETIME
  dwLowDateTime  As Long
  dwHighDateTime As Long
End Type

Const MAX_PATH  As Long = 260
Const ALTERNATE As Long = 14

' Can be used with either W or A functions
' Pass VarPtr(wfd) to W or simply wfd to A
Private Type WIN32_FIND_DATA
  dwFileAttributes As Long
  ftCreationTime   As FILETIME
  ftLastAccessTime As FILETIME
  ftLastWriteTime  As FILETIME
  nFileSizeHigh    As Long
  nFileSizeLow     As Long
  dwReserved0      As Long
  dwReserved1      As Long
  cFileName        As String * MAX_PATH
  cAlternate       As String * ALTERNATE
End Type

Private Const FILE_ATTRIBUTE_DIRECTORY As Long = 16 '0x10
Private Const INVALID_HANDLE_VALUE As LongPtr = -1

Function Recurse(folderPath As String, fileName As String)
    Dim fileHandle    As LongPtr
    Dim searchPattern As String
    Dim foundPath     As String
    Dim foundItem     As String
    Dim fileData      As WIN32_FIND_DATA

    searchPattern = folderPath & "\*"

    foundPath = vbNullString
    fileHandle = FindFirstFileW(StrPtr(searchPattern), VarPtr(fileData))
    If fileHandle <> INVALID_HANDLE_VALUE Then
        Do
            foundItem = Left$(fileData.cFileName, InStr(fileData.cFileName, vbNullChar) - 1)

            If foundItem = "." Or foundItem = ".." Then 'Skip metadirectories
            'Found Directory
            ElseIf fileData.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY Then
                foundPath = Recurse(folderPath & "\" & foundItem, fileName)
            'Found File
            'ElseIf StrComp(foundItem, fileName, vbTextCompare) = 0 Then 'these seem about equal
            ElseIf InStr(1, foundItem, fileName, vbTextCompare) > 0 Then 'for performance
                foundPath = folderPath & "\" & foundItem
            End If

            If foundPath <> vbNullString Then
                Recurse = foundPath
                Exit Function
            End If

        Loop While FindNextFileW(fileHandle, VarPtr(fileData))
    End If

    'No Match Found
    Recurse = vbNullString
End Function

Sub FindFile()

    Dim targetName As String
    Dim targetPath As String
    targetName = Range("A4").Value
    targetPath = "C:\Example\" & Range("B4").Value

    Dim target As String
    target = Recurse(targetPath, targetName)

    MsgBox "found: " & target

End Sub
选项显式
私有声明PtrSafe函数FindClose Lib“kernel32”(ByVal hFindFile作为LongPtr)为Long
私有将PtrSafe函数FindFirstFileW Lib“kernel32”(ByVal lpFileName作为LongPtr,ByVal lpFindFileData作为LongPtr)声明为LongPtr
私有将PtrSafe函数FindNextFileW Lib“kernel32”(ByVal hFindFile作为LongPtr,ByVal lpFindFileData作为LongPtr)声明为LongPtr
私有类型文件时间
dwLowDateTime尽可能长
dwHighDateTime尽可能长
端型
常量最大路径长度=260
常量替换长度=14
'可与W或A函数一起使用
'将VarPtr(wfd)传递给W或将wfd传递给A
私有类型WIN32\u查找\u数据
dwFileAttributes尽可能长
ftCreationTime作为文件时间
ftLastAccessTime作为文件时间
ftLastWriteTime作为文件时间
nFileSizeHigh尽可能长
只要
DW保留的长度为
D保留的时间尽可能长
cFileName作为字符串*MAX\u路径
cAlternate作为字符串*ALTERNATE
端型
Private Const FILE_ATTRIBUTE_目录长度=16'0x10
Private Const无效的\u句柄\u值为LongPtr=-1
函数递归(folderPath为字符串,文件名为字符串)
将文件句柄设置为LongPtr
将模式设置为字符串
将路径设置为字符串
将foundItem设置为字符串
将文件数据设置为WIN32_FIND_数据
searchPattern=folderPath&“\*”
foundPath=vbNullString
fileHandle=FindFirstFileW(StrPtr(searchPattern),VarPtr(fileData))
如果fileHandle\u HANDLE\u值无效,则
做
foundItem=Left$(fileData.cFileName,InStr(fileData.cFileName,vbNullChar)-1)
如果foundItem=“”或foundItem=“…”则“跳过元目录”
'找到目录
ElseIf fileData.dwFileAttributes和文件属性目录
foundPath=Recurse(folderPath&“\”&foundItem,文件名)
'找到文件
'ElseIf StrComp(foundItem、fileName、vbTextCompare)=0然后'
ElseIf InStr(1,foundItem,fileName,vbTextCompare)>0,然后为性能设置'
foundPath=folderPath&“\”&foundItem
如果结束
如果foundPath vbNullString,则
Recurse=foundPath
退出功能
如果结束
FindNextFileW(fileHandle,VarPtr(fileData))时循环
如果结束
“没有找到匹配项
Recurse=vbNullString
端函数
子FindFile()
将targetName设置为字符串
将targetPath设置为字符串
targetName=范围(“A4”).值
targetPath=“C:\Example\”和范围(“B4”).值
将目标变暗为字符串
目标=递归(targetPath,targetName)
MsgBox“找到:”&目标
端接头

使用FindFirstFile或FindFirstFileEx。内置的本机API将比VBA执行得快得多

a
Option Explicit

Private Declare PtrSafe Function FindClose Lib "kernel32" (ByVal hFindFile As LongPtr) As Long
Private Declare PtrSafe Function FindFirstFileW Lib "kernel32" (ByVal lpFileName As LongPtr, ByVal lpFindFileData As LongPtr) As LongPtr
Private Declare PtrSafe Function FindNextFileW Lib "kernel32" (ByVal hFindFile As LongPtr, ByVal lpFindFileData As LongPtr) As LongPtr

Private Type FILETIME
  dwLowDateTime  As Long
  dwHighDateTime As Long
End Type

Const MAX_PATH  As Long = 260
Const ALTERNATE As Long = 14

' Can be used with either W or A functions
' Pass VarPtr(wfd) to W or simply wfd to A
Private Type WIN32_FIND_DATA
  dwFileAttributes As Long
  ftCreationTime   As FILETIME
  ftLastAccessTime As FILETIME
  ftLastWriteTime  As FILETIME
  nFileSizeHigh    As Long
  nFileSizeLow     As Long
  dwReserved0      As Long
  dwReserved1      As Long
  cFileName        As String * MAX_PATH
  cAlternate       As String * ALTERNATE
End Type

Private Const INVALID_HANDLE_VALUE As LongPtr = -1

Private Sub Form_Load()
  Dim hFile     As LongPtr
  Dim sFileName As String
  Dim wfd       As WIN32_FIND_DATA

  sFileName = "c:\*.*" ' Can be up to 32,767 chars

  hFile = FindFirstFileW(StrPtr(sFileName), VarPtr(wfd))

  If hFile <> INVALID_HANDLE_VALUE Then
    Do While FindNextFileW(hFile, VarPtr(wfd))
      Debug.Print Left$(wfd.cFileName, InStr(wfd.cFileName, vbNullChar) - 1)
    Loop

    FindClose hFile
  End If
End Sub
'for windows API call to FindFirstFileEx
Private Const INVALID_HANDLE_VALUE = -1
Private Const MAX_PATH = 260

Private Type FILETIME
    dwLowDateTime   As Long
    dwHighDateTime  As Long
End Type

Private Type WIN32_FIND_DATA
    dwFileAttributes    As Long
    ftCreationTime      As FILETIME
    ftLastAccessTime    As FILETIME
    ftLastWriteTime     As FILETIME
    nFileSizeHigh       As Long
    nFileSizeLow        As Long
    dwReserved0         As Long
    dwReserved1         As Long
    cFileName           As String * MAX_PATH
    cAlternate          As String * 14
End Type

Private Const FIND_FIRST_EX_CASE_SENSITIVE  As Long = 1
'MSDN: "This value is not supported until Windows Server 2008 R2 and Windows 7."
Private Const FIND_FIRST_EX_LARGE_FETCH     As Long = 2

Private Enum FINDEX_SEARCH_OPS
    FindExSearchNameMatch
    FindExSearchLimitToDirectories
    FindExSearchLimitToDevices
End Enum

Private Enum FINDEX_INFO_LEVELS
    FindExInfoStandard
    FindExInfoBasic 'MSDN: "This value is not supported until Windows Server 2008 R2 and Windows 7."
    FindExInfoMaxInfoLevel
End Enum

Private Declare Function FindFirstFileEx Lib "kernel32" Alias "FindFirstFileExA" ( _
ByVal lpFileName As String, ByVal fInfoLevelId As Long, lpFindFileData As WIN32_FIND_DATA, _
    ByVal fSearchOp As Long, ByVal lpSearchFilter As Long, ByVal dwAdditionalFlags As Long) As Long
Private Declare Function FindNextFile Lib "kernel32" Alias "FindNextFileA" ( _
    ByVal hFindFile As Long, lpFindFileData As WIN32_FIND_DATA) As Long
Private Declare Function FindClose Lib "kernel32" (ByVal hFindFile As Long) As Long


Private Function GetFileNames(ByVal sPath As String) As Collection

    Dim fileInfo    As WIN32_FIND_DATA  'buffer for file info
    Dim hFile       As Long             'file handle
    Dim colFiles    As New Collection

    sPath = sPath & "*.*"

    hFile = FindFirstFileEx(sPath & vbNullChar, FindExInfoBasic, fileInfo, FindExSearchNameMatch, 0&, FIND_FIRST_EX_LARGE_FETCH)

    If hFile <> INVALID_HANDLE_VALUE Then
        Do While FindNextFile(hFile, fileInfo)
            colFiles.Add Left(fileInfo.cFileName, InStr(fileInfo.cFileName, vbNullChar) - 1)
        Loop

        FindClose hFile
    End If

    Set GetFileNames = colFiles

End Function
    Function FindFast(TargetFolder As String, Patt As String)
       
        Dim Folder As Object, SubFolder As Object, File As Object
        Dim FQueue As New Collection

       'Test view all folders:
'       Dim FolderColl As New Collection
        
       Dim Count As Integer

        Dim fl As String
           
        With CreateObject("Scripting.FileSystemObject")

            FQueue.Add .GetFolder(TargetFolder)
            Do While FQueue.Count > 0
               Set Folder = FQueue(1)
               FQueue.Remove 1
                'Code for individual folder
                For Each SubFolder In Folder.subFolders

                    'Test view all folders:
                    FolderColl.Add SubFolder

                    'Only 10 folders deep
     '               Count = Len(SubFolder) - Len(Replace(SubFolder, "\", ""))
     '               If Count < 13 Then
                        FQueue.Add SubFolder
                   
             '           ' Only look for the file in Working folder
               '        If InStr(1, SubFolder, "Working") > 1 Then
    
                            fl = Dir(SubFolder & "\" & Patt)
                ' Added as exact match return.  Otherwise will find all with pattern match
                If fl <> "" Then
                    FindFast = SubFolder & "\" & fl
                    Exit Function
                End If
                               
                 '       End If
        '             End If
                Next SubFolder
            Loop

'   Test view all folders:
' Dim i As Long
'For i = 1 To FolderColl.Count
'                                Range("A" & i).value = FolderColl(i)
'Next i           

        End With
         FindFast = vbNullString
    End Function
    
    
    Sub FindFile()
        Dim StartTime As Double
        Dim SecondsElapsed As Double
        Dim target As String
    
        Dim targetName As String
        Dim targetPath As String
        targetName = "5-3-21_Order_Sent.xlsx"
       '    Patt = "*_Order_Sent.xlsx"
        ' or wild extension   Patt = "*_ThisName.*"
    
        targetPath = "\\Fulfill\Company\Orders\Completed"

        StartTime = Timer    
        target = FindFast(targetPath, targetName)
        Debug.Print target
        SecondsElapsed = Round(Timer - StartTime, 2)
        Debug.Print "FindFast: " & SecondsElapsed & " Secs"
    
        MsgBox "found FindFast: " & target & " - " & SecondsElapsed & " Secs"       

        StartTime = Timer   
        target = Recurse(targetPath, targetName)
        Debug.Print target
    
         SecondsElapsed = Round(Timer - StartTime, 2)
        Debug.Print "Recurse: " & SecondsElapsed & " Secs"
    
         MsgBox "found Recurse: " & target & " - " & SecondsElapsed & " Secs"
    
    End Sub