在大型文本文件中查找和替换文本(Delphi XE5)

时间:2016-02-17 02:18:32

标签: delphi delphi-xe

我正在尝试查找和替换文本文件中的文本。我过去能够用以下方法做到这一点:

procedure SmallFileFindAndReplace(FileName, Find, ReplaceWith: string);
begin
  with TStringList.Create do
    begin
    LoadFromFile(FileName);
    Text := StringReplace(Text, Find, ReplaceWith, [rfReplaceAll, rfIgnoreCase]);
    SaveToFile(FileName);
    Free;
  end;
end;

然而,当文件相对较小时,上述工作正常;当文件大小类似170 Mb时,上述代码将导致以下错误: EOutOfMemory,消息“内存不足”enter image description here

我已成功尝试了以下内容,但运行需要很长时间:

procedure Tfrm_Main.button_MakeReplacementClick(Sender: TObject);
var
  fs : TFileStream;
  s  : AnsiString;
  //s  : string;
begin
  fs := TFileStream.Create(edit_SQLFile.Text, fmOpenread or fmShareDenyNone);
  try
    SetLength(S, fs.Size);
    fs.ReadBuffer(S[1], fs.Size);
  finally
    fs.Free;
  end;
  s := StringReplace(s, edit_Find.Text, edit_Replace.Text, [rfReplaceAll, rfIgnoreCase]);
  fs := TFileStream.Create(edit_SQLFile.Text, fmCreate);
  try
    fs.WriteBuffer(S[1], Length(S));
  finally
    fs.Free;
  end;
end;

我是“Streams”的新手并使用缓冲区。

有更好的方法吗?

谢谢。

5 个答案:

答案 0 :(得分:6)

第一个代码示例中有两个错误,第二个示例中有三个错误:

  1. 不要将整个大文件加载到内存中,尤其是在32位应用程序中。如果文件大小超过〜1 Gb,则总是会出现“内存不足”
  2. 由于重复的内存重新分配,
  3. StringReplace因大字符串而变慢
  4. 在第二个代码中,您不在文件中使用文本编码,因此(对于Windows)您的代码“认为”该文件具有UCS2编码(每个字符两个字节)。但是你得到的,如果文件编码是Ansi(每个字符一个字节)或UTF8(可变大小的char)?
  5. 因此,对于正确的查找和替换,您必须使用文件编码和读/写文件的部分,如LU RD所说:

    interface
    
    uses
      System.Classes,
      System.SysUtils;
    
    type
      TFileSearchReplace = class(TObject)
      private
        FSourceFile: TFileStream;
        FtmpFile: TFileStream;
        FEncoding: TEncoding;
      public
        constructor Create(const AFileName: string);
        destructor Destroy; override;
    
        procedure Replace(const AFrom, ATo: string; ReplaceFlags: TReplaceFlags);
      end;
    
    implementation
    
    uses
      System.IOUtils,
      System.StrUtils;
    
    function Max(const A, B: Integer): Integer;
    begin
      if A > B then
        Result := A
      else
        Result := B;
    end;
    
    { TFileSearchReplace }
    
    constructor TFileSearchReplace.Create(const AFileName: string);
    begin
      inherited Create;
    
      FSourceFile := TFileStream.Create(AFileName, fmOpenReadWrite);
      FtmpFile := TFileStream.Create(ChangeFileExt(AFileName, '.tmp'), fmCreate);
    end;
    
    destructor TFileSearchReplace.Destroy;
    var
      tmpFileName: string;
    begin
      if Assigned(FtmpFile) then
        tmpFileName := FtmpFile.FileName;
    
      FreeAndNil(FtmpFile);
      FreeAndNil(FSourceFile);
    
      TFile.Delete(tmpFileName);
    
      inherited;
    end;
    
    procedure TFileSearchReplace.Replace(const AFrom, ATo: string;
      ReplaceFlags: TReplaceFlags);
      procedure CopyPreamble;
      var
        PreambleSize: Integer;
        PreambleBuf: TBytes;
      begin
        // Copy Encoding preamble
        SetLength(PreambleBuf, 100);
        FSourceFile.Read(PreambleBuf, Length(PreambleBuf));
        FSourceFile.Seek(0, soBeginning);
    
        PreambleSize := TEncoding.GetBufferEncoding(PreambleBuf, FEncoding);
        if PreambleSize <> 0 then
          FtmpFile.CopyFrom(FSourceFile, PreambleSize);
      end;
    
      function GetLastIndex(const Str, SubStr: string): Integer;
      var
        i: Integer;
        tmpSubStr, tmpStr: string;
      begin
        if not(rfIgnoreCase in ReplaceFlags) then
          begin
            i := Pos(SubStr, Str);
            Result := i;
            while i > 0 do
              begin
                i := PosEx(SubStr, Str, i + 1);
                if i > 0 then
                  Result := i;
              end;
            if Result > 0 then
              Inc(Result, Length(SubStr) - 1);
          end
        else
          begin
            tmpStr := UpperCase(Str);
            tmpSubStr := UpperCase(SubStr);
            i := Pos(tmpSubStr, tmpStr);
            Result := i;
            while i > 0 do
              begin
                i := PosEx(tmpSubStr, tmpStr, i + 1);
                if i > 0 then
                  Result := i;
              end;
            if Result > 0 then
              Inc(Result, Length(tmpSubStr) - 1);
          end;
      end;
    
    var
      SourceSize: int64;
    
      procedure ParseBuffer(Buf: TBytes; var IsReplaced: Boolean);
      var
        i: Integer;
        ReadedBufLen: Integer;
        BufStr: string;
        DestBytes: TBytes;
        LastIndex: Integer;
      begin
        if IsReplaced and (not(rfReplaceAll in ReplaceFlags)) then
          begin
            FtmpFile.Write(Buf, Length(Buf));
            Exit;
          end;
    
        // 1. Get chars from buffer
        ReadedBufLen := 0;
        for i := Length(Buf) downto 0 do
          if FEncoding.GetCharCount(Buf, 0, i) <> 0 then
            begin
              ReadedBufLen := i;
              Break;
            end;
        if ReadedBufLen = 0 then
          raise EEncodingError.Create('Cant convert bytes to str');
    
        FSourceFile.Seek(ReadedBufLen - Length(Buf), soCurrent);
    
        BufStr := FEncoding.GetString(Buf, 0, ReadedBufLen);
        if rfIgnoreCase in ReplaceFlags then
          IsReplaced := ContainsText(BufStr, AFrom)
        else
          IsReplaced := ContainsStr(BufStr, AFrom);
    
        if IsReplaced then
          begin
            LastIndex := GetLastIndex(BufStr, AFrom);
            LastIndex := Max(LastIndex, Length(BufStr) - Length(AFrom) + 1);
          end
        else
          LastIndex := Length(BufStr);
    
        SetLength(BufStr, LastIndex);
        FSourceFile.Seek(FEncoding.GetByteCount(BufStr) - ReadedBufLen, soCurrent);
    
        BufStr := StringReplace(BufStr, AFrom, ATo, ReplaceFlags);
        DestBytes := FEncoding.GetBytes(BufStr);
        FtmpFile.Write(DestBytes, Length(DestBytes));
      end;
    
    var
      Buf: TBytes;
      BufLen: Integer;
      bReplaced: Boolean;
    begin
      FSourceFile.Seek(0, soBeginning);
      FtmpFile.Size := 0;
      CopyPreamble;
    
      SourceSize := FSourceFile.Size;
      BufLen := Max(FEncoding.GetByteCount(AFrom) * 5, 2048);
      BufLen := Max(FEncoding.GetByteCount(ATo) * 5, BufLen);
      SetLength(Buf, BufLen);
    
      bReplaced := False;
      while FSourceFile.Position < SourceSize do
        begin
          BufLen := FSourceFile.Read(Buf, Length(Buf));
          SetLength(Buf, BufLen);
          ParseBuffer(Buf, bReplaced);
        end;
    
      FSourceFile.Size := 0;
      FSourceFile.CopyFrom(FtmpFile, 0);
    end;
    

    使用方法:

    procedure TForm2.btn1Click(Sender: TObject);
    var
      Replacer: TFileSearchReplace;
      StartTime: TDateTime;
    begin
      StartTime:=Now;
      Replacer:=TFileSearchReplace.Create('c:\Temp\123.txt');
      try
        Replacer.Replace('some текст', 'some', [rfReplaceAll, rfIgnoreCase]);
      finally
        Replacer.Free;
      end;
    
      Caption:=FormatDateTime('nn:ss.zzz', Now - StartTime);
    end;
    

