Technischer Artikel

DBF, SYLK und DIF exportieren ohne eine Codepage im File

Chinesische Stadtnamen, nach .dbf exportiert auf einem westlichen PC, können als Fragezeichen zurückkommen aus einem Export, der Erfolg meldete, denn dBASE-, SYLK- und DIF-Dateien haben nirgends Platz, eine Kodierung zu vermerken. Ein sicherer Writer benutzt die Host-ANSI-Codepage und prüft jeden Namen und jeden Wert dagegen, bevor das erste Byte fällt; HotXLS verweigert alles, was diese Seite nicht halten kann

Die Host-Codepage ist die einzige Kodierung, die diese Dateien haben

Alle drei Formate stammen aus den frühen PC-Jahren: dBASE von Ashton-Tate, SYLK aus Microsofts Multiplan-Ära, DIF von Software Arts, der VisiCalc-Firma. Jedes speichert Text als einzelne Bytes und setzt voraus, dass der Reader die Codepage des Writers teilt — eine faire Wette, solange jede Maschine im Büro dasselbe DOS-Setup fuhr. Auf Windows ist diese Seite heute das, was GetACP zurückgibt: 1252 auf einer westeuropäischen Installation, 936 auf vereinfachtem Chinesisch, 1251 auf Kyrillisch. dBASE IV und FoxPro haben später eine Language-Driver-Markierung an Byte 29 des Tabellen-Headers vorgesehen, aber das dBASE-III-Layout, das HotXLS schreibt, lässt dieses Byte auf null, und SYLK, DIF und der leertenausgerichtete .prn-Text haben dafür überhaupt kein Header-Feld. Die Codepage wird also außerhalb der Datei vereinbart. HotXLS benutzt die Host-ANSI-Codepage beim Schreiben und beim Lesen und prüft vor dem Schreiben, ob jedes Zeichen ein Byte in dieser Seite hat

FormatEndung bei SaveAs / OpenIm File deklarierte KodierungText, den die Host-Seite nicht halten kann
dBASE-III-Tabelle.dbf, plus eine .dbt-Memo-Datei bei BedarfNein (Byte 29 bleibt null)EXlsDbf wird vor dem Schreiben geworfen, und die gerade erzeugten .dbf / .dbt werden gelöscht
SYLK.slk, .sylkNeinSaveAs gibt -1 zurück und erzeugt keine Datei
DIF.difNeinSaveAs gibt -1 zurück und erzeugt keine Datei
Formatierter Text.prnNeinSaveAs gibt -1 zurück und erzeugt keine Datei

Zehn Bytes für einen dBASE-Feldnamen, gezählt in der Codepage

Jede Größenbegrenzung in einer dBASE-Tabelle ist eine Byte-Zahl. Dort beißen Multibyte-Codepages zu. Ein Feld-Descriptor hält den Namen in elf Bytes — zehn für den Namen, eins für seinen abschließenden NUL — und stellt den Typ-Buchstaben direkt dahinter an Offset 11, die Breite an 16 und die Dezimalstellen an 17. Unter Codepage 936 ist ein chinesisches Zeichen zwei Bytes, also ist ein zehnzeichiger chinesischer Name zwanzig. Kopiert man diese Bytes ein, ohne sie zu messen, laufen sie gerade durch den NUL, den Typ-Buchstaben und die Breite durch; kein Reader kann diesen Descriptor danach wiederherstellen, obwohl der Name eine Zehn-Zeichen-Prüfung bestanden hat. Halten Sie dBASE-Feldnamen ASCII, auch auf einer CJK-Workstation. xBase-Tools, ODBC-Treiber und GIS-Software, die Shapefile-Attributtabellen lesen, hängen alle an diesen Namen, und ein Name, der nur unter 936 dekodiert, wird auf jeder Maschine mit anderer Seite zu anderen Bytes

