Entwickler-Ecke

Dateizugriff - Schnelles parsen von CSV-Dateien


OsCor - Mo 30.01.12 15:20

Moderiert von user profile iconNarses: Abgetrennt von [url=http://www.delphi-forum.de/viewtopic.php?p=657367#657367]hier[/url].

@zuma

Auf der Suche nach ganz was anderem habe ich diesen Thread überflogen. Dabei habe ich die mir noch unbekannten Methoden in Stringlist gesehen (Delimiter etc.). Da ich das Parsing von Strings, speziell von csv-Dateien (das ich sehr häufig brauche), bisher immer mit Pos, Copy und Delete erledigt habe (auch bei 800 000 Zeilen), hat mich der Hinweis darauf schier elektrisiert. Hast du Vergleiche bez. der Geschwindigkeit?

Oswald

P.S. Ich hoffe, dass mczero098 mir das „Thread-Hijacking” nicht verübelt.


Tranx - Mo 30.01.12 15:51

Vielleicht schaust Du mal hier rein:

http://www.delphi-treff.de/tutorials/

Da steht einiges zur Programmiersprache Delphi


jaenicke - Mo 30.01.12 18:37

user profile iconOsCor hat folgendes geschrieben Zum zitierten Posting springen:
Da ich das Parsing von Strings, speziell von csv-Dateien (das ich sehr häufig brauche), bisher immer mit Pos, Copy und Delete erledigt habe (auch bei 800 000 Zeilen), hat mich der Hinweis darauf schier elektrisiert. Hast du Vergleiche bez. der Geschwindigkeit?
Wenn es schnell gehen soll, würde ich mit PChars arbeiten. Dann brauchst du nicht die ganzen Stringoperationen. Wenn die Daten entsprechend formatiert sind, könntest du einfach die CSV-Trennzeichen durch Nullzeichen ersetzen und Pointer auf die Zeilenbestandteile speichern. Das sollte sehr fix gehen.


zuma - Di 31.01.12 09:01

user profile iconOsCor hat folgendes geschrieben Zum zitierten Posting springen:
@zuma

Auf der Suche nach ganz was anderem habe ich diesen Thread überflogen. Dabei habe ich die mir noch unbekannten Methoden in Stringlist gesehen (Delimiter etc.). Da ich das Parsing von Strings, speziell von csv-Dateien (das ich sehr häufig brauche), bisher immer mit Pos, Copy und Delete erledigt habe (auch bei 800 000 Zeilen), hat mich der Hinweis darauf schier elektrisiert. Hast du Vergleiche bez. der Geschwindigkeit?

Oswald

P.S. Ich hoffe, dass mczero098 mir das „Thread-Hijacking” nicht verübelt.



Hallo Oswald,
ich nutze das fast nur so für alle meine Import-Routinen.
Genaue Zeitmessungen hab ich nicht, aber wir haben eine alte Routine, die das auch mit Stringzerlegung per copy, pos, usw. macht, und die is nix schneller, lediglich aufwändiger zu schreiben.
Schreibe aktuell gerade einen csv-Import, werde mal mit gettickcount messen, wie lange so ein import dauert, und dann hier bescheid sagen.
Aber unabhängig von der Geschwindigkeit ist die Stringlist/Delimiter-variante wesentlich bequemer zu proggen ;)

Zuma


OsCor - Di 31.01.12 09:40

Auf die Zeiten bin ich gespannt.

@jaenicke

Meinst du mit „entsprechender Formatierung” feste Positionen innerhalb der Zeile? Wenn ja, dann fielen natürlich auch schon ohne Pointer 2 Arbeitsschritte weg. Da ich aber Endverwerter diverser Datenquellen bin, auf die ich nicht immer Einfluß habe, bleibt der „normale” csv-Input wohl Standard.
Aber vielleicht magst du noch erklären, wie du den Ansatz mit Pointer realisieren würdest? Ich sage es ehrlich: Wie viele andere habe ich die Arbeit damit bisher gescheut (hauptsächlich aus Angst vor schwer zu findenden Fehlern) - was nicht heißen muß, dass ich es nicht noch lerne :)

