简体   繁体   English

如何从Delphi中的文本文件中删除特定行

[英]How to delete a specific line from a text file in Delphi

I have a text file with user information stored in it line by line. 我有一个文本文件,其中逐行存储了用户信息。 Each line is in the format: UserID#UserEmail#UserPassword with '#' being the delimiter. 每行的格式为: UserID#UserEmail#UserPassword其中'#'为分隔符。

I have tried to use this coding to perform the task: 我试图使用此编码来执行任务:

var sl:TStringList;
begin
  sl:=TStringList.Create;
  sl.LoadFromFile('filename');
  sl.Delete(Index);
  sl.SaveToFile('filename');
  sl.free;
end;

But I'm not sure what to put in the "index" space. 但我不确定在“索引”空间放什么。

Is there any way I can receive the User ID as input and then delete the line of text from the text file that has this user ID in? 有什么方法可以接收用户ID作为输入,然后从具有此用户ID的文本文件中删除文本行? Any help would be appreciated. 任何帮助,将不胜感激。

You can set the NameValueSeparator to # then use IndexOfName to find the user, as long as the username is the first value in the file. 您可以将NameValueSeparator设置为#然后使用IndexOfName查找用户,只要用户名是文件中的第一个值即可。

sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')

So in your example, like so 所以在你的例子中,就像这样

var sl:TStringList;
begin
  sl:=TStringList.Create;
  sl.LoadFromFile('filename');
  sl.NameValueSeparator := '#';
  Index := sl.IndexOfName('455115')
  if (Index  <> -1) then
  begin
      sl.Delete(Index);
      sl.SaveToFile('filename');
  end;
  sl.free;
end;

This may be slow on large files as IndexOfName loops though each line in the TStringList and checks each string in turn until it finds a match. 对于大型文件,这可能会很慢,因为IndexOfName会循环访问TStringList中的每一行,并依次检查每个字符串,直到找到匹配项为止。

Disclaimer: Tested/ works with Delphi 2007, Delphi 7 may be diffrent. 免责声明:测试/使用Delphi 2007,Delphi 7可能不同。

I don't see why so many people make this so hard. 我不明白为什么这么多人如此努力。 It is quite simple: 这很简单:

function ShouldDeleteLine(const UserID, Line: string): Boolean;
begin    
  // Remember: Pos(Needle, Haystack)
  Result := Pos(UserID + '#', Line) = 1; // always 1-based!
end;

procedure DeleteLinesWithUserID(const FileName, UserID: string);
var
  SL: TStringList;
  I: Integer;
begin
  if not FileExists(FileName) then
    Exit;

  SL := TStringList.Create;
  try
    SL.LoadFromFile(FileName); // Add exception handling for the 
                               // case the file does not load properly.

    // Always work backward when deleting items, otherwise your index
    // may be off if you really delete.
    for I := SL.Count - 1 downto 0 do
      if ShouldDeleteLine(SL[I], UserID) then
      begin
        SL.Delete(I);
        // if UserID is unique, you can uncomment the following line.
        // Break;
      end;
    SL.SaveToFile(FileName);
  finally
    SL.Free;
  end;
end;

As Arioch'The says, if you save to the same file name, you risk losing your data when the save fails, so you can do something like 正如Arioch所说,如果你保存到相同的文件名,你可能会在保存失败时丢失数据,所以你可以做类似的事情

SL.SaveToFile(FileName + '.dup');
if FileExists(FileName + '.old') then
  DeleteFile(FileName + '.old');
RenameFile(FileName, FileName + '.old');
RenameFile(FileName + '.dup', FileName);

That keeps a backup of the original file as FileName + '.old' . 这使原始文件的备份保持为FileName + '.old'

Explanations 说明

Working backward 向后工作

Why work backward? 为什么要倒退? Because if you have the following items 因为如果你有以下物品

A B C D E F G
      ^

And you delete the item at ^ , then the following items will shift downward: 并且您在^处删除该项目,然后以下项目将向下移动:

A B C E F G
      ^

