Delphi:替代使用Reset/ReadLn进行文本文件读取的方法

12

我想逐行处理一个文本文件。从前,我会将文件加载到一个StringList中:

slFile := TStringList.Create();
slFile.LoadFromFile(filename);

for i := 0 to slFile.Count-1 do
begin
   oneLine := slFile.Strings[i];
   //process the line
end;

问题在于,一旦文件变得几百兆字节,我就不得不分配一个巨大的内存块;而实际上我只需要足够的内存来一次性保存一行。 (另外,在第一步中加载文件时系统锁定,你不能真正指示进度)。

然后我尝试使用Delphi提供的本地推荐文件I/O例程:

var
   f: TextFile;
begin
   Reset(f, filename);
   while ReadLn(f, oneLine) do
   begin
       //process the line
   end;

Assign的问题在于没有选项可以在不锁定文件的情况下读取文件(即fmShareDenyNone)。前一个stringlist示例也不支持无锁,除非将其更改为LoadFromStream

slFile := TStringList.Create;
stream := TFileStream.Create(filename, fmOpenRead or fmShareDenyNone);
   slFile.LoadFromStream(stream);
stream.Free;

for i := 0 to slFile.Count-1 do
begin
   oneLine := slFile.Strings[i];
   //process the line
end;

尽管我没有获得任何锁定,但现在我又回到了将整个文件加载到内存中的情况。

是否有一些替代Assign/ReadLn的方法,可以逐行读取文件,而不需要获取共享锁定?

我不想直接使用Win32 CreateFile/ReadFile,因为这样要处理分配缓冲区以及检测CRLFCRLF等问题。

我想过使用内存映射文件,但是如果整个文件无法(映射)到虚拟内存中,则会出现困难,并且必须一次映射文件的视图(片段)。这开始变得麻烦。

我只想要带有fmShareDenyNone选项的Reset

7个回答

16

在最新版本的 Delphi 中,你可以使用 TStreamReader。通过使用文件流构建它,然后调用其继承自TTextReaderReadLine 方法

对于所有版本的 Delphi,你还可以使用 Peter Below 的 StreamIO 单元,它提供了 AssignStream。它的用法类似于 AssignFile,但是用于流而不是文件名。一旦你使用该函数将流与 TextFile 变量关联起来,就可以像操作其他文件一样调用 ReadLn 和其他 I/O 函数。


3
如果TStreamReader不是“如此缓慢”,那将是非常好的。请从CodeCentral获取Uffe Kousgaard的“文本读取基准测试”并添加一个TStreamReader实现。运行它并观察你的CPU烧坏。它甚至不受I/O限制。 - afrazier
1
我编写了一种非常不错的替代方案,非常快速。它已经集成到Jedi JVCL中的TJvCsvDataSet源代码中。 - Warren P
4
请使用文字说明,Ian,而不仅仅是链接。否则我不知道你想表达什么意思。 - Rob Kennedy

