Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Zeilenumbruch ersetzen Algorithmus (https://www.delphipraxis.net/195571-zeilenumbruch-ersetzen-algorithmus.html)

hansklok 10. Mär 2018 14:18

Zeilenumbruch ersetzen Algorithmus
 
Hallo liebe Community,

ich lese teils sehr große Textdateien häppchenweise via Chunks mittels TFileStream ein. Später parse ich Zeile für Zeile. Nun gibt/gab es ja verschiedene Betriebssysteme, die unterschiedlich mit Zeilenumbrüchen umgehen. Ich gebe Zeilenumbrüche in Dateien nur noch mit dem Hex-Wert „0A“ aus. Es gibt aber auch die Kombination „0D0A“ und „0D“.

Ich bin jetzt kein Programmierprofi. Wie kann man mittels TFileStream nach den Kombinationen „0D0A“ (2 Bytes) und „0D“ (1 Byte) suchen und diese durch „0A“ ersetzen? Ich denke das ganze über TFileStream zu lösen ist wesentlich schneller und ressourcenschonender, als String Replace Methoden zu verwenden. Grade bei einer großen Textdatei von knapp 700 Megabyte.

Ich hoffe, ich habe meine Frage nachvollziehbar formuliert.

Viele Grüße

Uwe Raabe 10. Mär 2018 14:48

AW: Zeilenumbruch ersetzen Algorithmus
 
Schau dir mal in Sysutils die Function
Delphi-Quellcode:
AdjustLineBreaks
an. Die arbeitet zwar auf
Delphi-Quellcode:
string
, aber intern kommen auch nur
Delphi-Quellcode:
PChar
zum Einsatz. Das Verfahren lässt sich relativ leicht auf
Delphi-Quellcode:
Streams
umbauen. Du musst halt zusätzlich auf das Encoding achten.

p80286 10. Mär 2018 21:43

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von hansklok (Beitrag 1395732)
Ich denke das ganze über TFileStream zu lösen ist wesentlich schneller und ressourcenschonender, als String Replace Methoden zu verwenden.

Was stellst Du Dir darunter vor?
Es geht kein Weg daran vorbei, die zu verarbeitenden Daten in den Speicher zu laden. Die Frage ist "nur" wie groß die Happen sind die verarbeitet werden.

Gruß
k-H

KodeZwerg 10. Mär 2018 22:34

AW: Zeilenumbruch ersetzen Algorithmus
 
Ich weiß ja nicht wie Deine Ausgabe der Text-Datei vonstatten geht, erstellt dein Parser einen String pro Zeile?
Falls ja, check doch lediglich die letzten 2 zeichen nach deinen Wert und ersetz das dann.
Nachtrag:
Bzw. implementier in deinem Parser diese Zusatzfunktion, da du ja bereits eine Zeile in irgendeiner art und weise besitzt check die letzten 2 bytes.
Ein Einblick in deinen Parser wäre hilfreich.
Weiterer Nachtrag:
Delphi-Quellcode:
//hier ein mini Beispiel für Strings, mit bissl justierung auch für PChar's bzw alles wo man "position + wert" nutzt, daß Prinzip bleibt immer das selbe
Function CalibrateString( EineParserZeile : String ) : String;
begin
 // letzte 2 zeichen nach 0D0A checken
 if Length(EineParserZeile) > 1 then // falls man einen minimum längen-check einbaut wird's sicherer. optional.
  if ((EineParserZeile[Length(EineParserZeile)-1] = char($0D)) and (EineParserZeile[Length(EineParserZeile)] = char($0A))) then
  begin
   EineParserZeile[Length(EineParserZeile)-1] := char($0A);
   // hier fehlt ein aufruf um das letzte zeichen zu entfernen, ich hab grad kein delphi parat aber für Strings würd ich einen Copy() Befehl verwenden.
  end;
 // letztes zeichen nach 0D checken
 if Length(EineParserZeile) >= 1 then // für mehr Speed kann man auch die ausführung oben mit einbauen aber falls tatsächlich mal eine Zeile mit nur einem char($0D) reinkommt wird die nicht verarbeitet
  if EineParserZeile[Length(EineParserZeile)] = char($0D) then EineParserZeile[Length(EineParserZeile)] := char($0A);
 // Rückgabe an dein Programm
 Result := EineParserZeile;
end;

//ps: als "Case" kann man das ganze auch verpacken.

Delphi.Narium 10. Mär 2018 23:11

AW: Zeilenumbruch ersetzen Algorithmus
 
grober Vorschlag:

zwei Streams nehmen:

1. Stream = Input
2. Stream = Output

1. Stream byteweise (oder charweise) lesen.
2. jedes Zeichen abfragen, ob es in den Output soll.
3. Zeichen schreiben oder verwerfen.

ungefähr (ungetestet) sowas:
Delphi-Quellcode:
procedure TIrgendeineKlasse.Zeichenaustauschen(input : TMemoryStream; output : TMemoryStream);
Var
  ch        : Char;
begin
  input.Position := 0;
  output.Position := 0;
  // Ist überhaupt was in der Datei drin?
  if input.Read(ch, 1) > 0 then begin
    repeat
      case ch of
        // Hier zeichenweise entscheiden, was geschehen soll.
        #10 : begin
                output.Write(ch,1);
              end;
        #13 : begin
                // output.Write(ch,1);
              end;
      else
        // Übrige Zeichen ausgeben.
        output.Write(ch,1);
      end;
    until input.Read(ch, 1) = 0;
    input.Position := 0;
    output.Position := 0;
  end;
end;
Das sollte auch bei größeren Datenmengen noch vertretbar schnell sein.

hansklok 11. Mär 2018 12:51

AW: Zeilenumbruch ersetzen Algoritmud
 
Vielen Dank schon einmal für die vielen Beitröge. Ich versuche darauf einzugehen. Wie gesagt, ich bin kein Profi und mir geht es eher um einen Aufbau des Algorithmus.

Zitat:

Zitat von p80286 (Beitrag 1395754)
Es geht kein Weg daran vorbei, die zu verarbeitenden Daten in den Speicher zu laden. Die Frage ist "nur" wie groß die Happen sind die verarbeitet werden.

Das weiß ich ja. Deswegen lade ich mit einer Schleife immer die Chunks und verarbeite sie. In dem Fall so,Ute ich dann das Häppchen in Zeilen. Und da ich nur so,irrem wenn das Zeilenende „0A“ als Umbruch habe, muss ich eben vor diesem ganzen Vorgang die eventuellen, für mich ungewollten, Zeilenumbrüche durch „0A“ ersetzen. Und das ist eben meine Frage, wie ein solcher Algorithmus auf Basis von TFileStream aussehen kann, wo man z.B. einfach eine modifizierte Kopie der originalen Datei erstellt mit den ersetzten Zeilenumbrüchen.
Zitat:

Zitat von KodeZwerg (Beitrag 1395757)
Ich weiß ja nicht wie Deine Ausgabe der Text-Datei vonstatten geht, erstellt dein Parser einen String pro Zeile?

Genau der Parser arbeitet Zeilenweise. Es ist eben nur doof, wenn er auf eine Zeile eines falsch gesplitteten Strings zurück greift. Und wie gesagt, ich möchte ja erst im zweiten Durchgang splitten, wenn ich weiß, dass alle Ueilenenden der Datei „0A“ sind bzw. durch diese ersetzt wurden.
Zitat:

Zitat von Delphi.Narium (Beitrag 1395758)
ungefähr (ungetestet) sowas:
Delphi-Quellcode:
procedure TIrgendeineKlasse.Zeichenaustauschen(input : TMemoryStream; output : TMemoryStream);
Var
  ch        : Char;
begin
  input.Position := 0;
  output.Position := 0;
  // Ist überhaupt was in der Datei drin?
  if input.Read(ch, 1) > 0 then begin
    repeat
      case ch of
        // Hier zeichenweise entscheiden, was geschehen soll.
        #10 : begin
                output.Write(ch,1);
              end;
        #13 : begin
                // output.Write(ch,1);
              end;
      else
        // Übrige Zeichen ausgeben.
        output.Write(ch,1);
      end;
    until input.Read(ch, 1) = 0;
    input.Position := 0;
    output.Position := 0;
  end;
end;

Die Schleife geht ja Byteweise vor. Wenn ich jetzt auf #13 treffe, was muss ich dann machen?

Delphi.Narium 11. Mär 2018 14:28

AW: Zeilenumbruch ersetzen Algorithmus
 
Im Case kannst Du jeweils das zuletzt gelesene Zeichen abfragen.

Findest Du nun eine #13 und willst sie nicht ausgeben, so wird sie halt nicht geschrieben.

Oder anders ausgedrückt:

In der Else werden alle die Zeichen ausgegeben, die nicht vorher im Case irgendwie verarbeitet wurden.

Soll also z. B. die #13 nie ausgegeben werden, sähe der Quelltext so aus:
Delphi-Quellcode:
procedure TIrgendeineKlasse.Zeichenaustauschen(input : TMemoryStream; output : TMemoryStream);
Var
  ch : Char;
begin
  input.Position := 0;
  output.Position := 0;
  // Ist überhaupt was in der Datei drin?
  if input.Read(ch, 1) > 0 then begin
    repeat
      case ch of
        #13 : ; // Das Zeichen wird schlicht und einfach ignoriert.
      else
        output.Write(ch,1); // Übrige Zeichen ausgeben.
      end;
    until input.Read(ch, 1) = 0;
    input.Position := 0;
    output.Position := 0;
  end;
end;
Oder die Antwort auf deine Frage Wenn ich jetzt auf #13 treffe, was muss ich dann machen? ist: Nichts.

Alle anderen Zeichen werden ausgegeben.

Uwe Raabe 11. Mär 2018 15:03

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von Delphi.Narium (Beitrag 1395793)
Oder die Antwort auf deine Frage Wenn ich jetzt auf #13 treffe, was muss ich dann machen? ist: Nichts.

Und was ist, wenn auf das #13 kein #10 folgt?

Zitat:

Es gibt aber auch die Kombination „0D0A“ und „0D“.

Delphi.Narium 11. Mär 2018 15:33

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1395794)
Und was ist, wenn auf das #13 kein #10 folgt?

