Entwickler-Ecke

Basistechnologien - System.OutOfMemoryException beim Ändern einer CSV-Datei


CodingForBeer - Do 11.09.14 12:27
Titel: System.OutOfMemoryException beim Ändern einer CSV-Datei
Hallöle,

mit folgendem Code lese ich eine CSV-Datei
ein und ersetze die Zeichenfolge ", durch ";


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
StreamReader inputStreamReader3 = File.OpenText(filepathread);
String Inhalt3 = inputStreamReader3.ReadToEnd();
inputStreamReader3.Close();
String ersetzen3 = "\",";
String durch3 = "\";";
Inhalt3 = Inhalt3.Replace(ersetzen3, durch3);
StreamWriter outputStreamWriter3 = File.CreateText(filepathread);
outputStreamWriter3.Write(Inhalt3);
outputStreamWriter3.Close();
Inhalt3 = "";


Die CSV-Datei hat eine Größe von: 125,126 KB
Excel kann sie nicht vollständig laden, da sie mehr als 1.048.576 Zeilen hat
Der Windows-Editor kann sie laden. Da habe ich gesehen, dass die Datei 1.106.601 Zeilen hat.
Die Spaltenanzahl beträgt: 11

Folgende Fehlermeldung tritt beim Programmablauf auf:
Ein Ausnahmefehler des Typs "System.OutOfMemoryException" ist in mscorlib.dll aufgetreten.

Wie ließe sich der Code dahingehend optimieren, dass die Datei
doch noch manipuliert werden kann?

Oder ist hier nur die hardwaremäßige Aufrüstung möglich?

Ach ja, OS: Win7, RAM: 4GB

Danke und Gruß
CfB


Th69 - Do 11.09.14 13:03

Hallo und :welcome:

wäre es nicht besser, du liest die Datei zeilenweise und schreibst dann auch immer nur jeweils die geänderte Zeile wieder weg (anstatt die ganze Datei zu laden, zu ändern und wieder abzuspeichern).
Stichworte: StreamReader.ReadLine [http://msdn.microsoft.com/en-us/library/system.io.streamreader.readline%28v=vs.110%29.aspx] und StreamWriter.WriteLine [http://msdn.microsoft.com/en-us/library/system.io.streamwriter.writeline%28v=vs.110%29.aspx]


CodingForBeer - Do 11.09.14 13:07

HeyHo,

danke für's :welcome:

Aber ja, Du hast recht.
An das zeilenweise Einlesen und Wegschreiben
hatte ich gar nicht gedacht.

Probier ich gleich mal aus.
Rückmeldung gibt's dann auch. ;-)

Danke und Gruß
CfB


Palladin007 - Do 11.09.14 13:34

Und schau dir mal das Stichwort using [http://msdn.microsoft.com/de-de/library/yh598w02.aspx] an.

Dann würde ich eigentlich auch eher die Open-Methode von File nutzen. Du bekommst du den FileStream, mit dem du dann einen StreamWriter oder StreamReader öffnen kannst.
So muss die Datei nicht mehrfach geöffnet werden.
Das direkt in der selben Datei zu ändern ist auch unglücklich, da du in der Datei nur umständlich lesen und gleichzeitig schreiben kannst und das dann vermutlich auch nur mit Problemen.



Besser und sehr kompakt wäre es, wenn du mit den LINQtoObjects-Erweiterungsmethoden arbeitest und dann File.ReadLines und File.WriteAllLines verwendest. So musst du dich nicht selber um das Disposen kümmern und bei den beiden Methoden werden die Zeilen auch lazy nachgeladen.

Was ich allerdings nicht weiß (das müsstest du ausprobieren), ist, ob der die Zeilen am Anfang im Speicher behält und das dann trotzdem eine Exception gibt.


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
var filePath = "myCsvFile.csv";
var tempFilePath = filePath + ".temp";

File.Move(filePath, tempFilePath);

var modifiedLines = File.ReadLines(tempFilePath).Select(line => line.Replace("\",""\";"));
File.WriteAllLines(filePath, modifiedLines);

File.Delete(tempFilePath);



Ist ungetestet, hab hier kein VS installiert, müsste aber funktionieren.
Ob da dennoch der Speicher überläuft, kann ich dir leider nicht sagen, der Code ist meiner Meinung nach aber deutlich verständlicher.


CodingForBeer - Do 11.09.14 14:05

Hallo Palladin007,

absolut richtig.
Das gleichzeitige Offenhalten und Schreiben in ein
und dieselbe Datei ist in der Tat problematisch.

Zugriff verweigert...Datei wird schon benutzt...
Datei ist nicht offen...und so weiter ;-)

Bei Deinem Ansatz muss ich wegen dem Zugriff zwar
auch mit einer temporären Datei arbeiten, aber
das ist OK.

Also, vielen Dank!
Problem ist gelöst!

CfB

EDITH sagt:
Die Zeile 5: File.Delete(filePath);
muss ans Ende verschoben werden, da
erst nach den Operationen die
temporäre Datei gelöscht werden soll. ;-)