If you iterate forward, you will now point to 如果你向前迭代,你现在将指向

A B C E F G
        ^

and E is never examined. 从未检查过E If you go backward, then you will point to: 如果你倒退,那么你会指出:

A B C E F G
    ^

Note that E , F and G were examined already, so now you will indeed examine the next item, C , and you won't miss any. 请注意, EFG已经被检查过了,所以现在你确实会检查下一个项目C ,你不会错过任何一个。 Also, if you go upward using 0 to Count - 1 , and delete, Count will become one less and at the end, you will try to access past the boundary of the list. 此外,如果向上使用0 to Count - 1 ,并且删除, Count将减少一个,最后,您将尝试访问列表的边界。 This can't happen if you work backwards using Count - 1 downto 0 . 如果您使用Count - 1 downto 0向后工作,则不会发生这种情况。

Using + '#' 使用+ '#'

If you append '#' and test for Pos() = 1 , you will be sure to catch the entire UserID up to the delimiter, and not a line with a user ID that only contains the UserID you are looking for. 如果附加'#'并测试Pos() = 1 ,您将确保将整个UserID捕获到分隔符,而不是具有仅包含您要查找的UserID的用户ID的行。 IOW, if UserID is 'velthuis' , you don't want to delete lines like 'rudyvelthuis#rvelthuis01#password' or 'velthuisresidence#vr#password2' , but you do want to delete 'velthuis#bla#pw3' . IOW,如果UserID'velthuis' ,你不想删除像'rudyvelthuis#rvelthuis01#password''velthuisresidence#vr#password2' ,但你确实要删除'velthuis#bla#pw3'

Eg when looking for a user name, you look for '#' + UserName + '#' for the same reason. 例如,在查找用户名时,出于同样的原因,您会查找'#' + UserName + '#'

There is the only way to actually "delete a line from the text file" - that is to create a new file with changed content, to REWRITE it. 有唯一的方法来实际“从文本文件中删除一行” - 即创建一个具有更改内容的新文件,以REWRITE它。

So you better just do it explicitly. 所以你最好明确地做。

And don't you forget about protecting from errors. 你不要忘记防止错误。 Your current code might just destroy the file and leak memory, if any error occurs... 如果发生任何错误,您当前的代码可能只会破坏文件并泄漏内存...

var sl: TStringList;
    s, prefix: string;
    i: integer; okay: Boolean;
    fs: TStream;

begin
  prefix := 'UserName' + '#';
  okay := false;

  fs := nil;
  sl:=TStringList.Create;
  Try   /// !!!!
    sl.LoadFromFile('filename');
    fs := TFileStream.Create( 'filename~new', fmCreate or fmShareExclusive );

    for i := 0 to Prev(sl.Count) do begin
      s := sl[ i ];

      if AnsiStartsStr( prefix, Trim(s) ) then
         continue;  // skip the line - it was our haunted user

      s := s + ^M^J;  // add end-of-line marker for saving to file

      fs.WriteBuffer( s[1], length(s)*SizeOf(s[1]) );  
    end; 
  finally 
    fs.Free;
    sl.Free;
  end;

  // here - and only here - we are sure we successfully rewritten 
  // the fixed file and only no are able to safely delete old file
  if RenameFile( 'filename' , 'filename~old') then
     if RenameFile( 'filename~new' , 'filename') then begin
        okay := true; 
        DeleteFile( 'filename~old' ); 
     end;

  if not okay then ShowMessage(' ERROR!!! ');
end;

Note 1: See if check for username should be case-sensitive or case-ignoring: 注1:查看用户名检查是区分大小写还是忽略大小写:

Note 2: in Delphi 7 SizeOf( s[1] ) is always equal to one because string is an alias to AnsiString . 注2:在Delphi 7中, SizeOf( s[1] )总是等于1,因为stringAnsiString的别名。 But in newer Delphi version it is not. 但在较新的Delphi版本中却没有。 It might seems tedious and redundant - but it might save a LOT of headache in future. 这可能看起来很乏味和多余 - 但它可能会在未来挽救很多头痛。 Even better would be to have a temporary AnsiString type variable like a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a)); 更好的是拥有一个临时的AnsiString类型变量,如a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a)); a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));