4
您可以使用以下示例代码:
TTextStream = class(TObject)
      private
        FHost: TStream;
        FOffset,FSize: Integer;
        FBuffer: array[0..1023] of Char;
        FEOF: Boolean;
        function FillBuffer: Boolean;
      protected
        property Host: TStream read FHost;
      public
        constructor Create(AHost: TStream);
        destructor Destroy; override;
        function ReadLn: string; overload;
        function ReadLn(out Data: string): Boolean; overload;
        property EOF: Boolean read FEOF;
        property HostStream: TStream read FHost;
        property Offset: Integer read FOffset write FOffset;
      end;

    { TTextStream }

    constructor TTextStream.Create(AHost: TStream);
    begin
      FHost := AHost;
      FillBuffer;
    end;

    destructor TTextStream.Destroy;
    begin
      FHost.Free;
      inherited Destroy;
    end;

    function TTextStream.FillBuffer: Boolean;
    begin
      FOffset := 0;
      FSize := FHost.Read(FBuffer,SizeOf(FBuffer));
      Result := FSize > 0;
      FEOF := Result;
    end;

    function TTextStream.ReadLn(out Data: string): Boolean;
    var
      Len, Start: Integer;
      EOLChar: Char;
    begin
      Data:='';
      Result:=False;
      repeat
        if FOffset>=FSize then
          if not FillBuffer then
            Exit; // no more data to read from stream -> exit
        Result:=True;
        Start:=FOffset;
        while (FOffset<FSize) and (not (FBuffer[FOffset] in [#13,#10])) do
          Inc(FOffset);
        Len:=FOffset-Start;
        if Len>0 then begin
          SetLength(Data,Length(Data)+Len);
          Move(FBuffer[Start],Data[Succ(Length(Data)-Len)],Len);
        end else
          Data:='';
      until FOffset<>FSize; // EOL char found
      EOLChar:=FBuffer[FOffset];
      Inc(FOffset);
      if (FOffset=FSize) then
        if not FillBuffer then
          Exit;
      if FBuffer[FOffset] in ([#13,#10]-[EOLChar]) then begin
        Inc(FOffset);
        if (FOffset=FSize) then
          FillBuffer;
      end;
    end;

    function TTextStream.ReadLn: string;
    begin
      ReadLn(Result);
    end;

使用方法:

procedure ReadFileByLine(Filename: string);
var
  sLine: string;
  tsFile: TTextStream;
begin
  tsFile := TTextStream.Create(TFileStream.Create(Filename, fmOpenRead or    fmShareDenyWrite));
  try
    while tsFile.ReadLn(sLine) do
    begin
      //sLine is your line
    end;
  finally
    tsFile.Free;
  end;
end;

使用这种方法找到了一些缺失的行。 - hikari

3

看起来FileMode变量对于文本文件是无效的,但我的测试表明多次读取文件没有问题。你在问题中没有提到它,但如果你在读取文本文件时不打算写入它,那么应该没有问题。


即使对于非文本文件,当您调用“Reset”时,“FileMode”的除了最后两位之外的所有位都将被屏蔽掉,因此共享标志也会被忽略。 - Rob Kennedy
1
你真的试过吗?我制作了一个简单的应用程序,使用fmOpenRead + fmShareDenyWrite打开文本文件,每次按钮点击读取一行并将其添加到TMemo中。我可以同时执行两次应用程序并读取文件。此外,禁止向文件写入。如果有人感兴趣,我可以编辑我的答案以包括相关的源代码。顺便说一句,已在D2010上进行测试。 - Uwe Raabe
我刚刚进行了另一个测试:即使没有使用fmShareDenyWrite,它也可以工作。到目前为止我遇到的唯一缺点是似乎无法在文件打开以供读取时写入文件(即使使用fmShareDenyNone),但是从多个进程中读取似乎没有问题。 - Uwe Raabe

3

你能否将你的优秀代码兼容于Linux(CrossKylix或CrossFPC)? - SOUser

2
我所做的是使用TFileStream,但我会将输入缓冲成相当大的块(例如每个块几兆字节),并逐块读取和处理。这样我就不必一次性加载整个文件。
这种方式运行速度相当快,即使对于大文件也是如此。
我有一个进度指示器。随着我加载每个块,我会根据另外加载的文件部分的比例来增加它。
一次只读取一行,没有缓冲机制,对于大文件来说太慢了。

1

我几年前也遇到了同样的问题,尤其是文件锁定问题。我的解决方法是使用来自shellapi的低级readfile。虽然我的回答已经有2年了,但也许我的贡献能够帮助将来遇到同样问题的人。

const
  BUFF_SIZE = $8000;
var
  dwread:LongWord;
  hFile: THandle;
  datafile : array [0..BUFF_SIZE-1] of char;

hFile := createfile(PChar(filename)), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, 0);
SetFilePointer(hFile, 0, nil, FILE_BEGIN);
myEOF := false;
try
  Readfile(hFile, datafile, BUFF_SIZE, dwread, nil);   
  while (dwread > 0) and (not myEOF) do
  begin
    if dwread = BUFF_SIZE then
    begin
      apos := LastDelimiter(#10#13, datafile);
      if apos = BUFF_SIZE then inc(apos);
      SetFilePointer(hFile, aPos-BUFF_SIZE, nil, FILE_CURRENT);
    end
    else myEOF := true;
    Readfile(hFile, datafile, BUFF_SIZE, dwread, nil);
  end;
finally
   closehandle(hFile);
end;

对我来说,速度的提升似乎是显著的。


0
为什么不直接从TFileStream中逐行读取文件呢?
即(伪代码):
  readline: 
    while NOT EOF and (readchar <> EOL) do
      appendchar to result


  while NOT EOF do
  begin
    s := readline
    process s
  end;

你可能会发现的一个问题是,如果我没记错的话,TFileStream没有缓冲,因此在处理大文件时性能会不够优秀。然而,对于非缓冲流的问题,有许多解决方案,包括这个,如果这种方法可以解决你的初始问题,你可能希望进行调查。

我不想这样做的原因是因为很难搞对。例如,你的伪代码有三个微妙的漏洞。所以与其重新发明一个有缺陷的轮子,我宁愿使用经过测试的现成代码。 - Ian Boyd
1
它怎么可能包含错误?这只是为了阐述一个想法而写的伪代码,不是真正的代码!!你如何实现真正的代码将决定它是否包含错误。你想要在从磁盘读取文件时同时处理它,而不是在读取整个内容后再处理它,那么流式处理正是你需要的(你会注意到所有其他答案都是这个主题的变体!)。如果你已经有了想要听到的答案,为什么还要问这个问题呢? - Deltics
伪代码用于展示算法,而不必处理特定语言的麻烦。在这种情况下,算法存在缺陷。 - Ian Boyd
1
很抱歉,但如果您需要在这样的论坛中甚至以伪代码的形式得到完整的可工作代码,那么我认为您应该寻找另一份工作。软件开发显然不是您的强项(如果您足够聪明以发现微妙的逻辑缺陷,那么您足够优秀以编写没有这些缺陷的真正代码)。天哪! - Deltics
1
我当时能够发现错误的唯一原因是我花了几个小时来解决这个问题。如果今天我再写一遍,肯定会弄错的。我不想出现代码错误;我宁愿使用经过高度测试的可靠代码(即为什么要重新发明轮子)。我认为成为一个好程序员的一部分是在问题出现之前就能识别它们。 - Ian Boyd

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