Delphi: альтернатива использованию Reset/ReadLn для чтения текстовых файлов

я хочу обработать текстовый файл строка за строкой. В старые времена я загрузил файл в StringList:

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

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

проблема в том, что как только файл становится несколько сотен мегабайт, я должен выделить огромный кусок памяти; когда на самом деле мне нужно только достаточно памяти, чтобы удерживать одну строку за раз. (Кроме того, вы не можете указать прогресс, когда система заблокирована при загрузке файла на шаге 1).

я попытался использовать родной, и рекомендуемые, файловые процедуры ввода-вывода, предоставляемые Delphi:

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

7 ответов


С последними версиями Delphi вы можете использовать TStreamReader. Создайте его с помощью потока файлов, а затем вызовите его ReadLine метод (наследуется от TTextReader).

опция для всех версий Delphi должна использовать Питер Белоуз StreamIO блок, который дает вам AssignStream. Он работает так же, как AssignFile, но для потоков вместо имен файлов. После того, как вы использовали эту функцию, чтобы связать поток с TextFile переменной, вы можете звоните ReadLn и другие функции ввода-вывода на нем так же, как любой другой файл.


Если вам нужна поддержка ansi и Unicode в старых Delphis, вы можете использовать my GpTextFile или GpTextStream.


Вы можете использовать этот пример кода:

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;

как кажется содержит filemode переменной не допустим для текстовых файлов, но мои тесты показали, что многократное чтение из файла проблем нет. Вы не упомянули об этом в своем вопросе, но если вы не собираетесь писать в текстовый файл, пока он читается, вам должно быть хорошо.


Я использую TFileStream, но я буферизую вход в довольно большие блоки (например, несколько мегабайт каждый) и читаю и обрабатываю один блок за раз. Таким образом, мне не придется загружать весь файл сразу.

Он работает довольно быстро, даже для больших файлов.

У меня есть индикатор прогресса. При загрузке каждого блока я увеличиваю его на долю файла, который был дополнительно загружен.

чтение одной строки за раз, без чего-либо чтобы сделать буферизацию, просто слишком медленно для больших файлов.


почему бы просто не прочитать строки файла непосредственно из самого TFileStream по одному за раз ?

т. е. (в псевдокоде):

  readline: 
    while NOT EOF and (readchar <> EOL) do
      appendchar to result


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

одна из проблем, с которой вы можете столкнуться, заключается в том, что iirc TFileStream не буферизован, поэтому производительность по большому файлу будет неоптимальной. Однако существует ряд решений проблемы не буферизованных потоков,в том числе и этот, что вы можете исследовать, если этот подход решает ваш первоначальный проблема.


У меня была такая же проблема несколько лет назад, особенно проблема блокирования файла. Я использовал низкоуровневый readfile из shellapi. Я знаю, что вопрос старый, так как мой ответ (2 года), но, возможно, мой вклад может помочь кому-то в будущем.

const
  BUFF_SIZE = 00;
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;

для меня улучшение скорости оказалось значительным.