Dann muss man die Logik im Case an die eigenen Bedürfnisse anpassen.
Delphi-Quellcode:
procedure TIrgendeineKlasse.Zeichenaustauschen(input : TMemoryStream; output : TMemoryStream);
Var
  ch1 : Char;
  ch2 : Char;
begin
  input.Position := 0;
  output.Position := 0;
  // Ist überhaupt was in der Datei drin?
  if input.Read(ch1, 1) > 0 then begin
    repeat
      case ch of
        #13 : begin // Das nächste Zeichen lesen.
                if input.Read(ch2, 1) <> 0 then begin
                  case ch2 of
                    #10 : ; // ignorieren, wird als nächstes Zeichen gelesen.
                  else
                    begin // wenn wir 'ne #13 erhalten und keine #10 folgt, geben wir 'ne #10 aus.
                      ch2 := #10;
                      output.Write(ch2,1);
                    end;
                  end;
                  // und wieder ein Zeichen zurück.
                  input.Position := input.Position - 1;
                end;
              end;
      else
        output.Write(ch1,1); // Übrige Zeichen ausgeben.
      end;
    until input.Read(ch1, 1) = 0;
    input.Position := 0;
    output.Position := 0;
  end;
end;

Uwe Raabe 11. Mär 2018 16:20

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von Delphi.Narium (Beitrag 1395796)
Delphi-Quellcode:
procedure TIrgendeineKlasse.Zeichenaustauschen(input : TMemoryStream; output : TMemoryStream);
Var
  ch1 : Char;
  ch2 : Char;
