替换文件中的字符(更快的方法)

3
我们经常用另一个“好”的字符替换文件中的不良字符。
界面如下:
procedure cleanfileASCII2(vfilename: string; vgood: integer; voutfilename: string);

为了将所有非期望的字符替换为空格,我们可以调用 cleanfileASCII2(original.txt, 32 , cleaned.txt)。

问题是这需要很长时间。有没有比所示方法更好的方法?

procedure cleanfileASCII2(vfilename: string; vgood: integer; voutfilename:
string);
var
  F1, F2: file of char;
  Ch: Char;
  tempfilename: string;
  i,n,dex: integer;
begin
   //original
    AssignFile(F1, vfilename);
    Reset(F1);
    //outputfile
    AssignFile(F2,voutfilename);
    Rewrite(F2);
      while not Eof(F1) do
      begin
        Read(F1, Ch);
        //
          n:=ord(ch);
          if ((n<32)or(n>127))and (not(n in [10,13])) then
             begin // bad char
               if vgood<> -1 then
                begin
                ch:=chr(vgood);
                Write(F2, Ch);
                end
             end
           else   //good char
            Write(F2, Ch);
      end;
    CloseFile(F2);
    CloseFile(F1);
end;
7个回答

6
问题与您如何处理缓冲区有关。内存传输是任何操作中最昂贵的部分。在这种情况下,您正在逐字节查看文件。通过更改为块读取或缓冲读取,您将实现巨大的速度增加。请注意,正确的缓冲区大小因读取位置而异。对于网络文件,由于TCP/IP所施加的数据包大小,您会发现极大的缓冲区可能不太有效。即使对于来自gigE的大数据包,这一点也变得有些模糊,但总之,最好的结果是对其进行基准测试。
我只是出于方便将标准读取转换为文件流。您可以轻松地使用块读取做同样的事情。在这种情况下,我拿了一个15MB的文件并运行了您的例程。在本地文件上执行该操作需要131,478毫秒。使用1024缓冲区,则只需258毫秒。
procedure cleanfileASCII3(vfilename: string; vgood: integer; voutfilename:string);
const bufsize=1023;
var
  inFS, outFS:TFileStream;
  buffer: array[0..bufsize] of byte;
  readSize:integer;
  tempfilename: string;
  i: integer;
begin
   if not FileExists(vFileName) then exit;

   inFS:=TFileStream.Create(vFileName,fmOpenRead);
   inFS.Position:=0;
   outFS:=TFileStream.Create(vOutFileName,fmCreate);
   while not (inFS.Position>=inFS.Size) do
      begin
      readSize:=inFS.Read(buffer,sizeof(buffer));
      for I := 0 to readSize-1 do
          begin
          n:=buffer[i];
          if ((n<32)or(n>127)) and (not(n in [10,13])) and (vgood<>-1) then
             buffer[i]:=vgood;
          end;
      outFS.Write(buffer,readSize);
      end;
   inFS.Free;
   outFS.Free;
end;

2

几个改进:

  1. 缓冲数据,读取2k或16k或类似大小的块
  2. 使用查找表

这里是一个尝试,未经测试(现在我面前没有编译器):

procedure cleanfileASCII2(vfilename: string; vgood: integer; voutfilename: string);
var
    f1, f2: File;
    table: array[Char] of Char;
    index, inBuffer: Integer;
    buffer: array[0..2047] of Char;
    c: Char;
