Dateien parsen - mit welchen Werkzeugen ?

Für Fragen von Einsteigern und Programmieranfängern...
Warf
Beiträge: 1911
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von Warf »

pluto hat geschrieben:TStringlist würde ich nicht nehmen, da sie immer erst die Datei komplett einlist und dann kann mit der Verarbeitung angefangen werden.
Mit TFileStream spart man sich das. Es ist auch nur geringfügig aufwendiger.


Wenn man weiß das die Datei nicht zu groß ist, kann man auch die gesamte Datei auf einmal einlesen, so wie du es machst (zeichen für Zeichen) würde ich es schon mal auf gar keinen Fall machen, da das zu heftigen Performanceeinbußen auf HDD's führen kann. Das Betriebsystem versucht dabei den Pfad des lesekopfes für alle anstehenden anfragen zu Optimieren, wenn du jetzt ein zeichen nach einander liest wird der lesekopf immer wieder zu den selben blöcken geführt, welche zusammenhängend sind, allerdings ist zwischen den schritten so viel zeit das das OS an dieser Stelle oftmals andere Aufträge ausführt, welche den Lesekopf wieder bewegen. Wenn du allerdings große Blöcke auf einmal liest, so sieht das OS, das diese befehle auf nah beieinander liegende Regionen zugreift, und kann damit den Pfad des Lesekopfes gut optimieren.

Und so HDD's sind echt nicht grade schnell, du kannst performanceeinbußen bis in den Millisekunden Bereich erhalten, was wenn man mal überlegt das die CPU irgendwo bei 2-3 GHz idled schon einen sehr großen unterschied macht.

Ansonsten wie gesagt, für solch einfache Konfigfiles, welche nur 2 verschiedene Typen von Tokens (Tags und Strings) verwendet, welche sich beide durch Reguläre Grammatik erzeugt werden können, kann man einfach eine Simple Lexikalische Analyse mittels DEA's oder entsprechend RegEx, welches auch DEA's verwendet (bzw verwenden sollte) ausführen. Ein solches Konstrukt mit verschiedenen Booleans kommt mir da doch viel zu kompliziert für vor

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

pluto hat geschrieben:TStringlist würde ich nicht nehmen, da sie immer erst die Datei komplett einlist und dann kann mit der Verarbeitung angefangen werden.

Naja, bei halbwegs normal gro0en Dateien, bis hin zu etlichen MB Grösse, bemerkt man diese Verzögerung bei den heutigen Computern doch gar nicht. Was ich bemerke, ist, wenn eine Datei auf einer Platte liegt, die sich im Ruhemodus befindet. Aber sogar ob die Datei dann 100KB oder 20MB groß ist, macht dann kaum einen Unterschied.


pluto hat geschrieben:Mit TFileStream spart man sich das. Es ist auch nur geringfügig aufwendiger.

Was ist eigentlich der vorteil von TFileStream gegenüber einem ganz normelen Read von einem textfile - wenn feststeht, dass die Daten aus dem Textfile zu lesen sind und sicher nicht von wo anders her kommen. :?:

DL3AD hat geschrieben:gibt es eine String Suchfunktion mit der man nicht Casesensitiv suchen kann d.h. 'test' und 'TEST' liefert ein Ergebnis ?

ansicontainstext

Im Regex kannst du mit dem Modifier (?i) - case insensitiv - und (?-i) - case sensitiv - sogar innerhalb des Regex umschalten. Default ist case-sensitiv.

so wie du es machst (zeichen für Zeichen) würde ich es schon mal auf gar keinen Fall machen, da das zu heftigen Performanceeinbußen auf HDD's führen kann. Das Betriebsystem versucht dabei den Pfad des lesekopfes für alle anstehenden anfragen zu Optimieren, wenn du jetzt ein zeichen nach einander liest wird der lesekopf immer wieder zu den selben blöcken geführt, welche zusammenhängend sind, allerdings ist zwischen den schritten so viel zeit das das OS an dieser Stelle oftmals andere Aufträge ausführt, welche den Lesekopf wieder bewegen.