HotXLS dBASE-III-DBF-Feld-Descriptor-Layout in Delphi: ein Zehn-Byte-Name-Slot plus NUL, Typ-Buchstabe an Offset 11, Breite an 16 und Dezimalstellen an 17, wo AddField('CITY') passt, aber ein zehnzeichiger chinesischer Name unter Codepage 936 über zwanzig Bytes hinausläuft
Innerhalb eines 32-Byte-dBASE-Feld-Descriptors läuft ein chinesischer Feldname, der eine Zehn-Zeichen-Prüfung besteht, unter Codepage 936 in den NUL, den Typ-Buchstaben und die Breite über
BytesdBASE-III-Inhalt
0Version: 0x03, oder 0x83, wenn eine .dbt-Memo-Datei die Tabelle begleitet
1–3Letztes Update: Jahr seit 1900, Monat, Tag
4–7Record-Anzahl, 32-Bit little-endian
8–11Header-Länge und Record-Länge, 16-Bit-Wörter
ab 32Ein 32-Byte-Descriptor pro Feld, dann 0x0D; Records folgen, jeder hinter einem Deletion-Flag

Breiten sind ebenfalls Bytes. Ein Zeichenfeld (C) schließt bei 254 Bytes ab, N-Werte sind rechtsbündiger Text mit den deklarierten Dezimalstellen, D-Werte sind acht Zeichen in YYYYMMDD-Form, und ein zu breiter Wert für sein Feld wird zu einer Reihe Sternchen, der Konvention, die dBASE selbst benutzt. Längerer Text wandert in ein Memo-Feld (M), das eine Blocknummer in den .dbt-Begleiter hält: ein 512-Byte-Header, dann 512-Byte-Blöcke, jedes Memo beendet von 0x1A 0x1A. TXLSXWorkbook.SaveAs mit einem .dbf-Namen macht die erste benutzte Zeile des aktiven Sheets zu Feldnamen, leitet pro Spalte C, N, L oder D her, fällt für gemischte Spalten auf C zurück und verschiebt Text über 254 Zeichen nach M. Sollen die Breiten in Bytes kontrolliert werden, bauen Sie die Tabelle selbst mit TXlsDbfTable und bemessen jedes C-Feld nach der kodierten Länge seines längsten Werts

uses
  Windows, SysUtils, Variants, lxDbf;

procedure WriteCustomerTable(const FileName: string);
var
  Table: TXlsDbfTable;
  Row: Integer;
begin
  Table := TXlsDbfTable.Create;
  try
    // ASCII-Namen von höchstens zehn Bytes; Breiten sind Bytes in der Host-Codepage
    Table.AddField('CUSTNO', 'N', 6, 0);
    Table.AddField('CITY', 'C', 40, 0);
    Table.AddField('BALANCE', 'N', 12, 2);
    Table.AddField('OPENED', 'D', 8, 0);

    Row := Table.AddRow;
    Table.SetValue(Row, 0, 1001);
    Table.SetValue(Row, 1, WideString(#$5317#$4EAC#$5E02));  // 北京市, 6 Bytes in CP936
    Table.SetValue(Row, 2, 1234.5);
    Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));

    try
      XlsWriteDbfFile(FileName, Table);
    except
      on E: EXlsDbf do
        // Erwartet auf einem 1252-Host: die Meldung nennt Feld "CITY" und Zeile 1,
        // und keine .dbf bleibt auf der Platte
        raise Exception.CreateFmt('DBF export refused under code page %d: %s',
          [GetACP, E.Message]);
    end;
  finally
    Table.Free;
  end;
end;

Erwartet: Auf einem 936-Host wird die Tabelle geschrieben und öffnet sich wieder mit allen vier Feldern. Auf einem 1252-Host wirft XlsWriteDbfFile eine EXlsDbf, die Feld und die 1-basierte Zeile nennt. XlsWriteDbfStream konvertiert jeden Feldnamen und jeden C- oder M-Wert zur Host-Seite und zurück, vergleicht und misst jeden Namen gegen seine zehn Bytes, bevor der Header rausgeht, also lässt eine abgelehnte Tabelle den Stream unangetastet. Die Datei-Form geht einen Schritt weiter und löscht die .dbf, und jede .dbt, die sie schon erzeugt hatte

