서양 PC에서 .dbf로 내보낸 중국어 도시 이름이 성공을 보고한 내보내기에서 물음표로 돌아올 수 있습니다. dBASE, SYLK, DIF 파일에는 인코딩을 기록할 곳이 없기 때문입니다. 안전한 writer는 호스트 ANSI 코드 페이지를 쓰고 첫 바이트 전에 모든 이름과 값을 그 페이지로 검사합니다. HotXLS는 그 페이지가 담지 못하는 것은 무엇이든 거부합니다
호스트 코드 페이지가 이 파일들이 가진 유일한 인코딩입니다
세 형식 모두 PC의 초기 세월에서 왔습니다. dBASE는 Ashton-Tate에서, SYLK는 Microsoft의 Multiplan 시대에서, DIF는 VisiCalc 회사인 Software Arts에서. 각각은 텍스트를 1바이트로 저장하고 reader가 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에서 중국어 한 글자는 2바이트이므로 열 글자 중국어 이름은 스무 바이트입니다. 그 바이트들을 잴 것 없이 복사해 넣으면 NUL과 타입 문자와 폭을 그대로 관통합니다. 이름이 열 글자 검사를 통과했더라도 그 뒤로는 어떤 reader도 그 디스크립터를 되살리지 못합니다. CJK 워크스테이션에서도 dBASE 필드 이름은 ASCII로 두세요. shapefile 속성 테이블을 읽는 xBase 도구, ODBC 드라이버, GIS 소프트웨어 전부가 그 이름들에 매여 있고, 936에서만 디코딩되는 이름은 다른 페이지를 도는 머신마다 다른 바이트로 변합니다
| 바이트 | dBASE III 내용 |
|---|---|
| 0 | 버전: 0x03, .dbt 메모 파일이 테이블에 동승하면 0x83 |
| 1–3 | 마지막 업데이트: 1900년 이후의 연도, 월, 일 |
| 4–7 | 레코드 수, 32비트 리틀 엔디안 |
| 8–11 | 헤더 길이와 레코드 길이, 16비트 워드 |
| 32 이후 | 필드마다 32바이트 디스크립터 하나, 그다음 0x0D. 레코드가 뒤따르며 각각 삭제 플래그 뒤에 위치 |
폭도 바이트입니다. 문자 필드(C)는 254바이트가 한계이고, N 값은 선언된 소수 자릿수를 지닌 오른쪽 정렬 텍스트이며, D 값은 YYYYMMDD 형태의 여덟 문자입니다. 필드에 비해 너무 넓은 값은 별표 한 줄이 되는데, 이는 dBASE 자신이 쓰는 관습입니다. 더 긴 텍스트는 .dbt 동반자에 블록 번호를 담는 메모(M) 필드로 갑니다. 512바이트 헤더, 그다음 512바이트 블록들이며 각 메모는 0x1A 0x1A로 끝납니다. .dbf 이름으로 TXLSXWorkbook.SaveAs를 부르면 활성 시트의 첫 사용 행이 필드 이름이 되고, 열마다 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 호스트에서는 테이블이 쓰이고 네 필드를 모두 지닌 채로 다시 열립니다. 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에도 같은 형태의 문제가 있습니다. 줄을 나누려고 와이드 텍스트를 Text 속성에 할당하면 모든 문자가 들어가는 길에서 호스트 코드 페이지를 통과합니다. XlsParseSlk, XlsParseDifText, XlsParsePrnText는 WideString 입력을 받아 어떤 호스트에서도 그 모든 문자를 지킵니다. Lazarus로도 빌드한다면 Free Pascal 아래에서 HotXLS 유닛이 어떤 문자열 타입으로 컴파일되는지를 보세요
SYLK와 DIF 레코드, 한 줄씩
SYLK는 줄당 레코드 하나, 세미콜론으로 나뉜 필드들입니다. HotXLS는 ID;PHotXLS 헤더, 셀마다 C;Y행;X열;K값 레코드 하나, E 종결자를 씁니다. 텍스트는 따옴표로 감싸지고 안의 따옴표는 겹쳐 쓰이는데, 이는 값 안의 세미콜론도 지켜 줍니다. reader는 어떤 producer가 대신 쓰는 ;; 이스케이프도 받습니다. 불리언은 없습니다. 형식 고유의 writer들이 1과 0을 보내므로 SaveAs가 쓰는 것도 그것이고, 오류는 #N/A 같은 텍스트로, 날짜는 일련 숫자로 나갑니다. 맨 위의 그 두 글자가 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는 더 말이 많습니다. 네 개의 헤더 그룹, TABLE, VECTORS(열 수), TUPLES(행 수), DATA가 각각 세 줄을 차지하고, 데이터 항목 하나는 그다음 두 줄씩 차지합니다. 타입,번호 줄 하나와 따옴표로 감싼 문자열이나 값 표시자를 담은 줄 하나입니다. 행은 -1,0과 BOT으로 열리고, -1,0과 EOD가 데이터를 닫습니다. 숫자는 0,값에 V이고, 논리는 0,1에 TRUE 혹은 0,0에 FALSE이며, 문자열은 1,0에 감싼 텍스트입니다. 읽을 때 HotXLS는 NA와 ERROR 표시자를 #N/A와 #VALUE!로 맞추고 NULL은 빈칸으로 취급합니다. 유일한 행에 Beijing, 1234.5, True를 지닌 Orders라는 시트는 이렇게 내보내집니다:
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
소수점 쉼표 하나가 세 형식을 모두 깹니다
코드 페이지만이 이 파일에 새는 기계 설정이 아닙니다. 독일어나 프랑스어 지역 설정의 워크스테이션에서 FloatToStr는 1234.5를 1234,5로 만듭니다. DIF에서는 항목 줄이 0,1234,5가 되어 타입,값 형태를 잃습니다. SYLK에서는 K1234,5가 숫자가 아닙니다. dBASE N 필드에서는 xBase reader들이 점을 기대합니다. HotXLS는 셋 모두를 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에서 옵니다. 확장자가 결정하므로 어느 동반자도 서로로 잘못 읽히지 않습니다. 문자 데이터는 호스트 페이지로 디코딩됩니다. 코드 페이지 866 아래에서 DOS 프로그램이 쓰고 바이트 29를 0으로 둔 dBASE III 테이블은 따라서 1252 머신에서 엉뚱한 글자를 보여 줍니다. producer를 알 때는 그 코드 페이지가 시스템 페이지인 곳에서 임포트를 돌리세요. SYLK, DIF, .prn 파일도 같은 규칙을 따릅니다. Open이 와이드 텍스트 파서가 보기 전에 그들의 바이트를 호스트 페이지로 디코딩하기 때문입니다
로캘 사이에서 ASCII가 아닌 텍스트는 어느 형식이 나를까?
선택권이 있다면 이 넷 중 어느 것도 아닙니다. 받는 시스템이 고집할 때 — ERP 임포트 화면, xBase 애플리케이션, GIS 파이프라인 — 그리고 데이터가 ASCII이거나 양끝이 같은 코드 페이지를 공유할 때 쓰세요. 그 밖의 모든 것은 인코딩을 선언하는 형식으로 이동해야 합니다. XLSX, SpreadsheetML 2003, 혹은 HotXLS가 바이트 순서 표식을 얹은 UTF-8로 쓰는 CSV(CSV와 TSV 내보내기에는 그들만의 전송 함정이 있습니다). SaveAs가 확장자로 형식을 고르므로 코드는 첫 테이블부터 두 거부 스타일을 모두 다뤄야 합니다. .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'); // 처음 추가된 시트가 활성이 됩니다
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
// 활성 코드 페이지가 텍스트를 담지 못합니다: .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 스프레드시트 컴포넌트의 RTF, HTML, ZIP 항목 이름은 그들만의 인코딩 규칙을 따릅니다