使用退格符附加字符串时从文本文件中删除字符

Remove characters from text file when appending strings with backspaces

是否可以将包含退格符的字符串附加到文本文件并将其中的所有退格符视为“删除最后一个字符”操作?

例如我的文本文件:

This file has

two lines

像这样的某种 C# 代码:

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

执行此代码后,文本文件如下所示:

This file has one line.

StreamWriterFile 类 似乎没什么用。

如果不在每次追加操作时读取和写入整个文件,我无法找到实现此目的的最佳方法,这可能会导致大型文本文件出现严重的性能问题。这个想法是用这个新功能将日志语句集中写入文本文件。

我第二个关心的问题是如何处理windows式的换行符(“\r\n”)?即一个退格键应该删除整个单个换行符序列(“\r\n”)。

关于如何实现这个的任何想法?

源代码将不胜感激。

在 "most general case" 中做到 "correctly" 非常非常非常 困难。 .NET 中没有直接支持。让我们看看最先进的技术:

  • 有个FileStreamclass...就是read/write。可悲的是它不知道编码并且它以字节为单位工作。所以本机没有 UTF-8 和 Unicode。看到你漂亮的昵称sɐunıɔןɐqɐp了吗?它显然需要一些编码:-)

  • StreamReaderStreamWriter 可以是 "connected" 到 FileStream... 遗憾的是它们是分开的(一个只读,一个是只写),遗憾的是它们预先缓冲,因此 FileStream.Position 不对应于 StreamReader 中当前的 "read" 字符。这使得使用 StreamReader 读取然后使用 StreamWriter 进行更正 "in place" 非常复杂。

  • 就算我们有一个StreamReaderWriter,也会有点困难。 .NET 使用 UTF-16 chars,因此许多 Unicode 字符(例如像 grinning face 的表情符号)由两个 char 组成...所以一个 \b可能需要擦除一两个 char(以及 UTF-8 中的 1 到 4 个字节),具体取决于它找到的内容。

  • 请注意,更复杂的表情符号(如  ‍‍  family )由多个单个表情符号组成(4 个 unicode 代码点,对应 11.net char,对应 25 个字节UTF-8), 但我们会忽略这个问题

最简单的解决方案是将整个文件加载到 string(或类似文件)内存中,对其进行修改,然后将其重新写入磁盘。即使在这里,也要注意行尾,它可能是两个字符 (\r\n),而 "logically" 它们是单个字符(如果您在记事本中位于一行的开头并且按一次退格键,它将完全擦除 \r\n)。但正如您所注意到的,这个解决方案是 "slow" :-)

其他解决方案,有很多限制。正如我在评论中写的那样,你可以做相反的事情:在写之前保存 Position,写,如果你需要更正更改 Position 回来,重新写,SetLength()截断多余的文件(如果存在)。这将问题限制在您只能修改您在当前会话中编写的文本部分的情况,并且通常您只能修改文件的 "last" 部分。

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++ 中保持打开输出文件并每隔几秒刷新一次。

基于, and on ,我写了这个原型FileLogger class,它将字符串(待追加)评估为一系列初始退格字符加上剩余的字符串(没有任何退格键)。

如果有初始退格,程序会根据初始退格的数量截断 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 文件):

aaXYZ