Oswald


jaenicke - Di 31.01.12 10:04

user profile iconOsCor hat folgendes geschrieben Zum zitierten Posting springen:
Meinst du mit „entsprechender Formatierung” feste Positionen innerhalb der Zeile?
Nein, ich meine vor allem, ob escapte Zeichen vorkommen. Denn dann ist der String ja nicht im "Endzustand" drin, sondern muss bearbeitet werden.

Wenn man einfach nur die Positionen bestimmen muss, geht das sehr gut. Ein Beispiel kann ich erst heute Abend posten, da ich jetzt bei der Arbeit bin. Arbeiten tue ich auf diese Weise z.B. hier, nur bei dir lässt es sich noch weiter optimieren:
http://www.delphi-forum.de/viewtopic.php?p=567719
Mehr dazu heute Abend, da sollte ich kurz Zeit dafür haben. Als Maßstab: In dem verlinkten Programm lese ich eine 300 MiB .reg Datei in unter 5 Sekunden komplett zeichenweise in den Baum ein und parse diese. ;-)


zuma - Di 31.01.12 14:28

Hallo OsCor,
kann leider beim aktuellen Import keine Zeiten geben,
da mit Gettickcount nicht messbar ;)

Hab aktuell eine Datei mit 2 - n Werten pro Zeile (im Schnitt so 150 Werte)
die Zeile einer weiteren Stringlist als DelimitedText zuzuweisen,
die ersten 2 Werte in lokale Vars und den Rest in eine weitere Stringlist zu adden,
ergibt bei gettickcount keine Differenz, geht also zu schnell ums messen zu können.

Das Eintragen der Werte in die DB dauert da deutlich länger (ca. 0,016 Sec).

Ich warte auch mal auf Jaenicke, mal sehen, ob ich noch was dazulernen kann ;)
Evtl. darf ich die Tage noch einem Kollegen helfen, der muss eine Datei mit jeweils 5000 Messwerten einlesen, die mit der DB abgleichen, aus den Differenzen ein Diagramm erstellen und dieses als PDF ablegen, das ganze muss unter 5 Sec pro Durchlauf passieren, da werden wir wohl etwas genauer auf die Zeiten achten müssen ;)

Zuma


OsCor - Di 31.01.12 16:19

So zeitkritisch ist es bei mir nicht. Aber bei jetzigen Laufzeiten von 35 Sekunden bei einem Lesevorgang und knapp 900 000 Zeilen lohnt sich eine effektive Programmierung ganz sicher, zumal ich eigentlich auch noch Ergebnisse als Graphik darstellen wollte. Auswertung auf „Knopfdruck” stellen sich die Kollegen sicher anders vor :roll:

Oswald


jaenicke - Do 02.02.12 08:30

So, nur kurz in den letzten Minuten geklöppelt:
Kleine Datei mit einer halben Million Einträgen mit je 6 Spalten, die ersten 20 Einträge des Arrays werden ausgegeben. Einlesezeit bei mir 0,1 Sekunden. Mehr dazu später, hab keine Zeit.


zuma - Do 02.02.12 10:07

Hallo Sebastian,
danke fürs hochladen,
hab zwar keine Zeit im Moment,
mir das genauer anzugucken,
bzw. mal eine meiner Routinen umzubauen
und nen (Geschwindigkeits-) Vergleich zu machen
aber die wird sich schon noch finden ;)

Zuma


OsCor - Do 02.02.12 12:07

Danke, Sebastian! Ich werde auch erst heute abend daran kommen, aber bei der Aussicht auf schnelle Laufzeiten freue ich mich schon darauf.

Oswald


Horst_H - So 05.02.12 14:26

Hallo,

