如何将Unicode与Delphi中的文件写入代码配合使用

4

在移植到Unicode和Delphi 2009之前,我有一些代码会逐行将文本附加到日志文件中:

procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F, C1 : dword;
begin
  if LogFileName <> '' then begin
    F := CreateFileA(Pchar(LogFileName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_ALWAYS, 0, 0);
    if F <> 0 then begin
      SetFilePointer(F, 0, nil, FILE_END);
      S := S + #13#10;
      WriteFile(F, Pchar(S)^, Length(S), C1, nil);
      CloseHandle(F);
    end;
  end;
end;

但是CreateFileA和WriteFile是二进制文件处理程序,不适用于Unicode

我需要找到一个能够在Delphi 2009下执行相应操作并处理Unicode的工具。

我之所以为每一行都打开、写入、关闭文件,只是为了让其他程序(如WordPad)在日志写入时可以打开文件并读取它。

我一直在尝试使用TFileStream和TextWriter,但是它们的文档很少,示例也很少。

具体来说,我不确定它们是否适合这种不断打开和关闭文件的操作。此外,我也不确定它们是否可以在写入文件时使文件可供阅读。

有人知道我该如何在Delphi 2009或更高版本中实现这个操作吗?


结论:

Ryan的答案是最简单的,也是导致我找到解决方案的答案。使用他的解决方案,您还需要编写BOM并将字符串转换为UTF8(如我在他的答案中的评论中所述),然后就可以正常工作了。

但是我又更进一步研究了TStreamWriter。它是同名.NET函数的等效物。它理解Unicode,并提供非常干净的代码。

我的最终代码是:

procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F: TStreamWriter;
begin
  if LogFileName <> '' then begin
    F := TStreamWriter.Create(LogFileName, true, TEncoding.UTF8);
    try
      F.WriteLine(S);
    finally
      F.Free;
  end;
end;

最后,我发现另一个方面就是如果你追加了很多行(比如1000行或更多),那么对文件的追加需要越来越长的时间,并且变得非常低效。
因此,我最终决定不在每次重新创建和释放LogFile。相反,我保持它处于打开状态,这样速度就会非常快。唯一无法做到的事情就是在文件被创建时允许使用记事本查看文件。

我测试了你的AppendToLogFile,我的两个(非UTF和UTF)在日志追加变大时没有看到明显的增加。不知道为什么。有什么想法吗?我尝试了三种实现方式,添加10-20字节字符串的10K次需要大约2.4秒。 - Warren P
如果我保留一个全局TStreamWriter而不是创建和释放它,我的2.4秒就会降至93毫秒。哇。好的,从现在开始我就这样做了。好的提示。 - Warren P
@lkessler-还可以参考这个替代方案:http://stackoverflow.com/questions/35710087/how-to-save-classic-delphi-string-to-disk-and-read-them-back/36106740#36106740 - Gabriel
4个回答

4

为什么要使用流来记录日志?

为什么不使用文本文件?这是我其中一个日志例程的非常简单的示例。

procedure LogToFile(Data:string);
var
  wLogFile: TextFile;
begin
  AssignFile(wLogFile, 'C:\MyTextFile.Log');
  {$I-}
  if FileExists('C:\MyTextFile.Log') then
    Append(wLogFile)
  else     
    ReWrite(wLogFile); 
  WriteLn(wLogfile, S);
  CloseFile(wLogFile);
  {$I+}
  IOResult; //Used to clear any possible remaining I/O errors 
end;

我实际上有一个相当广泛的日志记录单元,使用关键部分进行线程安全控制,可以选择通过OutputDebugString命令进行内部日志记录,以及通过使用分段标识符记录指定代码部分。

如果有人感兴趣,我很乐意在此分享代码单元。


2
我在Vista和Windows 7上遇到了TextFile的问题,涉及到权限。例如,如何设置文本文件的共享模式?我想可能有一个全局变量可以解决这个问题。 - Warren P
@Warren P:FileMode是一个变量,它设置了读/写/读写的能力,并且如果我理解正确,还可以用于指定共享兼容性,在调用Append/Reset/Rewrite文件命令之前设置它。在Vista和7中,您受到写入文件的限制。例如,Windows会阻止您在Program Files文件夹下进行写入,这是权限问题。这与TextFiles的使用不一定有关,而是操作系统的功能。您确定问题是出在TextFiles上吗? - Vivian Mills
这是一个很好的答案。我尝试了一下,但发现它不能按原样写入Unicode。我不得不使用Dr. Bob在“Unicode文本文件输出”中提到的方法,写入BOM,然后将S更改为UTFString(S)。请参见:http://www.bobswart.nl/weblog/Blog.aspx?RootId=5:2975 - lkessler
@lkessler:谢谢你的更新。我还没有将该单元转换为Unicode。我会查看Dr. Bob的网站并相应地更新我的单元。 - Vivian Mills

2

自Delphi 2009版本开始,Char和String类型已经变为宽字符。因此,在编写代码时应该使用CreateFile而不是CreateFileA!

如果要写入字符串,应该使用Length(s)*sizeof(Char)作为字节长度,而不仅仅是Length(s),这是由于宽字符的问题所致。如果想要写入ANSI字符,则应将s定义为AnsiString或UTF8String,并使用sizeof(AnsiChar)作为乘数。

为什么您要使用Windows API函数而不是在classes.pas中定义的TFileStream?


我不确定在我编写 Delphi 2 和 Delphi 4 之间是否有 TFileStream。我一定是找到了一些使用 Windows API 程序的代码 - 既然它们能够工作,我就采用了它们。 - lkessler
他可能不想将UTF16写入日志文件。 - Warren P
@Warren:我的新程序现在是Unicode,所以我需要能够将Unicode字符写入日志文件。这是否使用UTF-16或UTF-8完成是另一回事,但不能再使用Ansi了。 - lkessler
据我所知,UTF8 是最好的选择。大多数文本编辑器都支持 ANSI 和 UTF8 编码,但不是所有文本编辑器都支持 UTF16 编码。虽然记事本和写字板支持 UTF16 编码,但这已经成为了一种“罕见的选择”,就像 PCX 格式的图片一样。 - Warren P
@Warren:如果文件不存在(例如在打开时文件大小为0),您可以添加保存UTF8 BOM的操作。这样编辑器就会自动识别它是UTF8编码。 - Ritsaert Hornstra

1

试试这个我专门为你准备的小函数。

procedure AppendToLog(filename,line:String);
var
  fs:TFileStream;
  ansiline:AnsiString;
  amode:Integer;
begin
  if not FileExists(filename) then
      amode := fmCreate
  else
      amode := fmOpenReadWrite;
fs := TFileStream.Create(filename,{mode}amode);
try
if (amode<>fmCreate) then
   fs.Seek(fs.Size,0); {go to the end, append}

 ansiline := AnsiString(line)+AnsiChar(#13)+AnsiChar(#10);
 fs.WriteBuffer(PAnsiChar(ansiline)^,Length(ansiline));
finally
   fs.Free;
end;

另外,试试这个UTF8版本:

procedure AppendToLogUTF8(filename, line: UnicodeString);
var
    fs: TFileStream;
    preamble:TBytes;
    outpututf8: RawByteString;
    amode: Integer;
  begin
    if not FileExists(filename) then
      amode := fmCreate
    else
      amode := fmOpenReadWrite;
    fs := TFileStream.Create(filename, { mode } amode, fmShareDenyWrite);
    { sharing mode allows read during our writes }
    try

      {internal Char (UTF16) codepoint, to UTF8 encoding conversion:}
      outpututf8 := Utf8Encode(line); // this converts UnicodeString to WideString, sadly.

      if (amode = fmCreate) then
      begin
          preamble := TEncoding.UTF8.GetPreamble;
          fs.WriteBuffer( PAnsiChar(preamble)^, Length(preamble));
      end
      else
      begin
        fs.Seek(fs.Size, 0); { go to the end, append }
      end;

      outpututf8 := outpututf8 + AnsiChar(#13) + AnsiChar(#10);
      fs.WriteBuffer(PAnsiChar(outpututf8)^, Length(outpututf8));
    finally
      fs.Free;
    end;
end;

顺便提一下,我倾向于避免直接使用win32文件API,如CreateFile。通过使用文件流而不是CreateFile,可以轻松实现您在注释中声明的目标。 - Warren P

1
如果您在多线程应用程序中尝试使用文本文件或Object Pascal类型/非类型文件,那么您将会遇到困难。
不开玩笑 - (Object) Pascal标准文件I/O使用全局变量来设置文件模式和共享。如果您的应用程序在多个线程(或者如果有人仍然使用它们,则为纤程)中运行,则使用标准文件操作可能会导致访问冲突和不可预测的行为。
由于日志记录的主要目的之一是调试多线程应用程序,请考虑使用其他文件I/O方式:流和Windows API。
(是的,我知道这实际上并不是对原始问题的回答,但我不想登录 - 因此我没有声誉得分来评论Ryan J. Mills的实际错误答案。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接