begin
  input.Position := 0;
  output.Position := 0;
  // Ist überhaupt was in der Datei drin?
  if input.Read(ch1, 1) > 0 then begin
    repeat
      case ch of
        #13 : begin // Das nächste Zeichen lesen.
                if input.Read(ch2, 1) <> 0 then begin
                  case ch2 of
                    #10 : ; // ignorieren, wird als nächstes Zeichen gelesen.
                  else
                    begin // wenn wir 'ne #13 erhalten und keine #10 folgt, geben wir 'ne #10 aus.
                      ch2 := #10;
                      output.Write(ch2,1);
                    end;
                  end;
                  // und wieder ein Zeichen zurück.
                  input.Position := input.Position - 1;
                end;
              end;
      else
        output.Write(ch1,1); // Übrige Zeichen ausgeben.
      end;
    until input.Read(ch1, 1) = 0;
    input.Position := 0;
    output.Position := 0;
  end;
end;

Der Code hat immer noch ein paar Probleme (unter der Annahme, daß hier mindestens das im Profil angegeben Delphi 2010 zum Einsatz kommt):

1. Auf der einen Seite werden ch1 und ch2 als Char (2 Bytes) deklariert, mit Read wird aber nur jeweils 1 Byte eingelesen. Damit ist das höhere Byte undefiniert und das case könnte daneben gehen. Da wir ja hier alle wohl von eine ANSI- bzw. UTF8-codierten Textdatei ausgehen, würde ich direkt mit Byte (oder AnsiChar) arbeiten.

