Удалить символы из текстового файла при добавлении строк с пробелами

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

Например. Мой текстовый файл:

Этот файл имеет

две линии

Какой-то код C#, подобный этому:

string str = "...\b\b\b\b\b\b\b\b\b\b\b\b\b one line."
myFile.Append(str);

И после выполнения этого кода текстовый файл выглядит так:

В этом файле одна строка.

Классы StreamWriter и File, кажется, мало помогают.

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

Моя вторая проблема заключается в том, как обращаться с символами новой строки в стиле Windows (\r\n)? то есть один backspace должен удалить целую последовательность символов новой строки (\r\n).

Любые идеи о том, как это реализовать?

Исходный код будет высоко оценен.


person sɐunıɔןɐqɐp    schedule 18.07.2018    source источник
comment
Итак, вы просто хотите удалить последние n символов файла перед добавлением к нему?   -  person TheGeneral    schedule 18.07.2018
comment
На самом деле нет, строка может содержать символы до того, как в ней появятся символы возврата. Как и в приведенном примере, в начале строки есть три символа точки (.). Но строка действительно всегда будет добавляться к текстовому файлу. (Я отредактировал пример, чтобы было понятно)   -  person sɐunıɔןɐqɐp    schedule 18.07.2018
comment
Короткий ответ - нет. Длинный ответ: вам придется создавать функциональность самостоятельно, используя FileStream некоторого описания Position или Seek, и старое доброе кодирование.   -  person TheGeneral    schedule 18.07.2018
comment
@TheGeneral Вероятно, это сложнее, чем использование FileStream, учитывая кодировку :-) (см., например, никнейм, используемый спрашивающим). К сожалению, в .NET нет унифицированного StreamReaderWriter, поэтому изменение закодированного текстового файла становится проблемой.   -  person xanatos    schedule 18.07.2018
comment
@xanatos очень хороший момент   -  person TheGeneral    schedule 18.07.2018


Ответы (2)


Сделать это "правильно" в "самом общем случае" очень-очень, очень сложно. В .NET нет прямой поддержки. Давайте посмотрим на состояние искусства:

  • Есть класс FileStream... Он доступен для чтения/записи. К сожалению, он не знает о кодировании и работает в байтах. Так что нет UTF-8 и нет изначально Unicode. Видишь свой красивый ник sɐunıɔןɐqɐp? Это явно нуждается в некоторой кодировке :-)

  • StreamReader и StreamWriter могут быть "подключены" к FileStream... К сожалению, они разные (один только для чтения, другой только для записи), и, к сожалению, они предварительно буферизуются, так что FileStream.Position не соответствует текущему "чтению". " символ в StreamReader. Это делает чтение с помощью StreamReader, а затем внесение исправлений «на месте» с помощью StreamWriter довольно сложным.

  • Даже если бы у нас было StreamReaderWriter, это было бы немного сложно. .NET работает с UTF-16 chars, поэтому многие символы Юникода (смайлики, такие как ???? ухмыляющееся лицо например) состоят из двух char... Таким образом, одному \b, вероятно, потребуется стереть один или два char (и от 1 до 4 байтов в UTF-8), в зависимости от того, что он находит .

  • Обратите внимание, что более сложные смайлики (например,  ????‍????‍????  family) состоит из нескольких одиночных смайликов (4 кодовых точки юникода, соответствующих 11 .net char, что соответствует 25 байтам в UTF-8), но мы проигнорируем эту проблему.

Самое простое решение — загрузить весь файл в память внутри string (или аналогичного), изменить его, а затем перезаписать на диск. И даже здесь остерегайтесь конца строки, это могут быть два символа (\r\n), тогда как "логически" это один символ (если вы находитесь в начале строки в блокноте и нажимаете одиночный пробел, это полностью удалит \r\n). Но, как вы заметили, это решение «медленное» :-)

Другое решение со многими ограничениями. Как я уже писал в комментарии, можно было сделать наоборот: сохранить Position перед записью, написать, если нужно исправить изменить Position обратно, перезаписать, SetLength() обрезать лишний файл, если он есть. Это ограничивает проблему случаями, когда вы можете изменить только текстовую часть, которую вы написали в текущем сеансе, и, как правило, вы можете изменить только «последнюю» часть файла.

public static long WriteAppend(this FileStream fs, string str, Encoding enc)
{
    long pos = fs.Length;
    fs.Position = pos;
    byte[] bytes = enc.GetBytes(str);
    fs.Write(bytes, 0, bytes.Length);
    return pos;
}


public static long RewriteTruncate(this FileStream fs, long pos, string str, Encoding enc)
{
    fs.Position = pos;
    byte[] bytes = enc.GetBytes(str);
    fs.Write(bytes, 0, bytes.Length);
    fs.SetLength(pos + bytes.Length);
    return pos;
}

Использовать:

int secs = 5;

