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;

Проблема с Assign в том, что нет возможности читать файл без блокировки (т.е. fmShareDenyNone). Предыдущий пример stringlist также не поддерживает no-lock, если вы не измените его на 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;

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

Есть ли альтернатива _9 _ / _ 10_, где я могу читать файл построчно, не блокируя общий доступ?

я бы предпочел не попадать прямо в Win32 _11 _ / _ 12_ и иметь дело с распределением буферов и обнаружением CR, LF, CRLF.

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

я просто хочу Reset с fmShareDenyNone!


person Ian Boyd    schedule 12.05.2010    source источник


Ответы (7)


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

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

person Rob Kennedy    schedule 12.05.2010
comment
TStreamReader был бы великолепен, если бы он не был чертовски медленным. Получите тест чтения текста Уффе Кусгаарда на CodeCentral и добавьте реализацию TStreamReader. Запустите его и смотрите, как горит ваш процессор. Это даже не привязка к вводу-выводу. - person afrazier; 12.05.2010
comment
Я написал действительно хорошую альтернативу, которая ДЕЙСТВИТЕЛЬНО БЫСТРО. Он встроен в исходный код TJvCsvDataSet в Jedi JVCL. - person Warren P; 07.04.2011
comment
Пожалуйста, используйте слова, Ян, а не только ссылки. Я не знаю, что вы пытаетесь передать иначе. - person Rob Kennedy; 23.04.2012

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

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;
person Linas    schedule 02.06.2010
comment
С помощью этого метода обнаружил несколько недостающих строк. - person hikari; 26.04.2012

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

person gabr    schedule 12.05.2010
comment
Сможете ли вы сделать свой отличный код совместимым с Linux (CrossKylix или CrossFPC)? - person SOUser; 26.02.2014

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

person Uwe Raabe    schedule 12.05.2010
comment
-1. Даже для нетекстовых файлов все, кроме двух младших битов FileMode, маскируются, когда вы вызываете Reset, поэтому флаги совместного использования также игнорируются. - person Rob Kennedy; 12.05.2010
comment
Вы правда пробовали? Я сделал простое приложение, которое открывает текстовый файл с помощью fmOpenRead + fmShareDenyWrite, читает одну строку при каждом нажатии кнопки и добавляет ее в TMemo. Я могу запустить приложение два раза и одновременно прочитать файл. Кроме того, запись в файл запрещена. Если кому-то интересно, я могу отредактировать свой ответ, включив соответствующий исходный код. Кстати, тестировал с D2010. - person Uwe Raabe; 13.05.2010
comment
Я только что сделал еще один тест: работает даже без fmShareDenyWrite. Единственный недостаток, с которым я столкнулся до сих пор, заключается в том, что кажется невозможным писать в файл, когда он открыт для чтения (даже с fmShareDenyNone), но чтение из нескольких процессов не кажется проблемой. - person Uwe Raabe; 13.05.2010

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

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

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

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

person lkessler    schedule 12.05.2010

У меня была такая же проблема несколько лет назад, особенно проблема блокировки файла. Что я сделал, так это использовал файл чтения низкого уровня из шеллапи. Я знаю, что вопрос устарел с момента моего ответа (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;

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

person HpTerm    schedule 18.01.2012

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

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

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


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

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

person Deltics    schedule 12.05.2010
comment
Причина, по которой я действительно не хочу этого делать, в том, что это нелегко сделать правильно. Например, в вашем псевдокоде есть 3 тонких ошибки. И поэтому вместо того, чтобы заново изобретать глючное колесо, я лучше буду использовать готовый, проверенный код. - person Ian Boyd; 12.05.2010
comment
КАК он может содержать ошибки? Это псевдокод, предназначенный для иллюстрации идеи, а не РЕАЛЬНЫЙ код !! То, как вы реализуете реальный код, определит, содержит ли он ошибки или нет. Вы хотите обработать файл во время чтения с диска, а не обрабатывать его после чтения всего содержимого, а затем обрабатывать его, пока STREAMing - это именно то, что вам нужно (вы заметите, что все другие ответы являются вариациями на это тема!). Если вы уже имели представление о том, какой ответ хотели бы услышать, зачем вообще задавать этот вопрос? - person Deltics; 13.05.2010
comment
псевдокод используется для демонстрации алгоритма без неудобств, связанных с определенным языком. В этом случае алгоритм ошибочен. - person Ian Boyd; 02.06.2010
comment
Мне очень жаль, но если вам нужно получить полный рабочий код с ложечки даже в форме псевдокода на таком форуме, как этот, то, имхо, вам следует искать другую работу. Разработка программного обеспечения явно не для вас (и если вы достаточно хороши, чтобы замечать тонкие логические недостатки, то вы достаточно хороши, чтобы писать НАСТОЯЩИЙ код без этих изъянов). FFS - person Deltics; 03.06.2010
comment
Единственная причина, по которой я смог обнаружить ошибки в то время, заключалась в том, что я потратил часы на решение проблемы. Если бы я написал это сегодня, я бы ошибся. И я не хочу ошибаться в коде; Я бы предпочел использовать проверенный фрагмент хорошо протестированного кода. (т.е. зачем изобретать велосипед). Я думаю, что часть того, чтобы быть хорошим программистом, - это распознавать проблемы до того, как они возникнут. - person Ian Boyd; 17.03.2011