in 0.1 Sekunden 22,6 MB ist ja schon enorm schnell, schneller als meine Festplatte, also aus dem Cache heraus.
Bei mir dauerte das gecachte Einlesen in eine popelige Stringlist mit freepascal schon 0,2 Sekunden.
Insgesamt mit dem nacheinander zerlegen aller 507075 Zeilen in 1 Sekunde ( 3,2 Ghz Phenom II, mit ausgeschaltetem Cool&quite)


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
program unbekannt;

uses 
  Classes, SysUtils;
 
var 
  t0,t1 :TDateTime;
  i,k,sp: integer;
  sExec: AnsiString;  
  AStringList,
  BStringList,
  CStringlist: TStringList;
  
begin
  T1:= now;
  T0:= now;
  T0 := 2*T0-T1;
  AStringList:= TStringlist.Create;

  sExec := IncludeTrailingPathDelimiter(GetCurrentDir)+'Mappe1.csv';

  AStringList.loadfromfile(sExec);
  
  T1 := now;
  Writeln('Es sind ',AStringlist.Count,' Zeilen');  
  Writeln('Einlesezeit ',FormatDateTime('HH:NN:SS.ZZZ',T1-T0));

  BStringList:= TStringlist.Create;
  BStringlist.delimiter := ';'
  CStringList:= TStringlist.Create;
  
  //Alle Zeilen zerlegen
  k := 0;
  For i := 0 to AStringlist.Count-1 do
    begin
    BStringList.delimitedtext := AStringList[i]; 
    CStringList.AddStrings(BStringList);
    sp := 0;
    While k < CStringlIst.count do
      begin
      CStringList.Objects[k] := TObject(sp  shl 24+i);
      inc(k);
      inc(sp);
      end;
    end;
  T1 := now;   
  AStringList.free;  
  //Ausgabe der letzten verarbeiteten Zeile 
  writeln(bStringList.text);   
  BStringList.free;

  IF k >= 40 then 
    k:= 39
  else 
    k := CStringlist.count-1;  
  For i := 0 to k do
    writeln(cStringlist[i],' Zeile ',Integer(CStringList.Objects[i]) AND $00FFFFFF:8,' Spalte ',Integer(CStringList.Objects[i]) shr 24:5) ;
    
  Writeln('Gesamtzeit: ',FormatDateTime('HH:NN:SS.ZZZ',T1-T0));
  CStringlist.free;
  readln;
end.

Ausgabe

Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
Free Pascal Compiler version 2.6.0 [2011/12/25] for i386 
....
Es sind 507075 Zeilen
Einlesezeit 00:00:00.193
639506
assasad
639506
assasad
639506
assasad

132432 Zeile        0 Spalte     0
assasad Zeile        0 Spalte     1
132432 Zeile        0 Spalte     2
assasad Zeile        0 Spalte     3
132432 Zeile        0 Spalte     4
assasad Zeile        0 Spalte     5
132433 Zeile        1 Spalte     0
assasad Zeile        1 Spalte     1
132433 Zeile        1 Spalte     2
assasad Zeile        1 Spalte     3
132433 Zeile        1 Spalte     4
assasad Zeile        1 Spalte     5
132434 Zeile        2 Spalte     0
assasad Zeile        2 Spalte     1
....
132437 Zeile        5 Spalte     0
assasad Zeile        5 Spalte     1
132437 Zeile        5 Spalte     2
assasad Zeile        5 Spalte     3
132437 Zeile        5 Spalte     4
assasad Zeile        5 Spalte     5
132438 Zeile        6 Spalte     0
assasad Zeile        6 Spalte     1
132438 Zeile        6 Spalte     2
assasad Zeile        6 Spalte     3
Gesamtzeit: 00:00:01.025


Ich gehe zunächst davon aus, dass das Einlesen nicht "die" Bremse ist.

Gruß Horst
Edit:
Ich habe jetzt auch alle Daten im Speicher mit Angabe von Zeile und Spalte ( maximal 255) .
Natürlich ist die Variante von jaenicke wesentlich speichersparender, aber selbst 900000 Zeilen lassen wohl meist im Hauptspeicher halten.