Wie prüft man, ob ein String die Host-Codepage überlebt?

Konvertieren Sie ihn zu AnsiString, konvertieren Sie das zurück zu WideString, und vergleichen Sie mit dem Original. Ein Zeichen, das die Seite nicht kennt, kommt als Ersatz zurück, meist ?, also scheitert der Vergleich genau dann, wenn der Export Daten verlieren würde. Die Writer benutzen dieselbe Konversion, also können Prüfung und Writer sich über die Codepage nicht uneinig werden. Halten Sie auch die Byte-Länge fest: Sie ist die Zahl, die man gegen einen Zehn-Byte-Namen-Slot oder eine Feldbreite hält

uses
  Variants;

// True, wenn jedes Zeichen von V die Host-ANSI-Codepage überlebt;
// ByteLen ist die kodierte Länge zum Vergleichen mit einer Feldbreite
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
  W: WideString;
  A: AnsiString;
begin
  W := VarToWideStr(V);   // VarToStr kann konvertieren, bevor diese Prüfung den Text sieht
  A := AnsiString(W);
  ByteLen := Length(A);
  Result := WideString(A) = W;
end;

Der Kommentar zu VarToWideStr trägt die eigentliche Lektion. In Delphi 2007 und früher ist string überall AnsiString, und unter Free Pascal wird die Unit Variants mit dem eigenen string-Typ des RTL kompiliert, AnsiString, ganz gleich, welchen Mode Ihre Unit benutzt. Auf diesen Compilern hat VarToStr bereits ein ? dort gesetzt, wo jedes chinesische Zeichen war. Eine Absicherung auf AnsiString(VarToStr(V)) vergleicht dann Fragezeichen mit Fragezeichen und akzeptiert alles. TStringList hat dort dasselbe Problem in anderer Gestalt: Weist man ihrer Text-Property breiten Text zu, um Zeilen zu splitten, geht jedes Zeichen auf dem Weg hinein durch die Host-Codepage. XlsParseSlk, XlsParseDifText und XlsParsePrnText nehmen WideString-Input und behalten jedes Zeichen davon auf jedem Host. Falls Sie auch mit Lazarus bauen, siehe welchen String-Typ die HotXLS-Units unter Free Pascal kompilieren

HotXLS Host-ANSI-Codepage-Round-Trip-Prüfung für DBF-, SYLK-, DIF- und PRN-Export in Delphi: AnsiString und zurück zu WideString hält chinesischen Text auf einem 936-Host, ergibt aber Fragezeichen auf 1252, abgelehnt mit EXlsDbf oder SaveAs, das -1 zurückgibt
Jeden Wert zur Host-Codepage und zurück zu konvertieren legt verlorene Zeichen offen, bevor irgendein Byte die Platte erreicht, vorausgesetzt der Text wurde mit VarToWideStr statt VarToStr gelesen

SYLK- und DIF-Records, Zeile für Zeile

SYLK ist ein Record pro Zeile, Felder durch Semikolons getrennt. HotXLS schreibt einen ID;PHotXLS-Header, einen C;Yrow;Xcol;Kvalue-Record pro Zelle und einen E-Terminator. Text wird in Anführungszeichen gesetzt, mit eingebetteten Quotes verdoppelt, was auch ein Semikolon in einem Wert schützt, und der Reader akzeptiert das ;;-Escape, das manche Producer stattdessen benutzen. Ein Boolean existiert nicht. Die eigenen Writer des Formats senden 1 und 0, also ist das, was SaveAs schreibt; Fehler gehen als Text raus, etwa #N/A, Daten als Serial Numbers. Diese zwei Buchstaben an der Spitze sind, wie Excel SYLK erkennt — und warum es ein CSV, dessen erste Zelle ID liest, für eine SYLK-Datei hält. Build und Parse arbeiten beide auf WideString, also liefert der In-Memory-Round-Trip unten auf jedem Host dieselbe Antwort

