Как удалить определенную строку из текстового файла в Делфи

у меня есть текстовый файл с информацией о пользователе, хранящейся в нем строка за строкой. Каждая строка имеет формат: UserID#UserEmail#UserPassword с разделителем"#".

Я попытался использовать эту кодировку для выполнения задачи:

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

но я не уверен, что поместить в пространство "индекс".

есть ли способ получить идентификатор пользователя в качестве входных данных, а затем удалить строку текста из текстового файла с этим идентификатором пользователя? Любая помощь будет оценена.

6 ответов


вы можете установить NameValueSeparator to # затем используйте 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 может быть различным.


я не понимаю, почему так много людей делают это так сложно. Это довольно просто:

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;

как Ариох там говорит, Если вы сохраните с тем же именем файла, вы рискуете потерять свои данные при этом сэкономить не удастся, так что вы можете сделать что-то вроде

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 до разделителя, а не строки с идентификатором пользователя, который только содержит the UserID вы ищете. IOW, if UserID is 'velthuis', вы не хотите, чтобы удалить строки 'rudyvelthuis#rvelthuis01#password' или 'velthuisresidence#vr#password2', а вы хотите удалить 'velthuis#bla#pw3'.

Е. Г. при поиске имени пользователя, вы ищите '#' + UserName + '#' по той же причине.


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

поэтому вам лучше просто сделать это явно.

и не забывайте о защите от ошибок. Ваш текущий код может просто уничтожить файл и утечку памяти, если произойдет какая-либо ошибка...

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: см., если проверка имени пользователя должна быть чувствительной к регистру или случай-игнорирование:

примечание 2: в Delphi 7 SizeOf( s[1] ) всегда равно единице, потому что string - это псевдоним для AnsiString. Но в более новой версии Delphi это не так. Это может показаться утомительным и избыточным, но это может сэкономить много головной боли в будущем. Еще лучше бы быть иметь временное AnsiString введите переменную типа a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));


до сих пор все предлагали использовать для For..Тогда цикл, но могу ли я предложить повторить..В то время как.

традиционный 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;

после вызова функция возвращает 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;

объяснение

  1. он загружает содержимое файла в строку.
  2. строка отправляется в TRegEx.Заменить
  3. регулярное выражение ищет имя пользователя, за которым следует знак хэша, затем любой символ, а затем CRLF. Он заменяет его пустой строкой.
  4. полученная строка затем записывается в оригинал файл

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