begin
    for c := #0 to #31 do
        table[c] := ' ';
    for c := #32 to #127 do
        table[c] := c;
    for c := #128 to #255 do
        table[c] := ' ';
    table[#10] := #10; // exception to spaces <32
    table[#13] := #13; // exception to spaces <32

    AssignFile(F1, vfilename);
    Reset(F1, 1);
    AssignFile(F2,voutfilename);
    Rewrite(F2, 1);
    while not Eof(F1) do
    begin
        BlockRead(f1, buffer, SizeOf(buffer), inBuffer);
        for index := 0 to inBuffer - 1 do
          buffer[index] := table[buffer[index]];
        BlockWrite(f2, buffer, inBuffer);
    end;
    Close(f2);
    Close(f1);
end;

1
+1 对于缓冲,但我不认为查找会有任何显著的差异。 - H H
+1 Henk。另外,Lasse可以将表格初始化更改为三行和单个循环(注释中不可用格式):FillChar(table,sizeof(table),#32); 对于c:=#32到#127 do table [c]:= c; table [#10]:=#10; table [#13]:=#13; - Ken White

1

缓冲是正确的处理方式。我修改了你的代码以查看差异:

procedure cleanfileASCII2(vfilename: string; vgood: integer; voutfilename:
string);
var
  F1, F2: file;
  NumRead, NumWritten: Integer;
  Buf: array[1..2048] of Char;
  Ch: Char;
  i, n: integer;
begin
    AssignFile(F1, vfilename);
    Reset(F1, 1); // Record size = 1
    AssignFile(F2, voutfilename);
    Rewrite(F2, 1); // Record size = 1
    repeat
      BlockRead(F1, Buf, SizeOf(Buf), NumRead);
      for i := 1 to NumRead do
      begin
        Ch := Buf[i];
        //
        n := ord(ch);
        if ((n<32)or(n>127))and (not(n in [10,13])) then
        begin // bad char
         if vgood <> -1 then
         begin
           ch := chr(vgood);
           Buf[i] := Ch;
         end
        //else   //good char
         //Write(F2, Ch);
        end;
      end;
      BlockWrite(F2, Buf, NumRead, NumWritten);
    until (NumRead = 0) or (NumWritten <> NumRead);
    CloseFile(F1);
    CloseFile(F2);
end;

1

你可以缓冲你的输入和输出,这样你就可以将一块字符(甚至是整个文件,如果它不太大)读入一个数组中,然后处理数组,最后将整个数组写入输出文件。

在大多数情况下,磁盘I/O是瓶颈所在,如果你可以做少量的大型读取,而不是许多小型读取,那么速度会更快。


0
我是这样做的,确保在处理之前将文件I/O一次性完成。该代码可能需要更新以支持Unicode,但它可以处理像null这样的恶劣文本字符,并为您提供TStrings功能。 Bri
procedure TextStringToStringsAA( AStrings : TStrings; const AStr: Ansistring);
// A better routine than the stream 'SetTextStr'.
// Nulls (#0) which might be in the file e.g. from corruption in log files
// do not terminate the reading process.
var
  P, Start, VeryEnd: PansiChar;
  S: ansistring;
begin
  AStrings.BeginUpdate;
  try
    AStrings.Clear;

    P := Pansichar( AStr );
    VeryEnd := P + Length( AStr );

    if P <> nil then
      while P < VeryEnd do
      begin
        Start := P;
        while (P < VeryEnd) and not CharInSet(P^, [#10, #13]) do
         Inc(P);
        SetString(S, Start, P - Start);
        AStrings.Add(string(S));
        if P^ = #13 then Inc(P);
        if P^ = #10 then Inc(P);
      end;
  finally
    AStrings.EndUpdate;
  end;
end;


procedure TextStreamToStrings( AStream : TStream; AStrings : TStrings );
// An alternative to AStream.LoadFromStream
// Nulls (#0) which might be in the file e.g. from corruption in log files
// do not terminate the reading process.
var
  Size : Integer;
  S    : Ansistring;
begin
  AStrings.BeginUpdate;
  try
    // Make a big string with all of the text
    Size := AStream.Size - AStream.Position;
    SetString( S, nil, Size );
    AStream.Read(Pointer(S)^, Size);

    // Parse it
    TextStringToStringsAA( AStrings, S );
  finally
    AStrings.EndUpdate;
  end;
end;

procedure LoadStringsFromFile( AStrings : TStrings; const AFileName : string );
// Loads this strings from a text file
// Nulls (#0) which might be in the file e.g. from corruption in log files
// do not terminate the reading process.
var
  ST : TFileStream;
begin
  ST := TFileStream.Create( AFileName, fmOpenRead + fmShareDenyNone);
  // No attempt is made to prevent other applications from reading from or writing to the file.
  try
    ST.Position := 0;
    AStrings.BeginUpdate;
    try
      TextStreamToStrings( ST, AStrings );
    finally
      AStrings.EndUpdate;
    end;

  finally
    ST.Free;
  end;
end;

1
如果你把 not CharInSet 替换为 (P^ <> #10) and (P^ <> #13),循环将会快得多。尽管 CharInSet 是内联的,但这不会改变任何事情。它使编译器无法生成最优化的代码。 - Andreas Hausladen
或者 'not P^ in [#10, #13]',这个也非常快。 - Ken White

0

不要试图在不知道瓶颈在哪里的情况下进行优化。

您应该使用采样分析器(delphitools.info)来了解瓶颈在哪里。它很容易使用。

在循环之前预先计算好vgood chr转换。

此外,您不需要一些转换:Ord()和Chr()。始终使用“Ch”变量。

if not (ch in [#10, #13, #32..#127]) then

如果你遵循自己的建议,你可能会发现预先计算vGood并没有什么区别(-: - H H

0

可能最简单的方法是:

  1. 创建另一个文件(临时文件)
  2. 将基本文件的所有内容逐行复制到临时文件中
  3. 检测读取到你想要替换的字符或单词时停止复制
  4. 对临时文件进行编辑
  5. 继续并完成将基本文件复制到临时文件中
  6. 重写(删除)基本文件的内容
  7. 将临时文件中的行复制到基本文件中
  8. 完成!

如果有帮助,请投票给这篇文章+1。


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