Ralf Jansen - Do 11.09.14 14:28

Zitat:
Die Zeile 5: File.Delete(filePath);


Ich hoffe du löscht am Ende tempFilePath und nicht filePath. Sonst wäre die ganze Aktion Zeitverschwendung ;)
Da wo Paladin den File.Delete platziert hatte war der eh eher sinnfrei. Nach einem Move sollte da nix mehr sein.


Palladin007 - Do 11.09.14 14:31

In so einem Fall halte ich es für sinnvoll, eine temporäre Datei zu verwenden, auf der Festplatte ist schließlich mehr Platz als im RAM:

Und du kannst gleichzeitig Schreiben und Lesen.
Wobei, nicht ganz gleichzeitig, aber man kann eine Datei zum schreiben und lesen öffnen und auch beides machen.
Das könnte sogar ganz gut funktionieren.

Öffne mal einen Stream zum Lesen UND Schreiben (geht auch mit dem Konstruktor von FileStream) und erstelle damit dann einen StreamWriter und StreamReader.
Ich könnte mir vorstellen, wenn du dort manuell nach Vorkommen vom Text suchst, den dort löschst und dann einfach den neuen String schreibst, hast du genau das, was du willst.
Allerdings müsstest du das Suchen dann selber bauen, mit dem Nachteil, dass du nur Zeichen für Zeichen durchlaufen kannst.

Wenn ich Zuhause bin, kann ich ja mal was basteln.
Ist denke ich allgemein ganz praktisch, möglichst performant in großen Dateien ohne temporäre Datei ersetzen.


Ralf:

Ja, stimmt, Delet ist sinnfrei und ich hab am Ende das Move "zurück" vergessen.
Werde ich gleich anpassen, danke für den Hinweis.


Ralf Jansen - Do 11.09.14 14:32

Zitat:
Move "zurück" vergessen.


Nö. Nur das löschen des Tmp File.


Palladin007 - Do 11.09.14 14:39

Was ist los mit mir? :/

Ich merk schon, das Routieren in den Abteilungen bekommt mir nicht, ich roste ein :D


Wie auch immer, ich habs angepasst, müsste jetzt stimmen.


CodingForBeer - Do 11.09.14 15:04

Höhö...
rotierst Du in den Abteilungen oder rotieren die wegen Dir. :P

Und ja, ich lösche die tmp-Datei und nicht die eigentliche CSV-Datei.

Mit dem Auslagern in eine zweite Datei ist das in Ordnung.
Hier spielt auch die Performance keine Rolle. Es werden Daten in eine DB
eingespielt und das geschieht maximal einmal in drei Monaten.
Ob's 1 Minute oder 1 Stunde dauert...Who cares? ;-)

Allerdings ist mir eben etwas anderes passiert. Mit wurde doch tatsächlich
ein Timeout vor die Nase gesetzt.