So far everyone has been suggesting the use for a For..Then Loop but can I suggest a Repeat..While . 到目前为止,每个人都建议使用For..Then循环,但是我可以建议重复...而不是

The traditional For..Loop is a good option but could be inefficient if you have a long list of Usernames (they are usually unique). 传统的For..Loop是一个不错的选择,但如果你有很长的用户名列表(它们通常是唯一的),效率可能会很低。 Once found and deleted the For Loop continues until the end of the list. 一旦找到并删除, For循环将继续,直到列表结束。 That's ok if you have a small list but if you have 500,000 Usernames and the one you want is at position 10,000 there is no reason to continue beyond that point. 如果你有一个小的列表,但如果你有500,000个用户名,而你想要的那个位于10,000,那么没有理由继续超过这一点。

Therefore, try this. 因此,试试这个。

    Function DeleteUser(Const TheFile: String; Const TheUserName: String): Boolean;
    Var
      CurrentLine: Integer;
      MyLines: TStringlist;
      Found: Boolean;
      Eof: Integer;

    Begin

      MyLines := TStringlist.Create;
      MyLines.LoadFromFile(TheFile);

      CurrentLine := 0;
      Eof := Mylines.count - 1; 

      Found := false;

      Repeat 

        If Pos(UpperCase(TheUserName), UpperCase(MyLines.Strings[CurrentLine])) = 1 Then
        Begin

         MyLines.Delete(CurrentLine);
          Found := True;

        End;

        Inc(CurrentLine);

      Until (Found) Or (CurrentLine = Eof); // Jump out when found or End of File

      MyLines.SaveToFile(TheFile);
      MyLines.Free;

      result := Found;
    End;

Once called the function returns True or False indicating the Username was deleted or not. 一旦被调用,该函数返回True或False,表示用户名已被删除。

If Not DeleteUsername(TheFile,TheUsername) then
ShowMessage('User was not found, what were you thinking!');

Just for fun, here's a compact solution, which I like for its readability. 只是为了好玩,这是一个紧凑的解决方案,我喜欢它的可读性。

const fn = 'myfile.txt';

procedure DeleteUser(id: integer);
var s:string; a:TStringDynArray;
begin
  for s in TFile.ReadAllLines(fn) do
    if not s.StartsWith(id.ToString + '#') then
      a := a + [s];

  TFile.WriteAllLines(fn, a);
end;

Obviously it's not the most efficient solution. 显然,这不是最有效的解决方案。 This could run faster by not appending single items to the array, or by caching the search string. 通过不向数组附加单个项目或通过缓存搜索字符串,可以更快地运行。

And to search for other fields, you could use s.split(['#'])[0] to find the username, s.split(['#'])[1] for email, etc. 要搜索其他字段,您可以使用s.split(['#'])[0]查找用户名, s.split(['#'])[1]用于发送电子邮件等。

For those who like one-liners. 对于那些喜欢单行的人。 This works too: 这也有效:

const fn = 'users.txt';

procedure DeleteUserRegExp(id: string);
begin
  TFile.WriteAllText(fn,TRegEx.Replace(TFile.ReadAllText(fn),''+id+'\#.*\r\n',''))
end;

Explanation 说明

  1. It loads the content of a file into a string. 它将文件的内容加载到字符串中。
  2. The string is sent to TRegEx.Replace 该字符串被发送到TRegEx.Replace
  3. The regular expression searches for the username followed by the hash sign, then any character, and then a CRLF. 正则表达式搜索用户名,后跟哈希符号,然后搜索任何字符,然后搜索CRLF。 It replaces it with an empty string. 它用空字符串替换它。
  4. The resulting string is then written to the original file 然后将生成的字符串写入原始文件

This is just for fun though, because I saw long code where I thought that this would be possible with a single line of code. 这只是为了好玩,因为我看到了很长的代码,我认为这可以用一行代码实现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM