如何从 Delphi 中的文本文件中删除特定行
How to delete a specific line from a text file in Delphi
我有一个文本文件,其中逐行存储了用户信息。每行的格式为:UserID#UserEmail#UserPassword
以“#”作为分隔符。
我曾尝试使用此编码来执行任务:
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.Delete(Index);
sl.SaveToFile('filename');
sl.free;
end;
但我不确定要在 "index" space 中放什么。
有什么方法可以接收用户 ID 作为输入,然后从包含此用户 ID 的文本文件中删除该行文本?任何帮助将不胜感激。
您可以将 NameValueSeparator
设置为 #
然后使用 IndexOfName 查找用户,只要用户名是文件中的第一个值即可。
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
所以在你的例子中,就像这样
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;
这在大文件上可能会很慢,因为 IndexOfName 循环遍历 TStringList 中的每一行并依次检查每个字符串,直到找到匹配项。
免责声明:已测试/适用于 Delphi 2007,Delphi 7 可能不同。
实际上 "delete a line from the text file" 的唯一方法是创建一个内容已更改的新文件,然后重写它。
所以你最好明确地做。
不要忘记防止错误。如果发生任何错误,您当前的代码可能只会破坏文件并泄漏内存...
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;
注意 1:查看用户名检查是区分大小写还是忽略大小写:
- http://www.freepascal.org/docs-html/rtl/strutils/ansistartsstr.html
- http://www.freepascal.org/docs-html/rtl/strutils/ansistartstext.html
注意 2:在 Delphi 7 中,SizeOf( s[1] )
始终等于 1,因为 string
是 AnsiString
的别名。但在较新的 Delphi 版本中它不是。这可能看起来乏味和多余 - 但它可能会在未来避免很多麻烦。更好的办法是有一个临时的 AnsiString
类型变量,例如 a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));
我不明白为什么这么多人让这件事变得如此困难。很简单:
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;
正如 Arioch'The 所说,如果你保存到相同的文件名,当保存失败时你有丢失数据的风险,所以你可以这样做
SL.SaveToFile(FileName + '.dup');
if FileExists(FileName + '.old') then
DeleteFile(FileName + '.old');
RenameFile(FileName, FileName + '.old');
RenameFile(FileName + '.dup', FileName);
将原始文件备份为FileName + '.old'
。
解释
向后工作
为什么要逆向工作?因为如果你有以下项目
A B C D E F G
^
并且您删除了^
处的项目,则以下项目将向下移动:
A B C E F G
^
如果你向前迭代,你现在将指向
A B C E F G
^
和 E
从未被检查过。如果你往回走,那么你会指向:
A B C E F G
^
注意 E
、F
和 G
已经检查过,所以现在您确实要检查下一项 C
,您不会错过任何。另外,如果你向上使用0 to Count - 1
,然后删除,Count
会少一个,最后,你会尝试越过列表的边界。如果您使用 Count - 1 downto 0
.
向后工作,则不会发生这种情况
使用+ '#'
如果您追加 '#'
并测试 Pos() = 1
,您一定会捕获整个 UserID
直到分隔符,而不是包含仅包含用户 ID 的行包含 您正在寻找的UserID
。 IOW,如果 UserID
是 'velthuis'
,您不想删除像 'rudyvelthuis#rvelthuis01#password'
或 'velthuisresidence#vr#password2'
这样的行,但您确实想删除 'velthuis#bla#pw3'
.
例如查找用户名时,出于同样的原因查找 '#' + UserName + '#'
。
到目前为止,每个人都建议使用 For..Then 循环,但我可以建议使用 Repeat..While。
传统的 For..Loop 是一个不错的选择,但如果您的用户名列表很长(它们通常是唯一的),则效率可能会很低。一旦找到并删除 For 循环就会继续,直到列表结束。如果您的列表很小,那没关系,但如果您有 500,000 个用户名,而您想要的用户名位于第 10,000 位,则没有理由继续超出该点。
因此,试试这个。
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;
一旦调用了函数returns True 或 False 表示用户名是否被删除。
If Not DeleteUsername(TheFile,TheUsername) then
ShowMessage('User was not found, what were you thinking!');
只是为了好玩,这里有一个紧凑的解决方案,我喜欢它的可读性。
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;
显然这不是最有效的解决方案。这可以 运行 通过不将单个项目附加到数组或通过缓存搜索字符串来更快。
要搜索其他字段,您可以使用 s.split(['#'])[0]
查找用户名,s.split(['#'])[1]
查找电子邮件等。
对于那些喜欢单线的人。这也有效:
const fn = 'users.txt';
procedure DeleteUserRegExp(id: string);
begin
TFile.WriteAllText(fn,TRegEx.Replace(TFile.ReadAllText(fn),''+id+'\#.*\r\n',''))
end;
说明
- 它将文件的内容加载到字符串中。
- 字符串发送到TRegEx.Replace
- 正则表达式搜索用户名,后跟井号,然后是任意字符,然后是 CRLF。它用一个空字符串替换它。
- 然后将生成的字符串写入原始文件
虽然这只是为了好玩,因为我看到了很长的代码,我认为用一行代码就可以做到这一点。
我有一个文本文件,其中逐行存储了用户信息。每行的格式为:UserID#UserEmail#UserPassword
以“#”作为分隔符。
我曾尝试使用此编码来执行任务:
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.Delete(Index);
sl.SaveToFile('filename');
sl.free;
end;
但我不确定要在 "index" space 中放什么。
有什么方法可以接收用户 ID 作为输入,然后从包含此用户 ID 的文本文件中删除该行文本?任何帮助将不胜感激。
您可以将 NameValueSeparator
设置为 #
然后使用 IndexOfName 查找用户,只要用户名是文件中的第一个值即可。
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
所以在你的例子中,就像这样
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;
这在大文件上可能会很慢,因为 IndexOfName 循环遍历 TStringList 中的每一行并依次检查每个字符串,直到找到匹配项。
免责声明:已测试/适用于 Delphi 2007,Delphi 7 可能不同。
实际上 "delete a line from the text file" 的唯一方法是创建一个内容已更改的新文件,然后重写它。
所以你最好明确地做。
不要忘记防止错误。如果发生任何错误,您当前的代码可能只会破坏文件并泄漏内存...
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;
注意 1:查看用户名检查是区分大小写还是忽略大小写:
- http://www.freepascal.org/docs-html/rtl/strutils/ansistartsstr.html
- http://www.freepascal.org/docs-html/rtl/strutils/ansistartstext.html
注意 2:在 Delphi 7 中,SizeOf( s[1] )
始终等于 1,因为 string
是 AnsiString
的别名。但在较新的 Delphi 版本中它不是。这可能看起来乏味和多余 - 但它可能会在未来避免很多麻烦。更好的办法是有一个临时的 AnsiString
类型变量,例如 a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));
我不明白为什么这么多人让这件事变得如此困难。很简单:
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;
正如 Arioch'The 所说,如果你保存到相同的文件名,当保存失败时你有丢失数据的风险,所以你可以这样做
SL.SaveToFile(FileName + '.dup');
if FileExists(FileName + '.old') then
DeleteFile(FileName + '.old');
RenameFile(FileName, FileName + '.old');
RenameFile(FileName + '.dup', FileName);
将原始文件备份为FileName + '.old'
。
解释
向后工作
为什么要逆向工作?因为如果你有以下项目
A B C D E F G
^
并且您删除了^
处的项目,则以下项目将向下移动:
A B C E F G
^
如果你向前迭代,你现在将指向
A B C E F G
^
和 E
从未被检查过。如果你往回走,那么你会指向:
A B C E F G
^
注意 E
、F
和 G
已经检查过,所以现在您确实要检查下一项 C
,您不会错过任何。另外,如果你向上使用0 to Count - 1
,然后删除,Count
会少一个,最后,你会尝试越过列表的边界。如果您使用 Count - 1 downto 0
.
使用+ '#'
如果您追加 '#'
并测试 Pos() = 1
,您一定会捕获整个 UserID
直到分隔符,而不是包含仅包含用户 ID 的行包含 您正在寻找的UserID
。 IOW,如果 UserID
是 'velthuis'
,您不想删除像 'rudyvelthuis#rvelthuis01#password'
或 'velthuisresidence#vr#password2'
这样的行,但您确实想删除 'velthuis#bla#pw3'
.
例如查找用户名时,出于同样的原因查找 '#' + UserName + '#'
。
到目前为止,每个人都建议使用 For..Then 循环,但我可以建议使用 Repeat..While。
传统的 For..Loop 是一个不错的选择,但如果您的用户名列表很长(它们通常是唯一的),则效率可能会很低。一旦找到并删除 For 循环就会继续,直到列表结束。如果您的列表很小,那没关系,但如果您有 500,000 个用户名,而您想要的用户名位于第 10,000 位,则没有理由继续超出该点。
因此,试试这个。
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;
一旦调用了函数returns True 或 False 表示用户名是否被删除。
If Not DeleteUsername(TheFile,TheUsername) then
ShowMessage('User was not found, what were you thinking!');
只是为了好玩,这里有一个紧凑的解决方案,我喜欢它的可读性。
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;
显然这不是最有效的解决方案。这可以 运行 通过不将单个项目附加到数组或通过缓存搜索字符串来更快。
要搜索其他字段,您可以使用 s.split(['#'])[0]
查找用户名,s.split(['#'])[1]
查找电子邮件等。
对于那些喜欢单线的人。这也有效:
const fn = 'users.txt';
procedure DeleteUserRegExp(id: string);
begin
TFile.WriteAllText(fn,TRegEx.Replace(TFile.ReadAllText(fn),''+id+'\#.*\r\n',''))
end;
说明
- 它将文件的内容加载到字符串中。
- 字符串发送到TRegEx.Replace
- 正则表达式搜索用户名,后跟井号,然后是任意字符,然后是 CRLF。它用一个空字符串替换它。
- 然后将生成的字符串写入原始文件
虽然这只是为了好玩,因为我看到了很长的代码,我认为用一行代码就可以做到这一点。