Chińskie nazwy miast wyeksportowane do .dbf na zachodnim PC mogą wrócić jako znaki zapytania z eksportu, który zgłosił sukces, bo pliki dBASE, SYLK i DIF nie mają gdzie zapisać kodowania. Bezpieczny writer używa ANSI strony kodowej hosta i sprawdza każdą nazwę i wartość względem niej przed pierwszym bajtem; HotXLS odmawia wszystkiego, czego ta strona nie pomieści
Strona kodowa hosta to jedyne kodowanie, jakie te pliki mają
Wszystkie trzy formaty pochodzą z wczesnych lat PC: dBASE od Ashton-Tate, SYLK z ery Multiplanu Microsoftu, DIF od Software Arts, firmy od VisiCalca. Każdy przechowuje tekst jako pojedyncze bajty i zakłada, że czytnik dzieli stronę kodową writera — uczciwy zakład, gdy każda maszyna w biurze biegała na tym samym DOS-ie. Na dzisiejszym Windows tę stronę określa to, co zwróci GetACP: 1252 przy instalacji zachodnioeuropejskiej, 936 przy uproszczonej chińskiej, 1251 przy cyrylickiej. dBASE IV i FoxPro położyły później znak language-drivera na bajcie 29 nagłówka tabeli, ale układ dBASE III, który zapisuje HotXLS, zostawia ten bajt na zerze, a SYLK, DIF i tekst .prn wyrównany spacjami nie mają dla niego pola w nagłówku w ogóle. Strona kodowa jest więc umówiona poza plikiem. HotXLS używa ANSI strony kodowej hosta przy zapisie i odczycie i przed zapisem sprawdza, czy każdy znak ma bajt na tej stronie
| Format | Rozszerzenie przy SaveAs / Open | Kodowanie zadeklarowane w pliku | Tekstu strona hosta nie pomieści |
|---|---|---|---|
| Tabela dBASE III | .dbf, plus plik memo .dbt, gdy potrzeba | Nie (bajt 29 zostaje na zerze) | EXlsDbf podnosi się przed zapisem, a świeżo utworzone .dbf / .dbt zostają skasowane |
| SYLK | .slk, .sylk | Nie | SaveAs zwraca -1 i nie tworzy pliku |
| DIF | .dif | Nie | SaveAs zwraca -1 i nie tworzy pliku |
| Tekst formatowany | .prn | Nie | SaveAs zwraca -1 i nie tworzy pliku |
Dziesięć bajtów na nazwę pola dBASE, liczone w stronie kodowej
Każdy limit rozmiaru w tabeli dBASE jest liczbą bajtów. Stąd boli multibyte'owe strony kodowe. Deskryptor pola trzyma nazwę w jedenastu bajtach — dziesięć na nazwę, jeden na kończący NUL — i kładzie literę typu tuż za nią na offsecie 11, szerokość na 16, a liczbę miejsc dziesiętnych na 17. Pod stroną kodową 936 chiński znak to dwa bajty, więc dziesięcioznakowa chińska nazwa to dwadzieścia. Wtrzyń te bajty bez ich zmierzenia, a przebiecą prosto przez NUL, literę typu i szerokość; żaden czytnik nie odtworzy potem tego deskryptora, choćby nazwa przeszła kontrolę dziesięciu znaków. Trzymaj nazwy pól dBASE w ASCII nawet na stacji CJK. Narzędzia xBase, sterowniki ODBC i oprogramowanie GIS czytające tabele atrybutów shapefile kluczują po tych nazwach, a nazwa dekodująca się tylko pod 936 zamienia w inne bajty na każdej maszynie z inną stroną
| Bajty | Zawartość dBASE III |
|---|---|
| 0 | Wersja: 0x03, albo 0x83, gdy tabelę akompaniuje plik memo .dbt |
| 1–3 | Ostatnia aktualizacja: rok od 1900, miesiąc, dzień |
| 4–7 | Liczba rekordów, 32-bitowe little-endian |
| 8–11 | Długość nagłówka i długość rekordu, słowa 16-bitowe |
| Od 32 | Po jednym 32-bajtowym deskryptorze na pole, potem 0x0D; za nimi rekordy, każdy za flagą usunięcia |
Szerokości też są bajtami. Pole znakowe (C) kończy się na 254 bajtach, wartości N to tekst wyrównany do prawej z zadeklarowanymi miejscami dziesiętnymi, wartości D to osiem znaków w formie YYYYMMDD, a wartość za szeroka na swoje pole staje się rzędem gwiazdek — konwencja, której sam dBASE używa. Dłuższy tekst idzie do pola memo (M) trzymającego numer bloku w towarzyszącym .dbt: nagłówek 512 bajtów, potem bloki po 512 bajtów, każdy memo zakończony 0x1A 0x1A. TXLSXWorkbook.SaveAs z nazwą .dbf zamienia pierwszy użyty wiersz aktywnego arkusza na nazwy pól, wnioskuje C, N, L albo D per kolumna, cofa się do C przy kolumnach mieszanych i przenosi tekst dłuższy niż 254 znaki do M. Gdy szerokościami trzeba sterować w bajtach, zbuduj tabelę sam przez TXlsDbfTable i zwymiaruj każde pole C na zakodowaną długość jego najdłuższej wartości
uses
Windows, SysUtils, Variants, lxDbf;
procedure WriteCustomerTable(const FileName: string);
var
Table: TXlsDbfTable;
Row: Integer;
begin
Table := TXlsDbfTable.Create;
try
// Nazwy ASCII co najwyżej dziesięciu bajtów; szerokości to bajty w stronie kodowej hosta
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 bajtów w CP936
Table.SetValue(Row, 2, 1234.5);
Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));
try
XlsWriteDbfFile(FileName, Table);
except
on E: EXlsDbf do
// Oczekiwane na hoście 1252: komunikat nazywa pole "CITY" i wiersz 1,
// a na dysku nie zostaje żadne .dbf
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
Oczekiwane: na hoście 936 tabela zostaje zapisana i otwiera się ponownie ze wszystkimi czterema polami. Na hoście 1252 XlsWriteDbfFile podnosi EXlsDbf, nazywając pole i wiersz liczony od jedynki. XlsWriteDbfStream konwertuje każdą nazwę pola i każdą wartość C albo M na stronę hosta i z powrotem, porównuje i mierzy każdą nazwę względem jej dziesięciu bajtów, zanim nagłówek wyjdzie, więc odrzucona tabela zostawia strumień nietknięty. Forma plikowa idzie o krok dalej i kasuje .dbf, oraz każde .dbt, które zdążyła już utworzyć
Jak sprawdzić, czy napis przeżyje stronę kodową hosta?
Skonwertuj go na AnsiString, skonwertuj to z powrotem na WideString i porównaj z oryginałem. Znak, którego strona nie ma, wraca jako zamiennik, zwykle ?, więc porównanie pada dokładnie wtedy, gdy eksport straciłby dane. Writerzy używają tej samej konwersji, więc kontrola i writer nie mogą się rozjechać co do strony kodowej. Zachowaj też długość w bajtach: to liczba, którą przykładasz do slotu nazwy dziesięciu bajtów albo do szerokości pola
uses
Variants;
// True, gdy każdy znak V przeżyje ANSI stronę kodową hosta;
// ByteLen to zakodowana długość do porównania z szerokością pola
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
W: WideString;
A: AnsiString;
begin
W := VarToWideStr(V); // VarToStr potrafi skonwertować, zanim ta kontrola zobaczy tekst
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
Komentarz przy VarToWideStr niesie prawdziwą lekcję. W Delphi 2007 i wcześniejszych string to wszędzie AnsiString, a pod Free Pascal unit Variants jest kompilowany z własnym typem string RTL, czyli AnsiString, niezależnie od trybu twojego unitu. Na tych kompilatorach VarToStr położył już ? tam, gdzie był każdy chiński znak. Straż zbudowana na AnsiString(VarToStr(V)) porównuje potem znaki zapytania ze znakami zapytania i akceptuje wszystko. TStringList ma tam problem tego samego kształtu: przypisz szeroki tekst do swojej własności Text, by rozdzielić wiersze, i każdy znak przechodzi przez stronę kodową hosta w drodze do środka. XlsParseSlk, XlsParseDifText i XlsParsePrnText biorą wejście WideString i zachowują każdy jego znak na dowolnym hoście. Jeśli budujesz też z Lazarusem, zobacz z jakim typem napisów unit HotXLS kompiluje się pod Free Pascal
Rekordy SYLK i DIF, wiersz po wierszu
SYLK to jeden rekord na wiersz, pola rozdzielone średnikami. HotXLS zapisuje nagłówek ID;PHotXLS, po jednym rekordzie C;Ywiersz;Xkolumna;Kwartość na komórkę i terminator E. Tekst jest w cudzysłowach z podwojonymi wbudowanymi cudzysłowami, co chroni też średnik wewnątrz wartości, a czytnik przyjmuje escape ;;, którego zamiast tego używają niektórzy producenci. Nie ma Boolean. Własne writery formatu wysyłają 1 i 0, więc to zapisuje SaveAs; błędy wychodzą jako tekst typu #N/A, daty jako numery seryjne. Te dwie litery na górze to sposób, w jaki Excel rozpoznaje SYLK — i powód, dla którego bierze CSV z pierwszą komórką ID za plik SYLK. Budowa i parsowanie pracują oba na WideString, więc round trip in-memory poniżej daje tę samą odpowiedź na dowolnym hoście
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 jest gadatliwszy. Cztery grupy nagłówka — TABLE, VECTORS (liczba kolumn), TUPLES (liczba wierszy) i DATA — biorą po trzy wiersze; każdy element danych bierze potem dwa: wiersz typ,liczba i wiersz trzymający napis w cudzysłowie albo wskaźnik wartości. Wiersz otwiera -1,0 z BOT, a -1,0 z EOD zamyka dane. Liczba to 0,wartość plus V, logiczna 0,1 plus TRUE albo 0,0 plus FALSE, napis 1,0 plus swój tekst w cudzysłowie. Przy czytaniu HotXLS mapuje wskaźniki NA i ERROR na #N/A i #VALUE!, a NULL traktuje jako puste. Arkusz o nazwie Orders, którego jedyny wiersz trzyma Beijing, 1234,5 i True, eksportuje się tak:
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
Przecinek dziesiętny łamie wszystkie trzy formaty
Strona kodowa to nie jedyne ustawienie maszyny, które przecieka do tych plików. Na stacji z niemieckimi albo francuskimi ustawieniami regionalnymi FloatToStr zamienia 1234.5 w 1234,5. W DIF wiersz elementu staje się 0,1234,5 i traci kształt typ,wartość. W SYLK K1234,5 nie jest liczbą. W polu dBASE N czytniki xBase oczekują kropki. HotXLS zapisuje i czyta wszystkie trzy z TFormatSettings.Invariant: SYLK dostaje najkrótszy token czytający się z powrotem na ten sam double, DIF niezmienny FloatToStr z doklejoną .0 do liczb całkowitych, pola numeryczne dBASE swoje zadeklarowane miejsca dziesiętne, wyrównane do prawej. Parsowanie też ignoruje lokalizację, więc plik z maszyny z przecinkiem dziesiętnym czyta się tak samo wszędzie
Czytanie tabel napisanych przez inne programy
Otwarcie cudzej tabeli przenosi pytanie o stronę kodową na maszynę czytającą. XlsReadDbfFile, a także TXLSXWorkbook.Open przy nazwie .dbf, przyjmują bajty wersji 0x03, 0x83, 0x8B, 0x30, 0x31 i 0x32, obejmując tabele dBASE III i IV oraz Visual FoxPro, łącznie z binarnymi typami FoxPro I (liczba całkowita 32-bitowa), B (double), Y (waluta skalowana przez 10 000) i T (dzień juliański plus milisekundy). Tekst memo pochodzi z .dbt obok tabeli albo z .fpt z układem bloków FoxPro, gdy .dbt nie istnieje; rozstrzyga rozszerzenie, więc żaden towarzysz nie zostaje odczytany jako drugi. Dane znakowe są dekodowane w stronie hosta. Tabela dBASE III zapisana przez program DOS-owy pod stroną kodową 866, z bajtem 29 zostawionym na zerze, pokaże więc złe litery na maszynie 1252. Gdy znasz producenta, odpal import tam, gdzie jego strona kodowa jest stroną systemową. Pliki SYLK, DIF i .prn podążają za tą samą regułą, bo Open dekoduje ich bajty w stronie hosta, zanim parser szerokiego tekstu je zobaczy
Który format ma nieść tekst spoza ASCII między lokalizacjami?
Żaden z tych czterech, jeśli masz wybór. Używaj ich, gdy system odbierający nalega — ekran importu ERP, aplikacja xBase, potok GIS — i gdy dane są ASCII albo oba końce dzielą stronę kodową. Wszystko inne powinno podróżować w formacie deklarującym swoje kodowanie: XLSX, SpreadsheetML 2003 albo CSV, który HotXLS zapisuje jako UTF-8 z BOM (eksport CSV i TSV ma swoje własne pułapki transportu). Skoro SaveAs wybiera format z rozszerzenia, twój kod musi obsłużyć oba style odmowy z pierwszej tabeli: wyjątek dla .dbf, wynik -1 dla formatów tekstowych
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'); // pierwszy dodany arkusz staje się aktywnym
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
// Aktywna strona kodowa nie pomieści tekstu: żadne .slk nie powstało
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;
Na hoście 936 ta funkcja produkuje .slk i .dbf, które oba trzymają 北京; na hoście 1252 produkuje .xlsx i raportuje, czemu .dbf zostało odmówione. Nazwy wpisów RTF, HTML i ZIP w HotXLS, komponencie arkuszy dla Delphi podążają za regułami kodowania własnymi