答案 1 :(得分:2)

您的第一次尝试在内存中创建了多个文件副本:

  1. 它将整个文件加载到内存(TStringList)
  2. 在访问.Text属性时创建此内存的副本
  3. 当将该字符串传递给StringReplace时,它会创建此内存的另一个副本(该副本是在StringReplace中构建的结果。)
  4. 您可以尝试通过删除这些副本中的一个或多个来解决内存不足问题:

    e.g。将文件读入一个简单的字符串变量而不是TStringList 或保留字符串列表,但分别在每一行上运行StringReplace,并逐行将结果写入文件。

    这会增加代码可以处理的最大文件大小,但是对于大文件,你仍然会耗尽内存。如果你想处理任何大小的文件,你的第二种方法是可行的方法。

答案 2 :(得分:1)

不 - 我认为第二个选项的速度更快(如果你想要一个完全通用的搜索&#39; n替换任何大小的任何文件的功能)。如果您根据自己的要求进行专门编码,则可以制作更快的版本,但作为通用搜索替换功能,我不相信您可以更快...

例如,您确定是否需要不区分大小写的替换?我希望这将是替换功能花费的大部分时间。尝试(仅用于踢)删除该要求,看看它是否不会在大文件上加快执行速度(这取决于如何进行StringReplace函数的内部编码 - 如果它具有特定的优化对于区分大小写的搜索)

答案 3 :(得分:0)

我认为需要对Kami的代码进行细化以解释未找到的字符串,但字符串的新实例的开始可能发生在缓冲区的末尾。 else子句不同:

if IsReplaced then begin
    LastIndex := GetLastIndex(BufStr, AFrom);
    LastIndex := Max(LastIndex, Length(BufStr) - Length(AFrom) + 1);
end else
    LastIndex :=Length(BufStr) - Length(AFrom) + 1;

答案 4 :(得分:0)

正确解决这个问题:

if IsReplaced then
begin
    LastIndex := GetLastIndex(BufStr, AFrom);
    LastIndex := Max(LastIndex, Length(BufStr) - Length(AFrom) + 1);
end
else
  if FSourceFile.Position < SourceSize then
    LastIndex := Length(BufStr) - Length(AFrom) + 1
  else
    LastIndex := Length(BufStr);