2. Der Code unterschlägt das letzte Zeilenende, wenn dieses durch ein einzelnes #13 gekennzeichnet ist.

3. Eine Unicode-Datei könnte man damit nicht direkt umsetzen, da der komplette Bereich 0D00-0DFF sowie alle Zeichen mit 0D im Low-Byte falsch behandelt werden. Ich vermute aber, daß dies hier nicht relevant sein wird (siehe 1).

KodeZwerg 11. Mär 2018 16:36

AW: Zeilenumbruch ersetzen Algorithmus
 
Noch eine Idee, aber ich weiß nicht ob alle Delphi Versionen das mitmachen,
deine Text-Datei einfach per ReadLn() einlesen, da ist automatisch schluss, egal wie die zeichenfolge ist.
also im BeginUpdate modus per schleife alles in deine memobox laden, dann passts.
Bei bedarf memo-box inhalt zurück auf platte schreiben, dann ist format einheitlich egal wie's vorher war.

Delphi.Narium 11. Mär 2018 16:40

AW: Zeilenumbruch ersetzen Algorithmus
 
@Uwe Raabe

Es geht mir nicht um einen allumfassenden, immer und überall, jederzeit in allen Umgebungen und mit allen Delphiversionen funktionierenden Code, sondern um einen Vorschlag zur Problemlösung.

Selbstdenken und anpassen ist also durch erlaubt, erwünscht und im Rahmen des Möglichen.

Wenn Char nicht ein Byte ist, dann muss man das halt anpassen.

Statt der 1 könnte man halt eben auch SizeOf(ch1) nehmen oder SizeOf(EbenDerTypDenManBenutzt).

KodeZwerg 13. Mär 2018 08:44

AW: Zeilenumbruch ersetzen Algorithmus
 
Ich finde es dennoch ineffektiv eine neue und vor allem byte-weise arbeitende Prozedur anzuwenden.
Wenn doch alles bereits bestens funktioniert nur halt die Ausgabe rumzickt dann arbeite ich am Parser und geh nicht im Vorfeld die genannten 700MB komplett (oder chunk-weise) durch um bytes zu ersetzen/entfernen was ja dann eh wieder an Parser geschickt wird der ja namentlich sowas regeln sollte, oder?

hansklok 13. Mär 2018 09:41

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von KodeZwerg (Beitrag 1395801)
Noch eine Idee, aber ich weiß nicht ob alle Delphi Versionen das mitmachen,
deine Text-Datei einfach per ReadLn() einlesen, da ist automatisch schluss, egal wie die zeichenfolge ist.
also im BeginUpdate modus per schleife alles in deine memobox laden, dann passts.

Das hatte ich versucht, ist aber viel langsamer, als alles in einem Wisch als String einzulesen und dann in ein Array zu splitten. Das funktioniert eben nur korrekt, wenn man eine mit „0A“ kodierten Zeilenumbrüchen Datei vorliegen hat.

Zitat:

Zitat von KodeZwerg (Beitrag 1395914)
Ich finde es dennoch ineffektiv eine neue und vor allem byte-weise arbeitende Prozedur anzuwenden.
Wenn doch alles bereits bestens funktioniert nur halt die Ausgabe rumzickt dann arbeite ich am Parser und geh nicht im Vorfeld die genannten 700MB komplett (oder chunk-weise) durch um bytes zu ersetzen/entfernen was ja dann eh wieder an Parser geschickt wird der ja namentlich sowas regeln sollte, oder?

Nochmal zur Aufklärung, die Parserklasse erwartet nur einen einzeiligen String, der dann mittels Regulärem Ausdruck in Einzelteile gesplittet wird. Heißt, den Parser interessiert ein Zeilenumbrüchen nicht. Er erwartet einen bereits korrekt gesplitteten String. Deswegen muss ich ja zwei Durchläufe machen. Einen zum splitten der Zeilen der Datei in ein Array und einen, der das Array Zeilenweise parst.
Mir fällt keine andere Taktik ein, als eine Art Präprozessor, der erst alle Zeilenumbrüche auf „0A“ vereinheitlicht, dann im zweiten Durchlauf alle Zeilen mit „0A“ am Ende in ein Zeilen-Array splittet und dann jede Zeile parst.

KodeZwerg 13. Mär 2018 10:07

AW: Zeilenumbruch ersetzen Algorithmus
 
Danke für Aufklärung, nun sieht die Lage für mich anders aus und byte-check ist absolut Ok.
Mein Ablauf wäre wie folgt:
Datei puffern,
Byte-Check starten,
innerhalb des checks gleich einen fertigen String anfertigen anstelle einen neuen Stream zu erschaffen,
(das erspart dir ein erneutes Suchen nach $0A, in theorie könntest Du sogar den check einzeln auf $0A und $0D beschränken,
also bei jedem 0A oder 0D den String als fertig betrachten und eher auf die Länge des produzierten Strings reagieren um Leerzeilen zu vermeiden, nachteil: bewusste Leerzeilen fehlen dann)
fertige Zeile eventuell mit TStringList verwalten (virtuell puffern) oder gleich an Bestimmungsort senden,
Parser weglassen da bereits alles fertig ist.

Ps: Im Nachhinein, als ich Nachricht bereits abgeschickt hatte, fiel mir auch ein, ReadLn() ist ja LowLevel langsame Routine, sorry dafür!

himitsu 13. Mär 2018 11:33

AW: Zeilenumbruch ersetzen Algorithmus
 
Zitat:

Zitat von KodeZwerg (Beitrag 1395801)
Noch eine Idee, aber ich weiß nicht ob alle Delphi Versionen das mitmachen,
deine Text-Datei einfach per ReadLn() einlesen, da ist automatisch schluss, egal wie die zeichenfolge ist.
also im BeginUpdate modus per schleife alles in deine memobox laden, dann passts.
Bei bedarf memo-box inhalt zurück auf platte schreiben, dann ist format einheitlich egal wie's vorher war.

Ihhh.

TStringList: Lesen tut die Alles (LoadFromXyz und .Text) und geschreiben wird, was in TStringList.LineBreak steht.
Aber TMemo ersetzt auch alles, gleich beim Zuweisen an .Text

PS: Delphi-Referenz durchsuchenAdjustLineBreaks
Und das sucht übrigens auch doppelt und bytweise.
* einmal um die Länge des Ergebnisses zu berechnen
* und dann nochmal inkl. Umkopieren
Denn das Suchen und nur eine Speicherreservierung ist wesentlich schneller, als mehrere Reservierungen+umkopieren während der Suche.