Die Daten schiebe ich per SqlBulkCopy in die DB:
SqlBulkCopy bc = new SqlBulkCopy(con.ConnectionString, SqlBulkCopyOptions.TableLock);

Das Hizufügen von
bc.BulkCopyTimeout = 600;
hat den Timeout dann schnell beseitigt.

Mal sehen, ob das so noch mit einer 1,4 GB CSV funktioniert.
Eventuell kommt dann doch wieder die MemoryException. :shock:

CfB


Palladin007 - Do 11.09.14 15:18

Die OutOfMemoryException kommt nur, wenn der Speicher voll läuft.
Wenn du die Daten nicht im SPeicher behältst, sollte das kein Problem sein.
Aber warum schreibst du erst in die Datei um dann in die Datenbank zu lesen? Lese doch direkt in die DB und passe die Zeilen in dem Zug direkt an.

Was den BulkCopy angeht, kann ich leider nicht helfen.
Das mit dem Timeout könnte ich mir aber so erklären, dass .NET auf eine Reaktion wartet, bei so vielen Daten dauert das aber.
Wahrscheinlich bist du da besser beraten, wenn du die Daten in Abschnitte aufteilst und die dann einzeln bearbeitest.
Aber bei dem Thema sollten besser die helfen, die auch genug Erfahrung haben, ich kenne nur die Grundlagen der POCOs von EF :D


CodingForBeer - Do 11.09.14 16:07

Hi,

die Dateien schreibe ich deswegen um, weil sie später noch benötigt werden und
dann nur in der manipulierten Form vorliegen sollen.

Inhaltlich sehen die CSV-Dateien so aus (verkürzt):

"Spalte1","Spalte2","Spalte3","Spalte4",...,"Spalte11"
"Eintrag1","Eintrag2","Eintrag3","Eintrag4",...,"Eintrag11"
"Eintrag12","Eintrag13","Eintrag14","Eintrag15",...,"Eintrag22"
...

Leider kann es auch passieren, dass innerhalb eines Wortes ein Komma auftaucht:

"Spalte1","Spalte2","Spalte3","Spalte4",...,"Spalte11"
"Eintrag1","Eintrag2","Eintrag
,3","Eintrag4",...,"Eintrag11"
"Eintrag12","Eintrag13","Eintrag14","Eintrag15",...,"Eintrag22"
...


Auch leere Wörter gibt es durchaus:

"Spalte1","Spalte2","Spalte3","Spalte4",...,"Spalte11"
"Eintrag1",
"","Eintrag3","Eintrag4",...,"Eintrag11"
"Eintrag12","Eintrag13","Eintrag14","Eintrag15",...,
""
...


Im ersten Schritt ersetze ich "" durch "NULL"
Somit gibt es keine leeren Wörter mehr.

Im zweiten Schritt ersetze ich ", durch ";
Somit ersetze ich nur die worttrennenden Kommas (oder heißt das Kommata?).
Die innerhalb eines Wortes bleiben erhalten.

Im dritten Schritt entferne ich alle "
so dass die CSV-Datei wie folgt aussieht:

Spalte1;Spalte2;Spalte3;Spalte4;...;Spalte11
Eintrag1;NULL;Eintrag,3;Eintrag4;...;Eintrag11
Eintrag12;Eintrag13;Eintrag14;Eintrag15;...;NULL
...


Anhand der Semikolons (oder heißt das Semikola?) trenne ich das dann in die einzelnen
Wörter auf, die in die jeweiligen DB-Tabellen wandern.

Schleifengesteuert lese ich alle CSV-Dateien eines Verzeichnisses ein und erzeuge anhand
der Dateinamen die Tabellennamen (nach vorheriger Entfernung unerlaubter Zeichen).
Die Anzahl der Spalten sowie ihre Bezeichnungen hole ich mir dann aus der jeweils ersten
Zeile der CSV-Datei.

Aber gut, ich will das hier jetzt nicht breit treten...ist ja schon Off-Topic verdächtig. ;-)

Gruß
CfB