Get Tree Info

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von Peter329.

    Get Tree Info

    Hi,

    hier habe ich eine recht komplexe Routine, die zu einem vorgegebenen Verzeichnis, folgendes ermittelt:

    die Gesamtzahl der Unterverzeichnisse im Verzeichnisbaum (also SubDirectories, SubSubDirectories ... etc.)
    die Gesamtzahl der File im Verzeichnisbaum
    die Gesamtanzahl der Bytes aller Files im Verzeichnisbaum.

    Das klingt gar nicht so schwierig ... aber es gibt folgende Verschärfung: Wenn ein Verzeichnis oder ein File nicht zugreifbar ist, weil die Berechtung dazu fehlt, dann soll das Dingens einfach ignoriert werden. Auf Wunsch kann man eine Liste der nicht zugreifbaren Objekte anzeigen.

    Das ist mein Coding:

    VB.NET-Quellcode

    1. Shared Function GetTreeInfo(ByVal directory As DirectoryInfo,
    2. ByVal errorHandler As Action(Of FileSystemInfo)) As Long
    3. Dim dirlen As Long = 0 'Initialize directory size field
    4. Try
    5. Dim enrDirs As IEnumerable(Of DirectoryInfo) = directory.EnumerateDirectories() 'Get list of all subdirectories in directory
    6. TreeSubDirectoriesCount += enrDirs.Count 'Accumulate directories (Module1 field)
    7. Dim enrFiles As IEnumerable(Of FileInfo) = directory.EnumerateFiles() 'Get list of all files in directory
    8. TreeSubFilesCount += enrFiles.Count 'Accumulate files (Module1 field)
    9. Try
    10. dirlen = enrFiles.Sum(Function(f) f.Length) 'Accumulate length (f.Length) of all files (f) into dirlen ...
    11. ' '... dirlen wil be changed,when the call has completed
    12. Catch exp As UnauthorizedAccessException 'Not enough access rights for file
    13. dirlen = enrFiles.Sum(Function(f)
    14. Try
    15. Return f.Length
    16. Catch ex As UnauthorizedAccessException
    17. errorHandler(f) 'Call errorHandler with the file that caused the error ...
    18. Return 0 '... and return 0 bytes for this file
    19. End Try
    20. End Function)
    21. End Try
    22. dirlen += directory.EnumerateDirectories().Sum(Function(dir) GetTreeInfo(dir, errorHandler)) 'recursive call - accumulate file lengths
    23. Catch ex As UnauthorizedAccessException 'Not enough access rights for directory
    24. errorHandler(directory) 'Call errorHandler with the directory that caused the error
    25. Catch ex As DirectoryNotFoundException 'Call errorHandler if directory not found
    26. errorHandler(directory)
    27. End Try
    28. Return dirlen 'Return total length of directory
    29. End Function


    Das Ding funktioniert auch prima! Allerdings dauert die Verarbeitung etwa der Systemresidenz ca. 45 Sekunden. Das scheint mir ein bissl lang zu sein.

    Hat jemand Ideen wie man die Performance verbessern könnte? Denn darauf kommt es schon an ... meine Systemresidenz hat etwa 60.000 Directories und 220.000 Files ... da spielt das schon eine Rolle, ob der Code performant ist. :)

    Ich schätze mal, dass man mindestens diese Kiste intelligenter abhandeln könnte:

    VB.NET-Quellcode

    1. Dim enrDirs As IEnumerable(Of DirectoryInfo) = directory.EnumerateDirectories() 'Get list of all subdirectories in directory
    2. TreeSubDirectoriesCount += enrDirs.Count 'Accumulate directories (Module1 field)
    3. Dim enrFiles As IEnumerable(Of FileInfo) = directory.EnumerateFiles() 'Get list of all files in directory
    4. TreeSubFilesCount += enrFiles.Count 'Accumulate files (Module1 field)


    Ich weiß, das ist sehr viel verlangt ... es sind zwar nur 30 Zeilen Code (beim Erstellen der Basis Routine hat mir RFG massiv zur Seite gestanden) ... aber die paar Zeilen haben es in sich. Es wäre nett, wenn jemand sich die Sache mal freundlich angucken könnte.

    LG
    Peter

    Dieser Beitrag wurde bereits 6 mal editiert, zuletzt von „Peter329“ ()

    Dir geht es also lediglich um obige drei Punkte?
    Da erscheint mir das ganze etwas ungeeignet...
    Ich habe die obigen Punkte mal so umgesetzt:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Structure DirectoryTreeInfo
    2. Public Property SubdirectoryCount As Integer
    3. Public Property FileCount As Integer
    4. Public Property CompleteFileSize As Long
    5. End Structure
    6. Public Function GetDirectoryTreeInfo(root As DirectoryInfo, errorHandler As Action(Of FileSystemInfo)) As DirectoryTreeInfo
    7. Dim info As New DirectoryTreeInfo()
    8. Dim method As Action(Of DirectoryInfo) = Sub(dir As DirectoryInfo)
    9. Try
    10. Dim subDirectories = dir.EnumerateDirectories()
    11. If subDirectories.Count > 0 Then
    12. info.SubdirectoryCount += subDirectories.Count
    13. For Each subDir In subDirectories
    14. method(subDir)
    15. Next
    16. End If
    17. Dim files = dir.EnumerateFiles()
    18. If files.Count > 0 Then
    19. info.FileCount += files.Count
    20. For Each file In files
    21. Try
    22. info.CompleteFileSize += file.Length
    23. Catch ex1 As UnauthorizedAccessException
    24. errorHandler(file)
    25. End Try
    26. Next
    27. End If
    28. Catch ex As DirectoryNotFoundException
    29. errorHandler(dir)
    30. Catch ex As UnauthorizedAccessException
    31. errorHandler(dir)
    32. End Try
    33. End Sub
    34. method(root)
    35. Return info
    36. End Function


    Sollte den selben Dienst erweisen.

    LG
    wow ... du hast genau erfasst was mein Anliegen ist!

    Und deine Lösung funktioniert auf Anhieb !

    Tja ... und dann hast du auch schon meine nächste Frage (sozusagen in vorauseilendem Gehorsam :) ) beantwortet: nämlich wie ich es schaffe, dass die Funktion DREI Werte und nicht nur EINEN Wert zurückliefert. Meine diesbezüglichen Versuche eine List(of long) zurück zu übergeben hatten sich mit der Art des rekursiven Aufrufs nicht vertragen ... und so habe ich als unbefriedigende Notlösung zwei Parameter über Public Variable in einem Datenmodul übergeben!

    Deine Lösung macht das sehr viel eleganter. Also, ich bin hell begeistert!

    Ich habe jetzt einen ersten Benchmark Test unternommen ... das ist nicht ganz so einfach, weil offensichtlich die Verarbeitung beim ersten Aufruf länger dauert als bei Folgeaufrufen (irgendwo wird da wohl ein Puffer gehalten). Nach meiner ersten Einschätzung läuft deine Lösung ein bissl schneller ... für einen Scan des C: Laufwerks braucht es jetzt ca. 40 (statt vorher 45) Sekunden. Nicht weltbewegend ... aber vermutlich geht es nicht signifikant schneller ...

    Herzlichen Dank für dein Code Beispiel ... und einen schönen Abend!

    LG
    Peter
    Deine Methode ruft zwei Mal dieselbe Methode auf, dürfte zwar keinen riesigen Unterschied machen, ist aber dennoch unnötig. Daher dürfte wohl der kleine Unterschied kommen.

    In dem Fall kann man ruhig auf eine Structure als Rückgabewert zurückgreifen, da nur einfache Datentypen darin vorkommen. Ansonsten sollte man halt eine Klasse nehmen.

    LG
    Die Sache mit dem API ist Klasse! Im Mittel durchsuche ich meine Systemresidenz jetzt in 8 - 11 Sekunden (Statt vorher 40 - 45 Sekunden).

    Ich hab die Routine dahingehend erweitert, dass jetzt auch noch eine Liste der nicht zugreifbaren Subdirectories (getrennt durch "Newline") zurückgeliefert wird. Man sollte schließlich im Display kenntlich machen, wenn einige Verzeichnisse wegen fehlender Zugriffsrechte ignoriert wurden - in diesem Fall sind die angezeigten Ergebnisse ja nur mit Vorsicht zu verwenden.

    Am Beginn der recursiven Routine teste ich, ob ein Break Key gedrückt ist (in meinem Fall ist das LCTRL) ... und wenn das der Fall ist zeige ich eine MessageBox, mit der man die Verarbeitung abbrechen kann. Das ist ganz angenehm, wenn man den Tree Scan versehentlich gestartet hat.

    Außerdem hab ich noch ein paar Kommentare angebracht, die vielleicht dem ein oder anderen ein bissl beim Verständnis der Routine helfen.

    VB.NET-Quellcode

    1. Private Sub EnumerateFilesystem(rootDir As String,
    2. ByRef files As Integer,
    3. ByRef folders As Integer,
    4. ByRef fullSize As Long,
    5. ByRef failingDirectories As String)
    6. Dim fls As Integer 'Define parameters to be returned
    7. Dim fldrs As Integer
    8. Dim fllSz As Long
    9. Dim failDirs As String = ""
    10. Dim INVALID_HANDLE_VALUE As New IntPtr(-1) 'Define directory search parameter
    11. Dim findData As WIN32_FIND_DATA = Nothing
    12. Dim recurse As Action(Of String) =
    13. Sub(directory)
    14. If StaticClass.CheckInterrupt() Then blnGetTreeInfoDisabled = True 'Check interrupt button depressed
    15. If blnGetTreeInfoDisabled Then Return 'Exit routine, if flag ist set
    16. 'Define recursive routine
    17. 'Use CharSet=CharSet.Unicode, and prepend your filename to search for with "\\?\".
    18. 'This gives go access to filenames up to 32767 bytes in length.
    19. 'Use "\\?\UNC\" prefix for fileshares.
    20. Dim findHandle = FindFirstFile("\\?\" & directory & "\*", findData) 'Read ahead directory entry
    21. If findHandle = INVALID_HANDLE_VALUE Then 'Cannot access directory
    22. failDirs &= directory & NewLine 'Return failing directory
    23. Return 'Jump one level higher
    24. End If
    25. Do
    26. If (findData.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY) = 0 Then 'Process files
    27. fls += 1 'Increment file count ...
    28. fllSz += findData.nFileSizeLow + (CLng(findData.nFileSizeHigh) << 32) '... respecting doubleword sizes (2 x UINT)
    29. Else 'Process directories
    30. If findData.cFileName(0) = "."c Then Continue Do 'Ignore root entries
    31. fldrs += 1 'Increment directory count
    32. recurse(Path.Combine(directory, findData.cFileName)) 'Call recursion
    33. End If
    34. Loop While FindNextFile(findHandle, findData) 'Process next file in directory
    35. FindClose(findHandle) 'Close handle
    36. End Sub 'End of recursive routine definition
    37. recurse(rootDir) 'Invoke recursive routine
    38. files = fls 'Return values
    39. folders = fldrs
    40. fullSize = fllSz
    41. failingDirectories = failDirs
    42. End Sub


    VB.NET-Quellcode

    1. Public NotInheritable Class StaticClass
    2. Public Const VK_LCONTROL As Keys = CType(&HA2, Keys) 'Interupt key
    3. <DllImport("user32.dll")>
    4. Public Shared Function GetAsyncKeyState(ByVal vKey As Keys) As Int16
    5. End Function
    6. Private Sub New() 'Do not allow creation of instance
    7. End Sub
    8. <DebuggerStepThrough>
    9. Public Shared Function CheckInterrupt() As Boolean
    10. If (GetAsyncKeyState(VK_LCONTROL) And &H8000) <> 0 Then
    11. If MessageBox.Show("Programm interrupted" & NewLine2 &
    12. "Do you want to exit?",
    13. "Program interrupt",
    14. MessageBoxButtons.YesNo,
    15. MessageBoxIcon.Warning) = DialogResult.Yes Then
    16. Return True
    17. End If
    18. End If
    19. Return False
    20. End Function
    21. End Class


    Recht herzlichen Dank an die beiden Ratgeber!
    LG
    Peter

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Peter329“ ()

    Wenn Du die

    Peter329 schrieb:

    VB.NET-Quellcode

    1. Sub(directory)
    nun noch statt einer anonymen Prozedur eine richtige draus machst, kannst Du sogar richtig drin debuggen.
    Anonyme Prozeduren sollten meiner Meinung nach nicht länger als 2-3 Zeilen sein.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Und wie mache ich das?

    Der Witz dieser Programmtechnik scheint mir ja gerade der zu sein, dass man die Routine als Action(Of String) definiert ... und damit die Rekursion startet.

    VB.NET-Quellcode

    1. Dim recurse As Action(Of String) =
    2. Sub(directory)
    3. ...
    4. End Sub 'End of recursive routine definition
    5. recurse(rootDir) 'Invoke recursive routine


    Was ist denn überhaupt eine "anonyme Prozedur" ...

    LG
    Peter
    @Rod: Was meinst du mit "kann man nicht debuggen"? Da kann man ebensogut einen Haltepunkt reinsetzen und durchsteppen etc.
    Und Bei Rekursion ist das oft sehr unschön, wenn man Daten in Listen akkumulieren will.
    Da muss man die Listen in jeden Selbstaufruf weiter hineinreichen, das bläht die Methode ziemlich auf.
    Hingegen eine anonyme Methode kann man die lokalen Variablen der umgebenden Methode zugreifen ähnlich wie eine normale Methode Klassenvariablen zugreift.
    Also vonne Kapselung her ist das ziemlich optimal.

    @Peter:

    Peter329 schrieb:

    Der Witz dieser Programmtechnik scheint mir ja gerade der zu sein, dass man die Routine als Action(Of String) definiert ... und damit die Rekursion startet.
    Die Definietion der Action startet nicht die Rekursion.
    Gestartet wird - ist auch richtig kommentiert - in zeile#5

    Eine anonyme Prozedur ist ein Prozedur, die keinen Namen hat.
    Das ist ziemlich verrückt, denn so kann man sie eigentlich garnet aufrufen. Kann man aber doch, wenn man sie gleich bei der Definition an eine Delegat-Variable zuweist (hier: recurse As Action(Of String))
    Der Delegat kann die Methode dann aufrufen.


    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    ErfinderDesRades schrieb:

    Die Definietion der Action startet nicht die Rekursion.


    Das ist mir schon klar ...

    Der Witz dieser Programmtechnik scheint mir ja gerade der zu sein, dass man die Routine als Action(Of String) definiert ... und mit diesem String die Rekursion dann später startet.

    ... so war das gemeint. Aber natürlich hast du das viel besser erklärt als ich das je könnte. :)

    Wenn ich deinen Rat also richtig interpretiere, dann sollte ich an der Prozedur nichts ändern ...

    Ansonsten läuft das Ding wie geschmiert ! Meine Nachfrage hat sich wirklich gelohnt! Ich bin richtig happy!

    LG
    Peter

    Peter329 schrieb:

    ... so war das gemeint...
    Jo, da bin ich kleinlich.
    Es ist eh schwierig zu verstehen, und da sitzt man glaub ganz schnell Missverständnissen und (fehlerhaftem) Halbwissen auf, wenn nicht genau korrekt formuliert ist.


    Peter329 schrieb:

    Wenn ich deinen Rat also richtig interpretiere, dann sollte ich an der Prozedur nichts ändern ...
    Jo.
    Du könntest es allenfalls mal ausprobieren mit dem Haltepunkt.