uses
  SysUtils, lxSlk;

function SylkRoundTripsInMemory: Boolean;
var
  Cells, Back: TXLSSlkCells;
  Text: WideString;
  I: Integer;
begin
  SetLength(Cells, 3);
  Cells[0].Row := 1; Cells[0].Col := 1; Cells[0].Kind := slkText;
  Cells[0].Text := WideString(#$4E0A#$6D77'; Pudong');   // 上海; Pudong
  Cells[1].Row := 1; Cells[1].Col := 2; Cells[1].Kind := slkText;
  Cells[1].Text := 'Pipe 12" steel';
  Cells[2].Row := 2; Cells[2].Col := 1; Cells[2].Kind := slkNumber;
  Cells[2].Number := 1234.5;

  Text := XlsBuildSlk(Cells);           // K"上海; Pudong", K"Pipe 12"" steel", K1234.5
  Result := XlsParseSlk(Text, Back) = Length(Cells);
  for I := 0 to High(Cells) do
    if Result then
      Result := (Back[I].Row = Cells[I].Row) and (Back[I].Col = Cells[I].Col) and
        (Back[I].Kind = Cells[I].Kind) and (Back[I].Text = Cells[I].Text) and
        ((Cells[I].Kind <> slkNumber) or (Back[I].Number = Cells[I].Number));
end;

DIF ist wortreicher. Vier Header-Gruppen, TABLE, VECTORS (Spaltenzahl), TUPLES (Zeilenzahl) und DATA, nehmen je drei Zeilen; jeder Daten-Item nimmt dann zwei, eine type,number-Zeile und eine Zeile mit dem quotierten String oder einem Value-Indikator. Eine Zeile eröffnet mit -1,0 und BOT, und -1,0 mit EOD schließt die Daten. Eine Zahl ist 0,value plus V, ein Logikwert 0,1 plus TRUE oder 0,0 plus FALSE, ein String 1,0 plus seinen quotierten Text. Beim Lesen mappt HotXLS die Indikatoren NA und ERROR auf #N/A und #VALUE! und behandelt NULL als leer. Ein Sheet namens Orders, dessen einzige Zeile Beijing, 1234.5 und True hält, exportiert als:

TABLE
0,1
"Orders"
VECTORS
0,3
""
TUPLES
0,1
""
DATA
0,0
""
-1,0
BOT
1,0
"Beijing"
0,1234.5
V
0,1
TRUE
-1,0
EOD

Ein Dezimalkomma bricht alle drei Formate

Die Codepage ist nicht die einzige Maschineneinstellung, die in diese Dateien hineinleakt. Auf einer Workstation mit deutschen oder französischen Ländereinstellungen macht FloatToStr aus 1234.5 ein 1234,5. In DIF wird die Item-Zeile zu 0,1234,5 und verliert ihre type,value-Form. In SYLK ist K1234,5 keine Zahl. In einem dBASE-N-Feld erwarten xBase-Reader einen Punkt. HotXLS schreibt und liest alle drei mit TFormatSettings.Invariant: SYLK bekommt das kürzeste Token, das zum selben Double zurückliest, DIF ein invariantes FloatToStr mit angehängtem .0 bei ganzen Zahlen, dBASE-numerische Felder ihre deklarierten Dezimalstellen, rechtsbündig. Auch das Parsen ignoriert das Locale, also liest sich eine Datei von einer Dezimalkomma-Maschine überall gleich

HotXLS SYLK- und DIF-Export in Delphi: C;Y;X;K-Records mit ID;PHotXLS-Header und Boolean als 1, DIF type,number-Item-Paare zwischen BOT und EOD, und TFormatSettings.Invariant hält 1234.5 intakt, wo ein Dezimalkomma DIF, SYLK und dBASE bricht
Die Orders-Zeile als SYLK-Records und DIF-Item-Paare zu schreiben zeigt, wo ein Dezimalkomma-Locale jedes Format korrumpieren würde, und warum HotXLS Zahlen mit invarianten Einstellungen formatiert

Tabellen lesen, die andere Programme schrieben

Die Tabelle eines anderen zu öffnen verlagert die Codepage-Frage auf die lesende Maschine. XlsReadDbfFile, und TXLSXWorkbook.Open auf einem .dbf-Namen, akzeptieren die Versions-Bytes 0x03, 0x83, 0x8B, 0x30, 0x31 und 0x32, also dBASE III und IV und Visual-FoxPro-Tabellen, eingeschlossen FoxPros Binärtypen I (32-Bit-Integer), B (Double), Y (Currency, skaliert mit 10,000) und T (Julianischer Tag plus Millisekunden). Memo-Text kommt aus einer .dbt neben der Tabelle, oder aus einer .fpt mit FoxPros Block-Layout, wenn keine .dbt existiert; die Endung entscheidet, also wird keiner der Begleiter als der andere verlesen. Zeichendaten werden in der Host-Seite dekodiert. Eine dBASE-III-Tabelle, geschrieben von einem DOS-Programm unter Codepage 866, mit Byte 29 auf null, zeigt daher auf einer 1252-Maschine die falschen Buchstaben. Wenn Sie den Producer kennen, lassen Sie den Import dort laufen, wo seine Codepage die Systemseite ist. SYLK-, DIF- und .prn-Dateien folgen derselben Regel, denn Open dekodiert ihre Bytes in der Host-Seite, bevor der Wide-Text-Parser sie sieht

Welches Format sollte Nicht-ASCII-Text zwischen Locales tragen?

Keins von diesen vieren, wenn Sie die Wahl haben. Benutzen Sie sie, wenn das empfangende System darauf besteht — ein ERP-Importscreen, eine xBase-Anwendung, eine GIS-Pipeline — und wenn die Daten ASCII sind oder beide Enden eine Codepage teilen. Alles andere sollte in einem Format reisen, das seine Kodierung deklariert: XLSX, SpreadsheetML 2003 oder CSV, das HotXLS als UTF-8 mit Byte Order Mark schreibt (der CSV- und TSV-Export hat eigene Transport-Fallstricke). Da SaveAs das Format an der Endung festmacht, muss Ihr Code beide Verweigerungs-Arten ab der ersten Tabelle handhaben: eine Exception für .dbf, ein Ergebnis von -1 für die Textformate

uses
  SysUtils, lxHandleX, lxDbf;

function SaveForLegacyImport(const BaseName: string): string;
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');   // das erste hinzugefügte Sheet wird aktiv
    Sheet.Cells[1, 1].Value := 'CUSTNO';
    Sheet.Cells[1, 2].Value := 'CITY';
    Sheet.Cells[2, 1].Value := 1001;
    Sheet.Cells[2, 2].Value := WideString(#$5317#$4EAC);  // 北京

    Result := BaseName + '.slk';
    if Book.SaveAs(Result) <> 1 then
    begin
      // Die aktive Codepage kann den Text nicht halten: keine .slk wurde erzeugt
      Result := BaseName + '.xlsx';
      Book.SaveAs(Result);
    end;

    try
      Book.SaveAs(BaseName + '.dbf');
    except
      on E: EXlsDbf do
        Result := Result + ' (no .dbf: ' + E.Message + ')';
    end;
  finally
    Book.Free;
  end;
end;

Auf einem 936-Host erzeugt diese Funktion eine .slk und eine .dbf, die beide 北京 halten; auf einem 1252-Host erzeugt sie eine .xlsx und meldet, warum die .dbf verweigert wurde. RTF-, HTML- und ZIP-Eintragsnamen in der HotXLS Delphi Spreadsheet-Komponente folgen Kodierungsregeln eigener Art