TIBInputDelimitedFileのバグ
Firebird-jp-general MLであったこの話です。
IBSQL.BatchInputで日本語が化けるって話だったので調べて見た所。
TIBInputDelimitedFile.GetColumnの中で
function TIBInputDelimitedFile.GetColumn(var Col: string): Integer; var c: Char; BytesRead: Integer; procedure ReadInput; begin if FLookAhead <> NULL_TERMINATOR then begin : end else BytesRead := FFile.Read(c, 1); end;
こんな感じでchar型の変数に1Byteずつデータを読む処理がありました。Delphi2007以前のDelphiだと問題無いんですが、Delphi2009以降だとcharはWideCharになってますからこれはまずいです。質問であったのは日本語が化けるって話だったんですが、恐らく日本語だけで無く例えば、英数のみのデータでもファイルがUTF-8で保存されてたりするとアウトだと思います。
一応QC登録しておきました。
Report No: 93446 Status: Open TIBInputDelimitedFile.GetColumn destroys non-ASCII data. http://qc.embarcadero.com/wc/qcmain.aspx?d=93446 QCWIN:Defect_No=93446
どう直せば良いのか悩んだのですが、なるべく処理をいじりたくなかったので、まずファイルからデータを読んだ後、そのデータをUnicodeStringに変換しそのデータをMemoryStreamに書き込む事にしました。TStrings.LoadFromStringの処理を参考にデータを読んでUnicodeStringに変換したデータをMemoryStreamに書き込み、元の処理ではFileStreamからデータを読んでたのをMemoryStreamから読むように変更しました。
TIBInputDelimitedFileを元に変更を加えてTIBInputDelimitedFile2というクラスにしました。それが下記です。
Shift_JISとUTF-8のデータだと上手く行くのは確認しました。
unit IBInputDelimitedFile2; interface uses Classes, SysUtils, IBSQL, IBUtils; type TIBInputDelimitedFile2 = class(TIBBatchInput) private FEncoding: TEncoding; FStream: TStream; procedure SetEncoding(const Value: TEncoding); procedure ReadBuffer; protected FColDelimiter, FRowDelimiter: string; FEOF: Boolean; FFile: TFileStream; FLookAhead: Char; FReadBlanksAsNull: Boolean; FSkipTitles: Boolean; public constructor Create(AEncoding: TEncoding); destructor Destroy; override; function GetColumn(var Col: string): Integer; function ReadParameters: Boolean; override; procedure ReadyFile; override; property Encoding: TEncoding read FEncoding write SetEncoding; property ColDelimiter: string read FColDelimiter write FColDelimiter; property ReadBlanksAsNull: Boolean read FReadBlanksAsNull write FReadBlanksAsNull; property RowDelimiter: string read FRowDelimiter write FRowDelimiter; property SkipTitles: Boolean read FSkipTitles write FSkipTitles; end; implementation const CBUFFSIZE: Integer = 1024; { TIBInputDelimitedFile2 } constructor TIBInputDelimitedFile2.Create(AEncoding: TEncoding); begin FEncoding := AEncoding.Clone; FStream := TMemoryStream.Create; end; destructor TIBInputDelimitedFile2.Destroy; begin FFile.Free; FreeAndNil(FStream); if (FEncoding <> nil) and not TEncoding.IsStandardEncoding(FEncoding) then FreeAndNil(FEncoding); inherited Destroy; end; function TIBInputDelimitedFile2.GetColumn(var Col: string): Integer; var c: Char; BytesRead: Integer; procedure ReadInput; begin if FLookAhead <> NULL_TERMINATOR then begin c := FLookAhead; BytesRead := 1; FLookAhead := NULL_TERMINATOR; end else begin BytesRead := FStream.Read(C, SizeOf(C)); if BytesRead = 0 then begin ReadBuffer; BytesRead := FStream.Read(C, SizeOf(C)); end; end; end; procedure CheckCRLF(Delimiter: string); begin if (c = CR) and (Pos(LF, Delimiter) > 0) then {mbcs ok} begin BytesRead := FStream.Read(c, SizeOf(C)); if BytesRead = 0 then begin ReadBuffer; BytesRead := FStream.Read(C, SizeOf(C)); end; if (BytesRead = 1) and (c <> #10) then FLookAhead := c; end; end; begin Col := ''; result := 0; ReadInput; while BytesRead <> 0 do begin if Pos(c, FColDelimiter) > 0 then {mbcs ok} begin CheckCRLF(FColDelimiter); result := 1; break; end else if Pos(c, FRowDelimiter) > 0 then {mbcs ok} begin CheckCRLF(FRowDelimiter); result := 2; break; end else begin Col := Col + C; end; ReadInput; end; end; procedure TIBInputDelimitedFile2.ReadBuffer; var Buffer: TBytes; Enc: TEncoding; Str: String; BytesRead: Integer; Size: Integer; begin Enc := Nil; SetLength(Buffer, CBUFFSIZE); BytesRead := FFile.Read(Buffer[0], CBUFFSIZE); Size := TEncoding.GetBufferEncoding(Buffer, Enc, FEncoding); SetEncoding(Enc); Str := Enc.GetString(Buffer, Size, BytesRead - Size); FStream.Position := 0; FStream.Size := Length(Str) * 2; FStream.Write(Str[1], Length(Str) * 2); FStream.Position := 0; end; function TIBInputDelimitedFile2.ReadParameters: Boolean; var i, curcol: Integer; Col: string; begin result := False; if not FEOF then begin curcol := 0; repeat i := GetColumn(Col); if (i = 0) then FEOF := True; if (curcol < Params.Count) then begin try if (Col = '') and (ReadBlanksAsNull) then Params[curcol].IsNull := True else Params[curcol].AsString := Col; Inc(curcol); except on E: Exception do begin if not (FEOF and (curcol = Params.Count)) then raise; end; end; end; until (FEOF) or (i = 2); result := ((FEOF) and (curcol = Params.Count)) or (not FEOF); end; end; procedure TIBInputDelimitedFile2.ReadyFile; var col : String; curcol : Integer; begin if FColDelimiter = '' then FColDelimiter := TAB; if FRowDelimiter = '' then FRowDelimiter := CRLF; FLookAhead := NULL_TERMINATOR; FEOF := False; if FFile <> nil then FFile.Free; FFile := TFileStream.Create(FFilename, fmOpenRead or fmShareDenyWrite); ReadBuffer; if FSkipTitles then begin curcol := 0; while curcol < Params.Count do begin GetColumn(Col); Inc(CurCol) end; end; end; procedure TIBInputDelimitedFile2.SetEncoding(const Value: TEncoding); begin if not TEncoding.IsStandardEncoding(FEncoding) then FEncoding.Free; if TEncoding.IsStandardEncoding(Value) then FEncoding := Value else if Value <> nil then FEncoding := Value.Clone else FEncoding := TEncoding.Default; end; end.
使い方は下記みたいな感じです。
TIBInputDelimitedFile2.Create(TEncoding.GetEncoding(932));とCreateに引数でファイルのEncodingを渡します。(この例だとShift_JIS)
データを読む処理はTStringsを参考にしましたのでTEncoding.Defaultを渡しておけば上手く行くのかなという気もしますが、あまりちゃんとテストしてないので微妙です。
var DelimInput : TIBInputDelimitedFile2; begin IBDatabase1.Open; IBSQL1.Transaction.StartTransaction; DelimInput := TIBInputDelimitedFile2.Create(TEncoding.GetEncoding(932)); try IBSQL1.SQL.Text := 'INSERT INTO NEW_TABLE VALUES(:IDX, :DATA);'; DelimInput.Filename := 'Data.txt'; IBSQL1.BatchInput(DelimInput); IBSQL1.Transaction.Commit; except IBSQL1.Transaction.Rollback; end; FreeAndNil(DelimInput); IBDatabase1.Close;