Naja, ich kenne kein Betriebssystem das die Daten nicht zumindest sektorweise, eher aber in grösseren Blöcken, im Cache puffert. Da wird ganz sicher nicht für jedes einzelne Zeichen die Festpatte extra bemüht. :D
Aber du hast recht, geblockt zu lesen ist trotzdem immer schneller, weil jeder Betriebssystem-Aufruf mit einem gewissen Overhead verbunden ist. Wie weit Free Pascal selbst das Lesen von einem Textfile puffert, weiß ich nicht, ich vermute aber doch auch. TStringlist.Loadfromfile ist jedenfalls bequem und nicht nennenswert langsamer als irgend eine andere Methode.

Mathias
Beiträge: 6206
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von Mathias »

so wie du es machst (zeichen für Zeichen) würde ich es schon mal auf gar keinen Fall machen, da das zu heftigen Performanceeinbußen auf HDD's führen kann.

Da kann ich voll zustimmen, ich hatte mal Vektordaten-Daten als einzelne Single auf die HDD gespeichert, das war gähnend langsam. Sogar eine SSD war da sehr lahm.

Ich habe meinen Code noch ein wenig angepasst. Ich lese es jetzt Zeile für Zeile mit einem ganz normalen Read ein.
Dies macht das Ganze recht übersichtlich.

Code: Alles auswählen

procedure TForm1.Button2Click(Sender: TObject);
var
  f: Text;
  s:String;
  sa, sa2:TStringArray;
  i, j: Integer;
begin
  Memo1.Clear;
  AssignFile(f, 'test.txt');
  Reset(f);
  while not EOF(f) do begin
    ReadLn(f, s);
    sa:=s.Split([' ']);
 
    for i := 0 to Length(sa) - 1 do begin
      s := copy(sa[i], 2);
      sa2 := s.Split([':', '>']);
      if Length(sa2) > 0 then begin
        if sa2[0] = 'eor' then begin
          sa2[0] := 'Satz_Ende';
        end;
        s := '';
        for j := 0 to Length(sa2) - 1 do begin   // Für Testausgabe
          s := s + sa2[j] + '       ';
        end;
        Memo1.Lines.Add(s);
      end;
    end;
 
  end;
  CloseFile(f);
end;
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

Mathias hat geschrieben:
so wie du es machst (zeichen für Zeichen) würde ich es schon mal auf gar keinen Fall machen, da das zu heftigen Performanceeinbußen auf HDD's führen kann.

Da kann ich voll zustimmen, ich hatte mal Vektordaten-Daten als einzelne Single auf die HDD gespeichert, das war gähnend langsam. Sogar eine SSD war da sehr lahm.

Aber das hat ganz offensichtlich nichts mit den Bewegungen des Lesekopfs zu tun. Eine SSD bewegt keinen Lesekopf.

Mathias hat geschrieben:Ich habe meinen Code noch ein wenig angepasst. Ich lese es jetzt Zeile für Zeile mit einem ganz normalen Read ein.
Dies macht das Ganze recht übersichtlich.

Auf die Einwände von wp_xyz und mir gehst du aber sicherheitshalber auch bei deinem angepassten Code nicht ein...

DL3AD
Beiträge: 478
Registriert: Fr 13. Sep 2013, 12:07
OS, Lazarus, FPC: Debian Bullseye (L 2.2.0)
CPU-Target: 64Bit
Wohnort: Rügen

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von DL3AD »

... es gibt kein Zeilenende und Leerzeichen sind nur optional - ich habe Beispieldateien in denen ist nicht ein einziges Leerzeichen.

Es gibt in der Datei je Datensatz viele verschidene mögliche Tags.
Aus diesen möglichen Tags suche ich mir aber nur die heraus die ich benötige.

Ein Tag hat folgenden Aufbau <QSL_RCVD:1>Y oder so mit gleicher Info <qsl_rcvd:1>Y

beginnt mit <
Text kann mit _ sein
ein Doppelpunkt :
eine 1 bis 3 Stellige Zahl
und ein > als Abschluss
Danach folgen die Daten mit eine Länge der Zahl hinter dem :

Ich denke der Ansatz mit den Reguleren Ausdrücken ist recht interessant - werde mal versuchen mir für den Tag einen zu bauen.

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

Der Regex für deine Ausdrücke schaut so aus:
'(?x)(?i) < (?: (eor)>) | ([^:]+) : ([^>]+) > ([^<]+))'
Dabei bedeuten die Bestandteile folgendes:
(?x) Leerstellen im Regex-String werden ignoriert, damit wird das ganze etwas lesbarer
(?i) Case-insensitive
< Der Match fängt mit dem Zeichen < an
(?: Was hier innerhalb der Klammern folgt, ist als Submatch uninteressant. Die Klammern sind nur dazu da, um die beiden Alternativen zu klammern
(eor)>) 1. Alternative: Der Text eor>, wobei "eor" als erster Submatch gespeichert wird (den kannst du dann im weiteren Programm abfragen, der ist dann entweder 'eor' oder ein leerer String).
| Hier endet die erste Alternative und beginnt die zweite Alternative
([^:]+) 1 oder mehr beliebige Zeichen bis zu einem ":" , wird im zweiten Submatch gespeichert
: Dann ein Doppelpunkt
([^>]+) 1 oder mehr beliebige Zeichen bis zu einem ">" , wird im dritten Submatch gespeichert. Dieser Submatch enthält offenbar die Anzahl der Zeichen danach, aber ich bin nicht ganz sicher, ob regex etwas damit anfangen kann. Man könnte für den 4. Teilausdruck, der den Datenwert zum Tag enthält "(.{\3})\s*" statt "([^<]+)" versuchen, aber ich bin nicht sicher, ob das so funktioniert. Müsstest du ausprobieren.
> Das Zeichen >
([^<]+) 1 oder mehr beliebige Zeichen außer dem < , wird im vierten Submatch gespeichert, Das verbraucht alle Zeichen bis zum Ende des Strings oder bis zum nächsten "<"-Zeichen. Diese Variante funktioniert allerdings nicht, wenn in den Daten ein "<"-Zeichen vorkommen kann. Dann müsstest du "(.{\3})\s*" vesuchen, wenn das nicht funktioniert, dann wird es etwas komplizierter, dann musst du jeden Token mit zwei Regex-Aufrufen untersuchen, wobei du den zweiten regex dynamisch mit Hilfe des 3. Submatch des ersten generieren müsstest - oder du könntest die Zeichenzahl, die die Daten benötigen, per Pascal aus dem String kopieren und entfernen, denn dafür ist ja eigentlich kein Pattern Matching nötig.
) Schliesst die Klammer von "(?:" - hier endet die zweite Alternative

Wenn irgendwo in deinem Datenfile Leerstellen vorkommen können, die dich nicht interessieren, dann setzt du an die entsprechende Stelle im Regex noch "\s*". Leerstellen, die in den Submatches landen und die zu ignorieren sind, kannst du am einfachsten danach im Delphi mit der Funktion trim entfernen. Es geht zwar auch direkt im Regex, in Verbindung mit "Lazy"Quantifikatoren ("+?\s*" statt nur "+"), es macht den Ausdruck aber nicht nur komplizierter sondern ist, so viel ich weiß, auch etwas langsamer als ein Matching ohne Lazy Quantifikatoren mit anschließendem Trim. Ist wohl Geschmackssache.
Zuletzt geändert von braunbär am Do 6. Jul 2017, 20:21, insgesamt 2-mal geändert.

pluto
Lazarusforum e. V.
Beiträge: 7180
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von pluto »

Der Regex für deine Ausdrücke schaut so aus:

Sowas schreckt mich immer ab.
MFG
Michael Springwald

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

Ja, muss man lernen. Dann schreckt es nicht mehr ab und spart enorm viel Zeit und Programmieraufwand.

pluto
Lazarusforum e. V.
Beiträge: 7180
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von pluto »

Was die Blockgröße angeht:
Ich habe bei meinen Parsern immer darauf geachtet, dass ich das "Zeichen" nur einmal anschauen muss.
Weil ich wollte es nicht doppelt lesen.

Macht es viel aus, wenn man zum Beispiel 200 Byte einlist und dann erneut "bearbeitet" und zwar vom RAM aus?
Mir ist klar, dass der RAM Deutlich schneller arbeitet als die Langsamen Festplatten.
Aber ich dachte halt, weil man der reihe nach List, müsste der "Festplatten-Lesekopf" auch nur Zeichen um Zeichen gesetzt werden.
MFG
Michael Springwald

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

Wie schon oben gesagt - Das Betriebssystem hält die Daten der Festplatte im Cache, der Lesekopf der Platte wird ganz sicher nicht mehrfach bemüht. Aber auch Betriebsytemaufrufe sind eher ineffizient und langsam, deshalb ist es besser, die Daten im eigenen Programm zu puffern.

"normal" grosse Dateien (bis zu ein paar MB) lese ich immer auf ein mal ein, es ist vom Programm her einfacher und am Zeitverhalten merkt der User gar nichts.

wp_xyz
Beiträge: 4892
Registriert: Fr 8. Apr 2011, 09:01

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von wp_xyz »

pluto hat geschrieben:
Der Regex für deine Ausdrücke schaut so aus:

Sowas schreckt mich immer ab.

Da geht es dir wie mir. Lieber schreibe ich ein paar Zeilen mehr und habe dann alles verstanden, und kann zur Not mit dem Debugger den Parser schrittweise durchlaufen, als stundenlang und frustriert ein fehlendes Zeichen in einem kryptischen RegEx-Ausdruck zu suchen.

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von braunbär »

Da gibt es zum Beispiel diesen wunderbaren online Regex-Tester, mit dem du deine Regexes sehr einfach schrittweise debuggen kannst. Damit findest du jedes fehlende Zeichen im Regex-Ausdruck innerhalb von Minuten - etwas Übung natürlich vorausgesetzt. Unbestritten ist diese Syntax gewöhnungsbedürftig, aber es zahlt sich wirklich aus.

Es geht nicht um "ein paar Zeilen mehr", sondern um sehr viel Code, den du mit einer einzigen Regex-Zeile erschlagen kannst.

wp_xyz
Beiträge: 4892
Registriert: Fr 8. Apr 2011, 09:01

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von wp_xyz »

@DL3AD: Falls dir das mit den PChars nicht ganz geheuer ist, findest du hier noch eine vereinfachte Version des ADIF-Parsers mit Integer als Laufvariablen. Um das Prinzip des Parsers zu zeigen, habe ich nach dem Doppelpunkt im Tag nicht getrennt. Das Programm schreibt jeden Tag und jeden Wert in eine eigene Zeile uns schreibt jeweils "TAG:" bzw. "TEXT:" davor:

Code: Alles auswählen

program Project1;
 
{$mode objfpc}{$H+}
 
const
  Txt =
    '<call:6>VP8STI <qso_date:8>20160122 <time_on:4>2359 <band:3>40M <mode:2>CW <freq:5>7.019'+
    '<station_callsign:6>DL13xyz'+
    '<dxcc:3>240 <iota:6>AN-009'+
    '<rst_sent:3>599 <rst_rcvd:3>599 <lotw_qsl_sent:1>Y <lotw_qsl_rcvd:1>Y <eqsl_qsl_sent:1>N <eqsl_qsl_rcvd:1>N <qsl_sent:1>N <qsl_rcvd:1>Y <eor>'+
    '<call:6>VP8STI <qso_date:8>20160123 <time_on:4>1450 <band:3>17M <mode:2>CW <freq:6>18.079'+
    '<station_callsign:6>DL1uvw'+
    '<dxcc:3>240 <cqz:2>13 <ituz:2>73 <iota:6>AN-009 <tx_pwr:3>100'+
    '<rst_sent:3>599 <rst_rcvd:3>599 <lotw_qsl_sent:1>Y <lotw_qsl_rcvd:1>Y <eqsl_qsl_sent:1>N <eqsl_qsl_rcvd:1>N <qsl_sent:1>N <qsl_rcvd:1>Y <eor>'+
    '<call:6>Z63MED <qso_date:8>20160124 <time_on:4>1517 <band:3>30M <mode:2>CW <freq:6>10.128'+
    '<station_callsign:6>DL9iii <name:5>DAVID'+
    '<dxcc:3>296 <cqz:2>15 <ituz:2>28 <tx_pwr:3>100'+
    '<rst_sent:3>599 <rst_rcvd:3>599 <lotw_qsl_sent:1>Y <lotw_qsl_rcvd:1>N <eqsl_qsl_sent:1>N <eqsl_qsl_rcvd:1>N <qsl_sent:1>N <qsl_rcvd:1>N <eor>';
 
var
  i: Integer;
begin
  i := 1;
  while i <= Length(Txt) do begin
    case Txt[i] of
      '<' : begin // Hier stehen wir auf den '<'
              // Neue Zeile beginnen und "TAG" ausgeben
              WriteLn;
              Write(' TAG: ');
              // Zum nächsten Zeichen gehen
              inc(i);
              // ... und weiterlaufen, bis das Stringende erreicht wird, oder
              // ein '>' kommt
              while (i <= Length(Txt)) and (Txt[i] <> '>') do
              begin
                // dabei gefundenes Zeichen einfach nur ausgeben
                Write(Txt[i]);
                // Weiter mit dem nächsten Zeichen, bis obige Bedingung eintritt.
                inc(i);
              end;
              // Jetzt stehen stehen wir auf dem '>' oder haben das Stringende erreicht.
              // Fall (1) StringEnde --> raus
              if i > Length(Txt) then
                break;
              // Fall (2): Wir stehen auf dem '>'
              // Wir machen weiter in der nächsten Zeile.
              // Da nun ein Textfeld kommt, schreiben wir "TEXT"
              WriteLn;
              Write('TEXT: ');
            end;
      else // Hier stehen wir auf einem anderen Zeichen als '<'. Aufgrund des
           // Ablaufs kann es nur das erste Zeichen nach einem '>' sein
           // Dieses Zeichen schreiben wir einfach nur hin:
           Write(Txt[i]);
    end;
    // Nun sind alle Fälle geprüft, wir gehen zum nächsten Zeichen
    inc(i);
  end;
  WriteLn;
 
  // Fertig
  // Auf Windows ein RETURN, damit der Bilschirm nicht veschwindet
  ReadLn;
end.
Zuletzt geändert von wp_xyz am Do 6. Jul 2017, 22:01, insgesamt 2-mal geändert.

DL3AD
Beiträge: 478
Registriert: Fr 13. Sep 2013, 12:07
OS, Lazarus, FPC: Debian Bullseye (L 2.2.0)
CPU-Target: 64Bit
Wohnort: Rügen

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von DL3AD »

Hallo wp_xyz,

DANKE -
wp_xyz hat geschrieben: Falls dir das mit den PChars nicht ganz geheuer ist
:mrgreen:
Jo dass ist nun für mein Hobby Programming verständlich 8)
Ja, das Prinzip habe ich verstanden und kann mir nun meine proceduren und functionen basteln wie ich sie benötige.
Das war eine super Hilfe von dir !

Gruß Frank

Warf
Beiträge: 1911
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Dateien parsen - mit welchen Werkzeugen ?

Beitrag von Warf »

pluto hat geschrieben:Was die Blockgröße angeht:
Ich habe bei meinen Parsern immer darauf geachtet, dass ich das "Zeichen" nur einmal anschauen muss.
Weil ich wollte es nicht doppelt lesen.

Macht es viel aus, wenn man zum Beispiel 200 Byte einlist und dann erneut "bearbeitet" und zwar vom RAM aus?
Mir ist klar, dass der RAM Deutlich schneller arbeitet als die Langsamen Festplatten.
Aber ich dachte halt, weil man der reihe nach List, müsste der "Festplatten-Lesekopf" auch nur Zeichen um Zeichen gesetzt werden.


Also neben der Festplattenkopfbewegung gibt es auch noch das problem mit den Kontextwechseln. Jeder aufruf von Read/Write (was der Filestream ja intern verwendet) sorgt dafür das zunächst dein Prozess von der CPU runter genommen werden muss, das Betriebsystem einen eigenen Prozess lädt, welcher die Aktion ausführt, sobald der Fertig ist wird dein Prozess wieder gestartet. Das ist super ineffizient, deshalb, wenn du nicht weißt wie groß die Datei ist, solltest du immer einen Kleinen Buffer (1kb - 1mb oder so) einlesen. Ich nehme auf kleinen Rechnern (Raspi, Arduino) für so etwas meist 1024 Byte(chars) und unter meinem Desktop, der dank 64 Bit genug speicher alloziieren kann, und auch genug Ram hat, meist 1-10 mb

Antworten