西欧の PC で .dbf に書き出した中国語の市名が、成功したはずの書き出しから疑問符で戻ってくることがあります。dBASE、SYLK、DIF のファイルにはエンコーディングを記録する場所がないからです。安全な writer はホストの ANSI コードページを使い、最初の 1 バイトを書く前にすべての名前と値をそのページで検査します。HotXLS はそのページに収まらないものを拒みます
ホストコードページこそ、これらのファイルが持つ唯一のエンコーディング
3 形式とも PC 初期の産物です。dBASE は Ashton-Tate、SYLK は Microsoft の Multiplan 時代、DIF は VisiCalc の Software Arts から来ました。どれもテキストをシングルバイトで格納し、リーダーが writer と同じコードページを共有していると想定します。オフィスの全マシンが同じ DOS 構成で動いていた時代には、筋のいい賭けでした。今日の Windows では、そのページは GetACP が返すものです。西欧インストールで 1252、簡体字中国語で 936、キリルで 1251。dBASE IV と FoxPro は後にテーブルヘッダーのバイト 29 へ言語ドライバの印を置きましたが、HotXLS が書く dBASE III レイアウトではこのバイトは 0 のまま、SYLK、DIF、スペース桁合わせの .prn テキストには、そもそもそれ用のヘッダーフィールドがありません。つまりコードページはファイルの外で合意されます。HotXLS は書き込みと読み込みの両方でホストの ANSI コードページを使い、書く前にすべての文字がそのページ内のバイトを持つかを検査します
| 形式 | SaveAs / Open の拡張子 | ファイル内で宣言されるエンコーディング | ホストページに収まらないテキストのとき |
|---|---|---|---|
| dBASE III テーブル | .dbf。必要なら .dbt メモファイルも | なし(バイト 29 は 0 のまま) | 書き込み前に EXlsDbf を上げ、直後に作った .dbf / .dbt を削除する |
| SYLK | .slk、.sylk | なし | SaveAs が -1 を返し、ファイルは作らない |
| DIF | .dif | なし | SaveAs が -1 を返し、ファイルは作らない |
| 整形テキスト | .prn | なし | SaveAs が -1 を返し、ファイルは作らない |
dBASE のフィールド名 10 バイト、数えるのはコードページで
dBASE テーブルのサイズ上限は、すべてバイト数です。マルチバイトコードページが痛いのはここです。フィールドディスクリプタは名前を 11 バイトに収めます。名前 10 バイト、終端 NUL に 1 バイト。タイプ文字がすぐ後ろのオフセット 11、幅が 16、小数桁数が 17 に座ります。コードページ 936 では漢字 1 文字が 2 バイトなので、10 文字の中国語名は 20 バイトになります。それを測らずにコピーすれば、バイト列は NUL、タイプ文字、幅をまっすぐ踏み抜きます。名前は 10 文字チェックを通っていたのに、そのディスクリプタはもう誰にも読めません。dBASE のフィールド名は、CJK ワークステーションでも ASCII にしましょう。xBase ツール、ODBC ドライバ、shapefile の属性テーブルを読む GIS ソフトは、どれもその名前をキーにします。936 でしか解読できない名前は、別のページで動くマシンの上で別のバイト列に化けます
| バイト | dBASE III の内容 |
|---|---|
| 0 | バージョン:0x03。テーブルに .dbt メモファイルが付くときは 0x83 |
| 1–3 | 最終更新:1900 年からの年、月、日 |
| 4–7 | レコード数。32 ビットのリトルエンディアン |
| 8–11 | ヘッダー長とレコード長。16 ビットワード |
| 32 以降 | フィールドごとに 1 つの 32 バイトディスクリプタ、続いて 0x0D。レコードはその後ろに、それぞれ削除フラグを伴って続く |
幅もバイトです。文字フィールド(C)の上限は 254 バイト、N の値は宣言どおりの小数桁の右寄せテキスト、D の値は YYYYMMDD 形の 8 文字です。フィールドに収まらない値はアスタリスクの並びになります。dBASE 自身が使う慣例です。長いテキストはメモ(M)フィールドへ行き、.dbt 相手のブロック番号を保持します。512 バイトのヘッダー、続いて 512 バイトのブロック、各メモは 0x1A 0x1A で終わります。.dbf 名での TXLSXWorkbook.SaveAs は、active シートの使用中最初の行をフィールド名に変え、列ごとに C、N、L、D を推定し、混在列は C に落とし、254 文字を超えるテキストは M へ移します。幅をバイトで管理したいときは、TXlsDbfTable でテーブルを自前で組み立て、各 C フィールドを最長値の符号化後の長さでサイズ決めましょう
uses
Windows, SysUtils, Variants, lxDbf;
procedure WriteCustomerTable(const FileName: string);
var
Table: TXlsDbfTable;
Row: Integer;
begin
Table := TXlsDbfTable.Create;
try
// 最大 10 バイトの ASCII 名。幅はホストコードページでのバイト数
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)); // 北京市。CP936 では 6 バイト
Table.SetValue(Row, 2, 1234.5);
Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));
try
XlsWriteDbfFile(FileName, Table);
except
on E: EXlsDbf do
// 1252 ホストでの期待結果:メッセージはフィールド "CITY" と行 1 を
// 名指し、ディスクに .dbf は残らない
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
期待結果:936 のホストではテーブルが書かれ、4 フィールドすべてが再オープンできます。1252 のホストでは XlsWriteDbfFile がフィールド名と 1 始まりの行番号を添えて EXlsDbf を上げます。XlsWriteDbfStream はすべてのフィールド名とすべての C、M の値をホストページへ変換して戻し、比較し、ヘッダーが出ていく前に各名前を 10 バイトで測ります。拒まれたテーブルはストリームを無傷のままにします。ファイル形態はさらに一歩進んで、すでに作ってしまった .dbf と、あれば .dbt を削除します
文字列がホストコードページを生き延びるかの試し方
AnsiString に変換し、それを WideString に戻し、元と比べます。ページにない文字は置き換え、たいてい ? で戻ってくるので、比較が失敗するのは、書き出しがデータを失うときにちょうど一致します。writer も同じ変換を使うので、検査と writer がコードページについて食い違うことはありません。バイト長も取っておきましょう。10 バイトの名前スロットやフィールド幅と突き合わせる数字です
uses
Variants;
// V のすべての文字がホスト ANSI コードページを生き延びるとき True。
// ByteLen はフィールド幅と比較する符号化後の長さ
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
W: WideString;
A: AnsiString;
begin
W := VarToWideStr(V); // VarToStr はこの検査がテキストを見る前に変換してしまうことがある
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
VarToWideStr へのコメントに本題が載っています。Delphi 2007 以前では string はどこも AnsiString で、Free Pascal の下では、あなたのユニットのモードが何であれ、Variants ユニットは RTL 自身の string 型、AnsiString でコンパイルされます。そうしたコンパイラでは VarToStr が、漢字のあった場所にすでに ? を置いています。AnsiString(VarToStr(V)) の上に組んだガードは、疑問符と疑問符を比較してすべてを受け入れます。TStringList にも同じ形の問題があります。行分割のために wide テキストを Text プロパティへ代入すると、入る途中で全文字がホストコードページを通ります。XlsParseSlk、XlsParseDifText、XlsParsePrnText は WideString を取り、どのホストでもその全文字を保持します。Lazarus でもビルドするなら、Free Pascal の下で HotXLS の各ユニットがどの文字列型でコンパイルされるかを参照してください
SYLK と DIF のレコード、行ごとに
SYLK は 1 行に 1 レコード、フィールドはセミコロン区切りです。HotXLS は ID;PHotXLS ヘッダー、セルごとに C;Y行;X列;K値 のレコード、そして E の終端子を書きます。テキストは引用符で囲み、埋め込まれた引用符は二重化します。値の中のセミコロンもこれで守られ、リーダーは一部の作成者が代わりに使う ;; エスケープも受け付けます。Boolean はありません。形式自身の writer が 1 と 0 を送るので、SaveAs もそう書きます。エラーは #N/A のようなテキストで出て行き、日付はシリアル値です。先頭のあの 2 文字は Excel が SYLK を認識する方法です。そして最初のセルが ID の CSV を SYLK ファイルと見違える理由でもあります。組み立てとパースはどちらも WideString で働くので、下のメモリ内往復はどのホストでも同じ答えを出します
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 はおしゃべりです。ヘッダーは 4 グループ、TABLE、VECTORS(列数)、TUPLES(行数)、DATA で、各 3 行使います。データ項目はその後 2 行ずつ。型,数 の行と、引用符付き文字列か値指示子の 1 行です。行は -1,0 と BOT で開き、-1,0 と EOD がデータを閉じます。数値は 0,値 に V、論理値は 0,1 に TRUE か 0,0 に FALSE、文字列は 1,0 に引用符付きテキストです。読み込みでは、HotXLS は NA と ERROR の指示子を #N/A と #VALUE! に写像し、NULL は空として扱います。Orders という名のシートで唯一の行が北京、1234.5、True を持つときの書き出しは:
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
小数点コンマは 3 形式すべてを壊す
これらのファイルに染み出す機械設定はコードページだけではありません。ドイツ語やフランス語の地域設定のワークステーションでは、FloatToStr が 1234.5 を 1234,5 に変えます。DIF では項目行が 0,1234,5 になり、型,値 の形を失います。SYLK では K1234,5 は数値ではありません。dBASE の N フィールドでは、xBase リーダーはポイントを期待します。HotXLS は 3 形式の書き込みと読み込みを TFormatSettings.Invariant で行います。SYLK は同じ double に読み戻せる最短トークン、DIF は整数に .0 を付けた不変の FloatToStr、dBASE の数値フィールドは宣言どおりの小数桁の右寄せです。パースもロケールを無視するので、小数点コンマのマシンが作ったファイルもどこで読んでも同じになります
他のプログラムが書いたテーブルを読む
他人のテーブルを開くと、コードページの問題は読み込み側のマシンに移ります。XlsReadDbfFile と、.dbf 名での TXLSXWorkbook.Open は、バージョンバイト 0x03、0x83、0x8B、0x30、0x31、0x32 を受け付けます。dBASE III と IV、Visual FoxPro のテーブルを覆い、FoxPro のバイナリ型 I(32 ビット整数)、B(double)、Y(10,000 倍の通貨)、T(ユリウス日+ミリ秒)も含みます。メモテキストはテーブルの脇の .dbt から、.dbt がなければ FoxPro のブロックレイアウトの .fpt から来ます。拡張子が決めるので、どちらの相手ももう一方として誤読されません。文字データはホストページでデコードされます。したがって、DOS プログラムがコードページ 866 で書き、バイト 29 を 0 のままにした dBASE III テーブルは、1252 のマシンで間違った文字を表示します。作成者が分かっているなら、そのコードページがシステムページになっている場所でインポートを実行しましょう。SYLK、DIF、.prn のファイルも同じ規則に従います。wide テキストのパーサが目にする前に、Open がホストページでバイトをデコードするからです
ロケール間で非 ASCII テキストを運ぶべき形式はどれか
選べるなら、この 4 つのどれでもありません。使うのは受け側のシステムが強く求めるとき——ERP のインポート画面、xBase アプリケーション、GIS のパイプライン——で、データが ASCII か、両端がコードページを共有するときです。それ以外は、エンコーディングを宣言する形式で運ぶべきです。XLSX、SpreadsheetML 2003、あるいは HotXLS が BOM 付き UTF-8 で書く CSV(CSV と TSV の書き出しには輸送上の落とし穴が専用にあります)。SaveAs は拡張子から形式を選ぶので、最初の表の 2 通りの拒否をコード側で両方扱うことになります。.dbf は例外、テキスト形式は -1 です
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'); // 最初に追加したシートが active になる
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
// active のコードページではテキストを収められない。.slk は作られていない
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;
936 のホストでは、この関数は北京を収めた .slk と .dbf を作ります。1252 のホストでは .xlsx を作り、.dbf が拒まれた理由を報告します。HotXLS Delphi spreadsheet component での RTF、HTML、ZIP エントリ名には、それぞれ専用のエンコーディング規則があります