AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Projekte Vorstellung Unit: File encoding detector

Vorstellung Unit: File encoding detector

Ein Thema von LTE5 · begonnen am 19. Nov 2017 · letzter Beitrag vom 15. Jan 2018
Antwort Antwort
Seite 2 von 4     12 34   
LTE5
Registriert seit: 13. Nov 2017
Ich möchte euch gerne meine Arbeit aus den vergangenen Tagen vorstellen.
Ich bin kein Profi und habe mich erst vor wenigen Tagen in Streams eingelesen.

Ich brauchte eine Lösung, um mehr oder weniger zuverlässig das Encoding einer Datei herausfinden zu können.
Über BOM ist das ja leider nicht immer feststellbar, daher habe ich eine andere Lösung entworfen.

Sie ist weder professionell noch gut, aber sie funktioniert. Es ist mehr oder weniger eine Zusammenstellung aus Dingen die ich im Internet gefunden habe.
Daher bitte ich um Feedback! Ihr könnt die Unit auch gerne kopieren, anpassen und hier im Beitrag wieder posten.

Der Aufruf ist einfach
Delphi-Quellcode:
var IsUnicode: Boolean;
begin
 IsUnicode := TEncodingDetect.IsFileUnicode('pfad-zur-datei.txt');
So ist es auch möglich
Delphi-Quellcode:
 Memo1.Lines.Clear;

 Memo1.Lines.Add('File unicode: ' + BoolToStr(TEncodingDetect.IsFileUnicode('pfad-zur-datei.txt'), True));
 Memo1.Lines.Add('String unicode: ' + BoolToStr(TEncodingDetect.IsTextUnicode('ʥ'), True));
 Memo1.Lines.Add('String unicode: ' + BoolToStr(TEncodingDetect.IsTextUnicode('ABC'), True));
 Memo1.Lines.Add('File content: ' + TFile.ReadAllText('pfad-zur-datei.txt', TEncodingDetect.GetEncoding('pfad-zur-datei.txt')));

Geändert von LTE5 (24. Nov 2017 um 13:32 Uhr)
 
LTE5

 
Delphi 10.2 Tokyo Starter
 
#11
  Alt 19. Nov 2017, 15:48
Wie genau müsste man denn dann nachprüfen?
Einfach bis 255 klingt ja zu einfach.
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

 
Delphi 10.3 Rio
 
#12
  Alt 19. Nov 2017, 16:26
Die Problematik liegt eigentlich in der Unterscheidung zwischen ANSI und UTF-8 ohne BOM. Welche CodePage bei ANSI verwendet werden soll kannst du eh kaum raus finden, wenn du keine Informationen über die Art des Inhaltes hast (manche Zeichen kommen in einer bestimmten Art Text halt nicht vor und sind ein Trigger für eine falsche Codierung). Allerdings beginnen in UTF-8 alle Zeichen > #127 mit einer bestimmten Sequenz. Sind also Zeichen > #127 vorhanden, die nicht mit einer dieser UTF-8 Sequenzen beginnen, handelt es sich offenbar nicht um ein UTF-8 Encoding.

Ein sehr einfacher Ansatz um ANSI und UTF-8 zu unterscheiden wäre z.B. einfach erst mit TEncoding.UTF8 (BOM oder nicht erkennt er automatisch) einzulesen und falls das eine Exception wirft eben mit TEncoding.ANSI zu lesen. Bei dieser Vorgehensweise braucht man auch nicht erst das Encoding ermitteln, sondern bekommt das beim Lesen gleich mit. Das spart ein erneutes Durchlaufen zur eigentlichen Verarbeitung der Daten, denn wozu brauche ich das Encoding der Daten, wenn ich sie danach nicht damit lesen will. Das könnte dann in etwa so aussehen:
Delphi-Quellcode:
function StreamToString(Stream: TStream): string;
var
  enc: TEncoding;
  reader: TStreamReader;
  savePosition: Int64;
