Out Of Memory bei einlesen einer Datei

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

Es gibt 18 Antworten in diesem Thema. Der letzte Beitrag () ist von Humax.

    Out Of Memory bei einlesen einer Datei

    Hallo, ich möchte eine Datei einlesen um dann zu schauen ob ein bestimmter String darin enthalten ist.

    Mit folgendem Code bekomme ich einen out of memory Fehler

    VB.NET-Quellcode

    1. ​Using reader As New System.IO.StreamReader(Dateiname)
    2. Dateiinhalt = reader.ReadToEnd()
    3. End Using



    Also lese ich jetzt mit folgendem Code die Datei Zeile für Zeile ein und schaue nach dem gesuchten String.


    VB.NET-Quellcode

    1. ​Using reader As New System.IO.StreamReader(Dateiname)
    2. Do Until gefunden = True Or reader.EndOfStream = True Or EInlesen_abbrechen = True
    3. My.Application.DoEvents()
    4. Dateiinhalt = reader.ReadLine.ToLower
    5. If Dateiinhalt.Contains(gesuchterText) Then
    6. gefunden = True
    7. End If
    8. Loop
    9. End Using


    Wenn der gesuchte Text nicht enthalten ist kann es bei einer großen Datei sehr lange dauern, deshalb die Frage ob es eine Möglichkeit gibt dies einfacher (schneller) zu lösen.
    Ich denke, ein großer Zeitfresser bei dir ist der Aufruf von

    Quellcode

    1. My.Application.DoEvents()


    Schmeiss den Kram raus und mach mit Async und Await einen aysnchronen Aufruf daraus.

    Zum Testen, wieviel schneller das geht lass vor dem Umbau einfach mal

    Quellcode

    1. My.Application.DoEvents()

    weg und vergleiche die Laufzeiten.

    Auszuprobieren wäre noch, ob das auch noch schneller geht:

    Quellcode

    1. DIm gefunden As Boolean = (reader.ReadLine.IndexOf(gesuchterText, StringComparison.OrdinalIgnoreCase) >= 0)


    Das ist ein Stringkopieren weniger drin

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

    Du sparst Zeit wenn du statt Zeilen Bloecke einliest, ich glaube so geht das in(bis zu) der haelfte der Zeit.

    VB.NET-Quellcode

    1. Sub xyz(ByVal filename As String)
    2. Using fs As New IO.FileStream(filename, IO.FileMode.Open)
    3. Using sr As IO.StreamReader = New IO.StreamReader(fs)
    4. Dim toFind As String = "some stuff"
    5. While Not sr.EndOfStream
    6. Dim buffer() As Char = New Char(2047) {}
    7. sr.ReadBlock(buffer, 0, buffer.Length)
    8. Dim txt As String = New String(buffer).ToLower()
    9. If txt.Contains(toFind) Then
    10. End If
    11. If Not sr.EndOfStream Then
    12. fs.Position -= toFind.Length
    13. End If
    14. End While
    15. End Using
    16. End Using
    17. End Sub

    And i think to myself... what a wonderfuL World!

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

    Aber: Bei Eddys Code wird immer der ganze Buffer durchsucht. Das kann zu falschen Ergebnissen führen. Beispiel:
    Dateiinhalt: "01234567012345670123456701234567" (also 32 Zeichen, 4 mal 0 bis 7)
    Puffergröße: 10
    Suchtext: "6767"
    Der Suchtext kommt offensichtlich nicht in der Datei vor. Aber man beachte, was der Puffer bei jedem Schleifendurchlauf beinhaltet (ich habe das Zurücksetzen der Streamposition jetzt mal ignoriert, weil das Problem trotzdem besteht):

    Quellcode

    1. 0123456701 // Nein
    2. 2345670123 // Nein
    3. 4567012345 // Nein
    4. 6767012345 // Ja!

    Im letzten Durchlauf wurden nur die ersten beiden Zeichen im Puffer überschrieben. Der Rest ist noch der alte Inhalt. Deshalb wird der Suchtext gefunden, obwohl er eigentlich nicht in der Datei steht.

    Die sr.ReadBlock-Funktion gibt die Anzahl an gelesenen Zeichen zurück. Die gibt man dann dem String-Konstruktor mit.

    Und mir ist gerade aufgefallen, dass der Puffer bei jedem Schleifendurchlauf erneut erstellt wird. Das heißt, dieses Problem tritt nur dann auf, wenn der Suchstring 0-Zeichen am Ende beinhaltet. Aber es sollte klar sein, dass man den Buffer einmal erstellen und dann weiterverwenden sollte.

    Übrigens lässt sich die Sache noch weiter optimieren: Wenn man weiß, dass der Suchstring keine Zeilenumbrüche beinhaltet, dann kann man einfach sowas verwenden:

    VB.NET-Quellcode

    1. Function FileContains(FilePath As String) As Boolean
    2. Using Reader As New StreamReader(FilePath)
    3. Do Until Reader.EndOfStream
    4. If Reader.ReadLine.ToLower.Contains(Suchstring) Then Return True
    5. Loop
    6. End Using
    7. Return False
    8. End Function
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    Danke fuer den Hinweis :thumbup:

    So ist es nu richtig und funktioniert recht flott:

    VB.NET-Quellcode

    1. Function FileContainsString(ByVal filename As String, ByVal toFind As String) As Boolean
    2. Using fs As New IO.FileStream(filename, IO.FileMode.Open)
    3. Using sr As IO.StreamReader = New IO.StreamReader(fs)
    4. Dim buffer() As Char = New Char(2047) {}
    5. Dim readCount As Integer
    6. Dim txt As String
    7. While Not sr.EndOfStream
    8. readCount = sr.ReadBlock(buffer, 0, buffer.Length)
    9. txt = New String(buffer, 0, readCount)
    10. If txt.Contains(toFind) Then
    11. Return True
    12. End If
    13. If Not sr.EndOfStream Then
    14. fs.Position -= toFind.Length
    15. End If
    16. End While
    17. End Using
    18. End Using
    19. Return False
    20. End Function

    And i think to myself... what a wonderfuL World!

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

    Hi, hatte erst jetzt mal kurz Zeit und habe mal nach Dukes Vorschlag die Zeit gemessen.
    Meine Methode, Zeile für Zeile mit DoEvents benötigt 46 Sekunden für die Datei
    Ohne DoEvents 44 Sekunden.

    Ich hatte es nicht für so wichtig erachtet, und daher im 1. Post nicht erwähnt... Da die Datei bzw. nun jede eingelesene Zeile nach mehreren Strings (13) durchsucht werden muss, hatte ich

    VB.NET-Quellcode

    1. If Dateiinhalt.Contains(gesuchterText) Then

    mit entsprechend vielen If Elseif behandelt. Habe den Code jetzt dann mal durch eine Select Case Abfrage ersetzt, macht es aber auch nicht viel schneller.
    Mit Select Case und DoEvents 46 Sekunden
    Mit Select Case ohne DoEvents 43 Sekunden.

    Zum Spass mal die Datei mit dem SpeedCommander geöffnet, der braucht 8 Sekunden...

    Während der DoEvents wird eigentlich eh nichts gemacht, ausser evtl den Abbrechen-Button zuzulassen.

    Werde heute Abend oder morgen mal den Code von Eddy probieren und vorher noch

    VB.NET-Quellcode

    1. ​DIm gefunden As Boolean = (reader.ReadLine.IndexOf(gesuchterText, StringComparison.OrdinalIgnoreCase) >= 0)
    testen, wie schnell das dann ist
    Die Dateien haben im Normalfall 50 - 800MB (jetziger Stand - demnächst wahrscheinlich bis zurGröße einer DVD, also so 4,6 GB) . Normalerweise wird der gesuchte Text relativ schnell gefunden. Hab jetzt testweise mal in 4 Dateien geguckt, da war der Text innerhalb der ersten 100 Zeilen gefunden. Aber ob es nicht mal doch anders ist kann ich nicht sagen.
    In dem Fall würde ich eher behaupten, dass Du nach Bytes suchen solltest, nicht nach Zeichen. Dadurch sparst Du Dir das Decoding.
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    So auf die schnelle waere das meine erste Idee:

    VB.NET-Quellcode

    1. Function FileContainsStrings(ByVal filename As String, ByVal toFind() As String) As Boolean
    2. Dim longestStringLength As Integer = toFind.OrderByDescending(Function(s) s.Length).First().Length
    3. Dim stringsFound() As Boolean = New Boolean(toFind.Length - 1) {}
    4. Dim buffer() As Char = New Char(2047) {}
    5. Dim readCount As Integer
    6. Dim txt As String
    7. Using fs As New IO.FileStream(filename, IO.FileMode.Open)
    8. Using sr As IO.StreamReader = New IO.StreamReader(fs)
    9. While Not sr.EndOfStream
    10. readCount = sr.ReadBlock(buffer, 0, buffer.Length)
    11. txt = New String(buffer, 0, readCount)
    12. For i = 0 To toFind.Length - 1
    13. If txt.Contains(toFind(i)) Then
    14. stringsFound(i) = True
    15. If stringsFound.All(Function(b) b) Then
    16. Return True
    17. End If
    18. End If
    19. Next
    20. If Not sr.EndOfStream Then
    21. fs.Position -= longestStringLength
    22. End If
    23. End While
    24. End Using
    25. End Using
    26. Return False
    27. End Function

    And i think to myself... what a wonderfuL World!

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

    Hi Eddy, glaube du hattest mich falsch verstanden oder ich mich schlecht ausgedrückt oder ich habe deinen Code nicht so ganz verstanden... Ich wollte nicht alle "Suchtexte" finden sondern nur einen von mehreren.
    Ich habe deinen Code jetzt mal so angepasst. Bringt mir das Ergebnis jetzt in 30 Sekunden , statt vorher 44.
    Erstmal vielen Dank für die Hilfe.
    Hier mal der angepasste Code:

    VB.NET-Quellcode

    1. ​Dim tofind() As String = {"SLES-", "SCES-", "SLUS-", "SCUS-", "SLPM-", "SLPS-", "SCPS-", "SLES_", "SCES_", "SLUS_", "SCUS_", "SLPM_", "SLPS_", "SCPS_"}
    2. 'Dim longestStringLength As Integer = tofind.OrderByDescending(Function(s) s.Length).First().Length
    3. Dim buffer() As Char = New Char(2047) {}
    4. Dim readCount As Integer
    5. Dim ID As String = ""
    6. Dim txt As String = String.Empty
    7. Using fs As New IO.FileStream(Dateiname, IO.FileMode.Open)
    8. Using sr As IO.StreamReader = New IO.StreamReader(fs)
    9. While Not sr.EndOfStream
    10. readCount = sr.ReadBlock(buffer, 0, buffer.Length)
    11. txt = New String(buffer, 0, readCount)
    12. For i = 0 To tofind.Length - 1
    13. If txt.Contains(tofind(i)) Then
    14. ID = txt.Substring(txt.IndexOf(tofind(i)), 11)
    15. Exit While
    16. End If
    17. Next
    18. If Not sr.EndOfStream Then
    19. fs.Position -= tofind.Length
    20. End If
    21. End While
    22. End Using
    23. End Using


    Habe aber trotzdem noch ein paar Fragen zur Verständnis...
    Da mein gesuchter text immer gleich lang ist (5 Zeichen), habe ich longeststringlength weggelassen.
    Könntest du noch kurz erklären was dein Code macht, also speziell diese Zeile

    VB.NET-Quellcode

    1. ​Dim buffer() As Char = New Char(2047) {})

    Buffer als ein Array angelegt, warum 2047 - welche Auswirkungen hätte hier eine anderer Zahl? Hat das was mit dem maximal verwendeten Speicher zu tun wegen der Auslastung des RAM?
    Hi Humax,

    hatte verstanden das du pruefen wolltest ob alle n Strings in der Datei zu finden sind.

    Die gennante Zeile erzeugt einen Char-Array mit 2048 Indicies, mit der Default-Value, somit werden pro Durchlauf der Schleife 2KB der Datei verarbeitet. Bei kleineren Werten wird die Schleife also oefter durchlaufen als bei groesseren. Bei der Verarbeitung in der Schleife dauert es etwas laenger wenn ein groesserer Puffer zu verarbeiten ist(hier wird minimal sein). Da musst du mal schauen welche Puffer-Groesse hier die beste ist, waehle fuern den Anfang mal einen deutlich groesseren Puffer z.B. mal 1048576(1MB). Der kleine Puffer hat mit dem RAM nicht wirklich viel zu tun, selbst wenn man einen 10MB Puffer nimmt, das faellt heutzutage kaum noch ins Gewicht. Bei sehr alten Maschinen z.B. mit 256 MB Ram oder weniger, das sollte man doch kleine Puffer waehlen, da sonst viel auf der Festplatte zwischengespeichert werden muss, das kostet natuerlich Zeit.
    And i think to myself... what a wonderfuL World!