using (var fs = new FileStream("Hello.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
{
    fs.WriteAppend("Beginning of the elaboration\r\n", Encoding.UTF8);

    long pos1 = fs.WriteAppend("Step 1\r\n", Encoding.UTF8);
    long pos2 = fs.WriteAppend($"Working 0\r\n", Encoding.UTF8);

    for (int i = 1; i < 10; i++)
    {
        Thread.Sleep(secs * 1000);
        fs.RewriteTruncate(pos2, $"Working {i}\r\n", Encoding.UTF8);
    }

    Thread.Sleep(secs * 1000);
    fs.RewriteTruncate(pos1, $"Finished working\r\n", Encoding.UTF8);
}

Не закрывайте выходной файл в Notepad++ и обновляйте его каждые несколько секунд.

person xanatos    schedule 18.07.2018
comment
Самое странное, что нужно учитывать так много моментов, но даже человек, не связанный с ИТ, поймет, чего я здесь пытаюсь достичь :-) - person sɐunıɔןɐqɐp; 18.07.2018
comment
@sɐunıɔןɐqɐp Вы можете сделать наоборот: сохранить Position перед записью, написать, если нужно исправить, изменить Position обратно, переписать, SetLength обрезать лишний файл, если он есть. Это ограничивает проблему случаями, когда вы можете изменить только текстовую часть, которую вы написали в текущем сеансе, и, как правило, вы можете изменить только последнюю часть файла. - person xanatos; 18.07.2018
comment
+1 Большое спасибо, xanatos, я поделился кодом, который был реализован на основе этого ответа. Хотя многие функции, которые вы указали, все еще не охвачены кодом, он обеспечивает хорошую основу для начала. - person sɐunıɔןɐqɐp; 18.07.2018

На основе ответа xanatos и Комментарий General, я написал этот прототип класса FileLogger, который оценивает строку (которая должна быть добавлена) в последовательность начальных символы возврата плюс оставшаяся строка (без пробелов).

Если есть начальные символы возврата, программа усекает объект FileStream на основе количества начальных символов возврата (очень наивным способом), а затем добавляет оставшуюся строку.

К сожалению, это решение НЕ учитывает любую последовательность новой строки \r\n, которая должна быть удалена одним возвратом, как из FileStream, так и из добавленной строки. Как и сейчас, для удаления одной последовательности символов новой строки в стиле Windows требуется два пробела.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

namespace Example
{
    public static class FileLogger
    {
        public static bool IsStarted { get; private set; }
        public static Encoding Encoding { get; private set; }
        public static string LogFilePath { get; private set; }

        private static FileStream FS;
        private static int BytesPerChar;
        private static readonly object Locker = new object();

        public static void Start(string logFilePath, Encoding encoding = null)
        {
            lock (Locker)
            {
                if (IsStarted) return;
                LogFilePath = logFilePath;
                Encoding = encoding ?? Encoding.UTF8;
                if (File.Exists(LogFilePath)) File.SetAttributes(LogFilePath, FileAttributes.Normal);
                FS = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, FileOptions.RandomAccess);
                FS.SetLength(0);
                FS.Flush();
                BytesPerChar = Encoding.UTF8.GetByteCount(new[] { 'A' });
                IsStarted = true;
            }
        }

        public static void Close()
        {
            lock (Locker)
            {
                if (!IsStarted) return;
                try { FS?.Close(); } catch { }
                FS = null;
                IsStarted = false;
            }
        }

        public static void WriteToFile(string text)
        {
            lock (Locker)
            {
                if (string.IsNullOrEmpty(text)) return;

                if (!text.Contains('\b'))
                {
                    FS.Position = FS.Length;
                    byte[] bytes = Encoding.GetBytes(text);
                    FS.Write(bytes, 0, bytes.Length);
                    FS.Flush();
                    return;
                }

                // Evaluates the the string into initial backspaces and remaining text to be appended:
                EvaluateText(text, out int initialBackspaces, out string remainingText);

                // If there are no initial backspaces after evaluating the string, just append it and return:
                if (initialBackspaces <= 0)
                {
                    if (string.IsNullOrEmpty(remainingText)) return;

                    FS.Position = FS.Length;
                    byte[] bytes = Encoding.GetBytes(remainingText);
                    FS.Write(bytes, 0, bytes.Length);
                    FS.Flush();
                    return;
                }

                // First process the initial backspaces:
                long pos = FS.Length - initialBackspaces * BytesPerChar;
                FS.Position = pos > 0 ? pos : 0;
                FS.SetLength(FS.Position);

                // Then write any remaining evaluated text:
                if (!string.IsNullOrEmpty(remainingText))
                {
                    byte[] bytes = Encoding.GetBytes(remainingText);
                    FS.Write(bytes, 0, bytes.Length);
                }
                FS.Flush();
                return;
            }
        }

        public static void EvaluateText(string text, out int initialBackspaces, out string remainingTextToAppend)
        {
            initialBackspaces = 0;
            StringBuilder sb = new StringBuilder();
            foreach (char ch in text)
            {
                if(ch == '\b')
                {
                    if (sb.Length > 0) sb.Length--;
                    else initialBackspaces++;
                }
                else sb.Append(ch);
            }
            remainingTextToAppend = sb.ToString();
        }
    }
}

Тестовый код:

FileLogger.Start("test.log");
FileLogger.WriteToFile("aaa\r\n");
FileLogger.WriteToFile("bbbb");
FileLogger.WriteToFile("\b");
FileLogger.WriteToFile("\b\b");
FileLogger.WriteToFile("\b\b\b\b");
FileLogger.WriteToFile("XXX");
FileLogger.WriteToFile("\b\bYY\bZ");
FileLogger.WriteToFile("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
FileLogger.WriteToFile("Done!");
FileLogger.Close();

Вывод (файл test.log):

ааXYZ

person sɐunıɔןɐqɐp    schedule 18.07.2018