begin
  savePosition := Stream.Position;
  try
    { ANSI als letztes, denn das klappt immer } 
    for enc in TArray<TEncoding>.Create(TEncoding.UTF8, TEncoding.ANSI) do begin
      Stream.Position := savePosition;
      reader := TStreamReader.Create(Stream, enc, false);
      try
        try
          result := reader.ReadToEnd;
          { Wenn es geklappt hat, Schleife verlassen }
          Break;
        except
          on EEncodingError do ;
        end;
      finally
        reader.Free;
      end;
    end;
  finally
    Stream.Position := savePosition;
  end;
end;
Uwe Raabe
  Mit Zitat antworten Zitat
Wosi
 
#13
  Alt 19. Nov 2017, 16:27
Für den Fall, dass es keinen BOM gibt:
Gibt es nur Zeichen bis #127, dann ist es sicher den Stream als ASCII-String zu interpretieren.
Liegen einzelne Byte jedoch im Wertebereich zwischen #128 und #255 dann wird es komplizierter. Dann könnte es sich um UTF8 oder ANSI oder ein anderes lokales Format handeln.
Es gibt dabei leider kein 100% sicheres Vorgehen um das korrekte Encoding zu ermitteln. Es existieren lediglich Vorgehensweisen um ein möglichst gutes Ergebnis zu erzielen.

Ich musste mal ein Problem lösen, bei dem eine Software muss regelmäßig entscheiden musste, ob eine Datei UTF8 oder ANSI beinhaltet. So habe ich es damals gelöst:

- Ist ein BOM-Header vorhanden, dann nimm das passende Encoding für den BOM-Header
- Ansonsten untersuche den Bytestream auf gültige UTF8-Multi-Byte-Zeichen. Gibt es eine Byte-Sequenz, die nicht UTF8-Konform ist, dann nimm ANSI. Entspricht der Byte-Stream hingegen bis zum Ende gültigem UTF8, dann nimm UTF8.

Die Definition von Multi-Byte-Zeichen gibt es bei Wikipedia.
Das beschriebene Vorgehen funktioniert generell ganz gut. Allerdings scheitert es, wenn
- wenn sich mindestens ein ungültiges Multi-Byte-Zeichen in einer ansonsten gültigen UTF8-Datei befindet (von Text-Editoren werden diese Zeichen in der Regel als "�" dargestellt)
- wenn der Dateiinhalt weder ANSI noch UTF8 ist
  Mit Zitat antworten Zitat
LTE5

 
Delphi 10.2 Tokyo Starter
 
#14
  Alt 19. Nov 2017, 16:40
Habe wieder viel zu lesen! Melde mich gleich.
Ich habe in der Zwischenzeit Support für Streams eingebaut. Schreibe ich gleich in Beitrag #1.

Zitat:
Das spart ein erneutes Durchlaufen zur eigentlichen Verarbeitung der Daten, denn wozu brauche ich das Encoding der Daten, wenn ich sie danach nicht damit lesen will.
Genau das will ich ja aber. Ich brauche das Encoding, eben damit ich die Daten lesen kann. Sonst würde ich mir die ganze Mühe nicht machen.

Zitat:
Allerdings beginnen in UTF-8 alle Zeichen > #127 mit einer bestimmten Sequenz. Sind also Zeichen > #127 vorhanden, die nicht mit einer dieser UTF-8 Sequenzen beginnen, handelt es sich offenbar nicht um ein UTF-8 Encoding.
Welche Sequenz ist das denn genau und wie prüfe ich danach?

Sollte man demnach hier unten also besser bis 255 prüfen und ab 127 zusätzlich diese Sequenz prüfen?
Delphi-Quellcode:
class function TEncodingDetect.IsStreamUnicode(const Stream: TStream): Boolean;
var
 i: Integer;
 B: Byte;