Ach ja, ich hatte mir mal eine Ersetzenklasse geschrieben, welche die Operationen sammelt, in einem statischen Array und dann zusammen ausführt, wenn der Puffer voll oder die Arbeit beendet ist.
Muß man auch nur einmal suchen, aber bearbeitet nicht jeden Schritt einzeln.

hansklok 13. Mär 2018 21:44

AW: Zeilenumbruch ersetzen Algorithmus
 
Danke für das zahlreiche Feedback.
Zitat:

Zitat von KodeZwerg (Beitrag 1395926)
Datei puffern

Was meinst Du mit puffern?
Zitat:

Zitat von KodeZwerg (Beitrag 1395926)
Byte-Check starten, innerhalb des checks gleich einen fertigen String anfertigen anstelle einen neuen Stream zu erschaffen, (das erspart dir ein erneutes Suchen nach $0A, in theorie könntest Du sogar den check einzeln auf $0A und $0D beschränken, also bei jedem 0A oder 0D den String als fertig betrachten und eher auf die Länge des produzierten Strings reagieren um Leerzeilen zu vermeiden, nachteil: bewusste Leerzeilen fehlen dann) fertige Zeile eventuell mit TStringList verwalten (virtuell puffern) oder gleich an Bestimmungsort senden, Parser weglassen da bereits alles fertig ist.

Oje, jetzt wird’s kompliziert für mich, wie meinst Du das? Leerzeichen gibt es in den Dateien nicht.

KodeZwerg 15. Mär 2018 11:01

AW: Zeilenumbruch ersetzen Algorithmus
 
Nun ist mir wieder eingefallen in welchen Zusammenhang ich ReadLn() verwendete so das es auch noch relativ flott war.

hier ein mini beispiel:
Delphi-Quellcode:
begin
 test := TStreamReader.Create('C:\Test\Temp\Delphi\DP\Testing\Textfile.txt'); // hier kann auch ein dein puffer rein (TStream/Memory/File) damit es schneller abläuft
// wenn im puffer-modus, musst du noch eine methode einbauen die die letzten zeichen vor dem ende sich "merkt" und beim nächsten puffer-laden vorneweg einbindet
// LESE UNBEDINGT VORHER DIE DELPHI HILFE UM ZWISCHEN ASCII UND UTF-8 DATEIEN ZU UNTERSCHEIDEN, im normalfall wird ascii (0-127) verwendet (ich glaube so etwas gelesen zu haben)
 try
  while not(test.EndOfStream) do // in diesem beispiel verarbeite den TStreamReader.Create komplett bis zum ende der datei
   begin
    Memo1.Lines.Add(test.ReadLine); // das hier ist nur platzhalter damit man weiß wie man ReadLine benutzt
// wenn du RichEdit anstelle von Memo nimmst, wird es ein wenig schneller
// Memo konvertiert manchmal alles nochmal um und verbraucht damit 2-4x soviel speicher bei gleicher datenmenge
// hier könntest du zum beispiel 100 zeilen in eine tstringlist o.ä. sammeln/puffern
// die strings eventuell noch bearbeiten
// und dann alle 100 auf einen rutsch in die anzeige mit beginupdate/endupdate zu casten
   end;
 finally
  test.Free;
 end;
end;
// dieses beispiel geht von richtigen input/output angaben aus und besitzt deswegen keinerlei weiterer checks
// try -> finally reichte mir zum testen aus
Zu Deinen Fragen:
1. Mit Puffern meine ich das was Du bereits machst, dir einen Teil vom ganzen Puffern (TStream).
2.1 Ich meinte Leerzeilen, nicht Leerzeichen

2.2 Die Zeile hier drüber ist eine Leerzeile und überall wo nichts steht ist ein Leerzeichen, das macht einen großen Unterschied ;-)


Alle Zeitangaben in WEZ +1. Es ist jetzt 13:32 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz