【问题标题】:How can I know if a text file ends with carriage return or not?如何知道文本文件是否以回车符结尾?
【发布时间】:2017-05-29 16:12:34
【问题描述】:

我必须处理一个文本文件并检查它是否以回车符结尾。

我必须阅读全部内容,进行一些更改并将其重新写入目标文件,并保持与原始文件完全相同的格式。这就是问题所在:我不知道原始文件最后是否包含换行符。

我已经试过了:

  • StreamReader.ReadLine() 方法,但返回的字符串不包含终止回车和/或换行。
  • ReadToEnd() 方法也是一种解决方案,但我想知道在文件非常大的情况下的性能。解决方案必须有效。
  • 获取最后 2 个字符并检查它们是否等于 "\r\n" 可能会解决此问题,但我必须处理大量编码,而且实际上似乎无法获取它们。

如何有效地读取文件的所有文本并确定它是否以换行符结尾?

【问题讨论】:

  • 如果你无论如何都必须阅读整个内容,你为什么担心使用 ReadToEnd() ?
  • @PhillipH 因为 ReadToEnd() 返回文件的全部内容。
  • @CodeCaster,这行不通。根据编码,这些字符的含义可能完全不同。
  • @CodeCaster 例如,UCS-2 文件或 UTF-16 文件将以 00 0D 00 0A 之类的内容结尾,具体取决于字节序。每个字符(包括 ASCII)的长度为 16(或 32)位。
  • @CodeCaster 但是如果你不知道编码,你将不知道十六进制序列000D 000A是四个字符("\0\r\0\n"),两个字符("\r\n"),还是别的什么.如果你想一次读取一个字符,你必须关心编码(无论一个字符总是 8 位,总是 16 位,可以变化 8/16/24 等位,还是别的什么)。

标签: c# encoding text-files newline streamreader


【解决方案1】:

通过ReadLine()读取文件后,可以回溯到文件末尾的两个字符,并将这些字符与CR-LF进行比较:

string s;
using (StreamReader sr = new StreamReader(@"C:\Users\User1\Desktop\a.txt", encoding: System.Text.Encoding.UTF8))
{
    while (!sr.EndOfStream)
    {
        s = sr.ReadLine();
        //process the line we read...
    }

    //if (sr.BaseStream.Length >= 2) { //ensure file is not so small

    //back 2 bytes from end of file
    sr.BaseStream.Seek(-2, SeekOrigin.End);

    int s1 = sr.Read(); //read the char before last
    int s2 = sr.Read(); //read the last char 
    if (s2 == 10) //file is end with CR-LF or LF ... (CR=13, LF=10)
    {
        if (s1 == 13) { } //file is end with CR-LF (Windows EOL format)
        else { } //file is end with just LF, (UNIX/OSX format)
    }

}

【讨论】:

  • 有一点需要注意,ReadLine() 会吃换行符。当 OP 将使用变量 s 重写文件并附加换行符时,您可能正在替换换行符。
  • 谢谢大家。这肯定会起作用,但仅适用于 UTF-8 编码。如果你必须处理几种类型的编码,比如我的例子,还有很多工作要做。
  • @Cristy “这肯定有效,但仅适用于 UTF-8 编码” - 此答案中显示的只是 StreamReader 的众多构造函数之一。显示的代码与编码无关。
【解决方案2】:

所以您正在处理一个文本文件,这意味着您需要阅读所有文本,并希望保留任何换行符,即使在文件末尾也是如此。

您已经正确地得出结论,ReadLine() 吃掉了那些,即使文件不以 1 结尾。事实上,ReadLine() 在文件以 1 结尾时吃掉最后一个回车符(StreamReader.EndOfStream 在读取倒数第二行后是 true)。 ReadAllText() also eats the last newline。鉴于您可能正在处理大文件,您也不希望一次读取内存中的整个文件。

您也不能只比较文件的最后两个字节,因为有些编码使用超过一个字节来对字符进行编码,例如 UTF-16。所以你需要读取编码感知的文件。 StreamReader 就是这样做的。

因此,一个解决方案是创建您自己的 ReadLine() 版本,其中包括末尾的换行符:

public static class StreamReaderExtensions
{
    public static string ReadLineWithNewLine(this StreamReader reader)
    {
        var builder = new StringBuilder();

        while (!reader.EndOfStream)
        {
            int c = reader.Read();

            builder.Append((char) c);
            if (c == 10)
            {
                break;
            }
        }

        return builder.ToString();
    }
}

然后你可以检查最后返回的行是否以\n结尾:

string line = "";

using (var stream = new StreamReader(@"D:\Temp\NewlineAtEnd.txt"))
{
    while (!stream.EndOfStream)
    {
        line = stream.ReadLineWithNewLine();
        Console.Write(line);
    }
}

Console.WriteLine();

if (line.EndsWith("\n"))
{
    Console.WriteLine("Newline at end of file");
}
else
{
    Console.WriteLine("No newline at end of file");
}

虽然StreamReader 进行了大量优化,但我不能保证一次读取一个字符的性能。与ReadLine()(~1800 vs ~400 ms)相比,使用两个相等的 100 MB 文本文件进行的快速测试显示出相当大的减速。

这种方法确实保留了原始行结尾,这意味着您可以使用此扩展方法返回的字符串安全地重写文件,而无需将所有\n 更改为\r\n,反之亦然。

【讨论】:

  • 即使文件是 UTF-8(比如说)并且包含来自Unicode plane 0 之外的字符,似乎也可以工作。在这种情况下,while 循环似乎将针对该单个文件迭代两次字符,根据需要为字符创建 两个 UTF-16 代码单元(在 .NET 中称为 char 值)(参见 surrogate pair)。当Read() 的返回类型为int 时,没有详细记录。人们可能会认为它会使用 UTF-32,一次返回整个字符。
  • @Jeppe 谢谢。而且我认为我对文本编码以及 .NET 如何处理 Unicode 了解一两件事。所以我需要添加另一个检查,以验证 Read() 是否返回了代理对的前半部分,如果是,即使其代码点为 10,也不要将后半部分视为 \n? 10 是代理对的有效后半部分吗?我目前无法进行更多测试。
  • 不,16 位代码单元不能同时作为代理组件和普通“单个”代码点有效。根据 16 位代码单元的精确值(.NET 中的char),它将是(1)“单个”代码点,或(2)代理对的下部,或(3)代理对的上半部分,但它永远不能超过这三个中的一个。因此,如果我们假设没有遇到无效文件,那么您在使用中就不必担心这一点。
  • @CodeCaster 非常感谢!这个解决方案似乎很合理。创建自己的 ReadLine() 方法是个好主意。我会尽快尝试,然后回来提供消息。
猜你喜欢
  • 1970-01-01
  • 2015-09-16
  • 1970-01-01
  • 1970-01-01
  • 2012-06-24
  • 1970-01-01
  • 2022-01-05
  • 2011-03-21
  • 1970-01-01
相关资源
最近更新 更多