begin
 Result := False;

 if Stream.Size = 0 then
  Exit;

 for i := 0 to Stream.Size - 1 do
  begin
   Stream.ReadData(B, Sizeof(B));
   Result := Ord(B) > 127;
   if Result then
    Break;
  end;
end;
Zitat:
- Ansonsten untersuche den Bytestream auf gültige UTF8-Multi-Byte-Zeichen. Gibt es eine Byte-Sequenz, die nicht UTF8-Konform ist, dann nimm ANSI. Entspricht der Byte-Stream hingegen bis zum Ende gültigem UTF8, dann nimm UTF8.
Ich werde mich mal dran versuchen und TEncodingDetect.IsStreamUnicode anpassen. Könnte jedenfalls schwer werden, denn von dem Wikipedia-Artikel verstehe ich nichts.

Geändert von LTE5 (19. Nov 2017 um 16:55 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

 
Delphi 10.3 Rio
 
#15
  Alt 19. Nov 2017, 16:56
Genau das will ich ja aber. Ich brauche das Encoding, eben damit ich die Daten lesen kann. Sonst würde ich mir die ganze Mühe nicht machen.
Die gezeigte Funktion liefert dir ja auch den String korrekt decodiert zurück. Den kannst du dann ja weiter verarbeiten ohne dich noch um das Encoding der Datei kümmern zu müssen.

Welche Sequenz ist das denn genau und wie prüfe ich danach?
Kannst du bei Wikipedia nachsehen: Zulässige Bytes und ihre Bedeutung
Uwe Raabe
  Mit Zitat antworten Zitat
LTE5

 
Delphi 10.2 Tokyo Starter
 
#16
  Alt 19. Nov 2017, 16:59
Zitat:
Die gezeigte Funktion liefert dir ja auch den String korrekt decodiert zurück.
Leider bringt mir der String aber nicht, wenn es um das Encoding geht, welches ich bei TMemIniFile brauche.

Zitat:
Kannst du bei Wikipedia nachsehen: Zulässige Bytes und ihre Bedeutung
Auf soviele verschiedene Bytes kann ich doch gar nicht prüfen.
Oder sind das insgesamt nur 6?


Ich habe mich mal so daran versucht. Ist das so richtig?
Delphi-Quellcode:
var i: Integer; B: Byte; Bytes: TBytes;
begin
 Stream.Position := 0;
 for i := 0 to Stream.Size - 1 do
  begin
   SetLength(Bytes, 2);
   Stream.Read(Bytes, Length(Bytes));

   if
    (Bytes = TBytes.Create($C0, $C1)) or
    (Bytes = TBytes.Create($F5, $F7)) or
    (Bytes = TBytes.Create($F8, $FB)) or
    (Bytes = TBytes.Create($FC, $FD)) or
    (Bytes = TBytes.Create($FE, $FF)) then
     ShowMessage('Ungültige Sequenz gefunden.');
    end;
  end;
end;

Geändert von LTE5 (24. Nov 2017 um 13:33 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

 
Delphi 10.3 Rio
 
#17
  Alt 19. Nov 2017, 17:47
Auf soviele verschiedene Bytes kann ich doch gar nicht prüfen.
Oder sind das insgesamt nur 6?
Angenommen du liest jedes Byte nacheinander, dann sind alle Bytes im Bereich $00 - $7F gültige Zeichen. Ist das nicht der Fall, musst du prüfen, ob eine UTF-8 Sequenz folgt. Diese wird eingeleitet von einem Byte im Bereich $C2..$F4 - alles andere wäre ein Fehler. Die Länge der UTF-8 Sequenz wird durch dieses Byte bestimmt. Liegt es im Bereich $C2..$DF folgt ein weiteres Byte, liegt es im Bereich $E0..$EF folgen zwei weitere Bytes und liegt es im Bereich $F0..$F4 folgen drei weitere Bytes. Alle diese weiteren Bytes müssen im Bereich $80..$BF liegen. Nachdem du die entsprechende Menge weiterer Bytes gelesen hast, beginnt das Spiel wieder von vorn. Liegt ein Byte außerhalb des gerade erwarteten Bereichs liegt ein Fehler vor oder es handelt sich nicht um eine UTF-8 Codierung.

Eine Prüfroutine für UTF-8 könnte etwa so aussehen (ungetestet):
Delphi-Quellcode:
function IsUTF8(Bytes: TBytes): Boolean;
var
  B: Byte;
  weitere: 0..3;
begin
  weitere := 0;
  for B in Bytes do begin
    case B of
      $00..$7F: ; { ASCII }
      $80..$BF: begin
        if weitere > 0 then begin
          Dec(weitere);
        end
        else begin
          Exit(False);
        end;
      end;
      $C2..$DF: weitere := 1;
      $E0..$EF: weitere := 2;
      $F0..$F4: weitere := 3;
    else
      Exit(False);
    end;
  end;
  Result := True;
end;
Uwe Raabe
  Mit Zitat antworten Zitat
LTE5

 
Delphi 10.2 Tokyo Starter
 
#18
  Alt 19. Nov 2017, 17:54
Spätestens hier resigniere ich.

Ich muss mir das heute Abend mal in Ruhe angucken. Speziell dieses weitere mit inkrementieren und dekrementieren.

Ist es möglich von irgendeiner Quelle UTF-8-Dateien mit absichtlichen Fehlern zu bekommen?

Geändert von LTE5 (19. Nov 2017 um 18:00 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von himitsu
himitsu

 
Delphi 10.2 Tokyo Professional
 
#19
  Alt 19. Nov 2017, 17:58
Eine Prüfroutine für UTF-8 könnte etwa so aussehen (ungetestet)
Billigvariante: (testet nicht nur, sondern wandelt um und braucht zusätzlichen etwas RAM)
Delphi-Quellcode:
function IsUTF8(Bytes: RawByteString{oder TBytes}): Boolean;
begin
  Result := {(Bytes = '') and} (UTF8ToString(Bytes) <> ''); // inkl. dem Auskommentierten, wird auch ein Leerstring als UTF-8 erkannt, auch wenn "garnichts" im String ist.
end;
Der Code in der System-Unit wirft keine Exception, sondern gibt einfach nichts zurück (Leerstring), wenn es man kein valides UTF-8 rein gibt.
  Mit Zitat antworten Zitat
Wosi
 
#20
  Alt 19. Nov 2017, 18:06
Eine Prüfroutine für UTF-8 könnte etwa so aussehen (ungetestet):
Vom Ansatz her gut aber du prüfst nicht, ob eine Multibyte-Sequenz vorzeitig abgebrochen wird.
Ebenfalls ungetesteter Fix:

Delphi-Quellcode:
function IsUTF8(Bytes: TBytes): Boolean;
var
  B: Byte;
  weitere: 0..3;
begin
  weitere := 0;
  for B in Bytes do begin
    case B of
      $00..$7F: if weitere > 0 then Exit(False); { ASCII }
      $80..$BF: begin
        if weitere > 0 then begin
          Dec(weitere);
        end
        else begin
          Exit(False);
        end;
      end;
      $C2..$DF: if weitere > 0 then Exit(False) else weitere := 1;
      $E0..$EF: if weitere > 0 then Exit(False) else weitere := 2;
      $F0..$F4: if weitere > 0 then Exit(False) else weitere := 3;
    else
      Exit(False);
    end;
  end;
  Result := True;
end;
  Mit Zitat antworten Zitat
Themen-Optionen Thema durchsuchen
Thema durchsuchen:

Erweiterte Suche
Ansicht

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 01:02 Uhr.
Powered by vBulletin® Copyright ©2000 - 2019, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2019 by Daniel R. Wolf