StreamWriter 和 UTF-8 字节顺序标记

Posted

技术标签:

【中文标题】StreamWriter 和 UTF-8 字节顺序标记【英文标题】:StreamWriter and UTF-8 Byte Order Marks 【发布时间】:2011-07-13 01:42:40 【问题描述】:

我遇到了 StreamWriter 和字节顺序标记的问题。该文档似乎声明 Encoding.UTF8 编码启用了字节顺序标记,但是在写入文件时,有些有标记,而另一些则没有。

我正在通过以下方式创建流编写器:

this.Writer = new StreamWriter(this.Stream, System.Text.Encoding.UTF8);

任何关于可能发生的事情的想法都将不胜感激。

【问题讨论】:

请注意,虽然技术上允许在 UTF-8 中使用,但 Unicode 既不要求也不推荐 BOM(参见 ref)。一方面,它是无用的(与 UTF-16 不同)——UTF-8 字节顺序是由标准指定的。另一方面,它可能会破坏文本处理。例如,如果在 XML 序言之前有任何字符,许多 XML 解析器会阻塞。 您确定要指定 UTF8 吗?因为如果你不指定它,它仍然会写一个UTF8,但是没有BOM 来自 Unicode 标准 5.0:Unicode 标准还指定使用初始字节顺序标记 (BOM) 来明确区分某些 Unicode 编码方案中的大端或小端数据. 您解决了这个问题吗?如果是这样,请标记正确答案或发布您自己的答案以帮助他人。 Create Text File Without BOM的可能重复 【参考方案1】:

您是否对每个文件都使用相同的 StreamWriter 构造函数?因为文档说:

要使用 UTF-8 编码和 BOM 创建 StreamWriter,请考虑使用指定编码的构造函数,例如 StreamWriter(String, Boolean, Encoding)。

前段时间我也遇到过类似的情况。我最终使用了Stream.Write 方法而不是StreamWriter,并在编写Encoding.GetBytes(stringToWrite) 之前编写了Encoding.GetPreamble() 的结果

【讨论】:

【参考方案2】:

我唯一一次看到构造函数不添加 UTF-8 BOM 是当您调用它时流不在位置 0 处。例如,在下面的代码中,没有写 BOM:

using (var s = File.Create("test2.txt"))

    s.WriteByte(32);
    using (var sw = new StreamWriter(s, Encoding.UTF8))
    
        sw.WriteLine("hello, world");
    

正如其他人所说,如果您使用 StreamWriter(stream) 构造函数,但未指定编码,那么您将看不到 BOM。

【讨论】:

我认为“位置 0”基本上是关于这个问题的关键信息。 另外,this 构造函数也不会输出 BOM:new StreamWriter("file.txt", Encoding.UTF8)【参考方案3】:

能否请您展示它不生产它的情况?我能找到的唯一不存在序言的情况是没有任何东西写给作者(Jim Mischel 似乎找到了另一个,合乎逻辑的,更有可能是你的问题,看看它的答案)。

我的测试代码:

var stream = new MemoryStream();
using(var writer = new StreamWriter(stream, System.Text.Encoding.UTF8))

    writer.Write('a');

Console.WriteLine(stream.ToArray()
    .Select(b => b.ToString("X2"))
    .Aggregate((i, a) => i + " " + a)
    );

【讨论】:

【参考方案4】:

似乎如果文件已经存在并且不包含 BOM,那么在覆盖时它不会包含 BOM,换句话说,StreamWriter 在覆盖文件时保留 BOM(或它不存在)。

【讨论】:

【参考方案5】:

正如有人已经指出的那样,不带编码参数的调用就可以解决问题。 但是,如果你想明确一点,试试这个:

using (var sw = new StreamWriter(this.Stream, new UTF8Encoding(false)))

要禁用 BOM,关键是使用 new UTF8Encoding(false) 构造,而不仅仅是 Encoding.UTF8Encoding。这和在没有编码参数的情况下调用 StreamWriter 一样,在内部它只是在做同样的事情。

要启用 BOM,请改用 new UTF8Encoding(true)

更新:从 Windows 10 v1903 开始​​,当在 notepad.exe 中保存为 UTF-8 时,BOM 字节现在是一个可选功能。

【讨论】:

我不明白 - 导致 C# 语法错误的答案如何在六年内获得 64 票赞成,而没有人提到它会导致语法错误? 哈哈,我想这给读者留下了一个练习题:P 我想我已经修复了这个错误。 另一种修复方法是using (var sw = new StreamWriter("text.txt", false, new UTF8Encoding(false))) new UTF8Encoding(false) 是重要的一点。你不真的相信人们只是复制粘贴东西,对吗? 为什么是假的?应该是真的。请在下面查看 Nik 的答案。我没明白,这个答案怎么能得到最高票,因为它提供了相反的答案。【参考方案6】:

问题是由于您在Encoding class 上使用静态UTF8 property。

当在UTF8 属性返回的Encoding 类的实例上调用GetPreamble method 时,它会返回字节顺序标记(三个字符的字节数组)并在任何其他之前写入流内容被写入流(假设是一个新流)。

您可以通过自己创建UTF8Encoding class 的实例来避免这种情况,如下所示:

// As before.
this.Writer = new StreamWriter(this.Stream, 
    // Create yourself, passing false will prevent the BOM from being written.
    new System.Text.UTF8Encoding());

根据default parameterless constructor 的文档(强调我的):

此构造函数创建一个不提供 Unicode 字节顺序标记并且在检测到无效编码时不抛出异常的实例。

这意味着对 GetPreamble 的调用将返回一个空数组,因此不会将 BOM 写入底层流。

【讨论】:

编码是我们程序中的一个用户设置(它通过 TCP 发送文本消息)...它是通过使用 enc = Encoding.GetEncoding(...) 的简单解析来检索的。我发现的唯一方法是在它后面添加if (enc is UTF8Encoding) enc = new UTF8Encoding(false);。虽然是一个相当肮脏的修复,但我认为没有其他方法可以解决它...... @Nyerguds 这不是唯一的方法。您可以将编码的获取抽象为一个给定参数的接口,获取编码。然后,您将该接口的实现传递/注入到您的代码中。然后它使一切都变得非常干净。 这只是将同一件事移到不同的班级。总的来说,我只是觉得 GetEncoding 以某种方式设法不使用默认构造函数是非常奇怪的。啊,好吧。 详细说明,GetPreambleStreamWriter 内部调用(参见the source),因此当调用UTF8 属性(由internally 和UTF8Encoding(true) 构造)时,它返回 BOM,如答案中所述(另请参阅remarks 部分)。【参考方案7】:

我的回答是基于 HelloSam 的回答,其中包含所有必要的信息。 只有我相信 OP 要求的是如何确保将 BOM 发送到文件中。

因此,您需要传递 true,而不是将 false 传递给 UTF8Encoding ctor。

    using (var sw = new StreamWriter("text.txt", new UTF8Encoding(true)))

试试下面的代码,在十六进制编辑器中打开生成的文件,看看哪个包含 BOM,哪个不包含。

class Program

    static void Main(string[] args)
    
        const string nobomtxt = "nobom.txt";
        File.Delete(nobomtxt);

        using (Stream stream = File.OpenWrite(nobomtxt))
        using (var writer = new StreamWriter(stream, new UTF8Encoding(false)))
        
            writer.WriteLine("HelloПривет");
        

        const string bomtxt = "bom.txt";
        File.Delete(bomtxt);

        using (Stream stream = File.OpenWrite(bomtxt))
        using (var writer = new StreamWriter(stream, new UTF8Encoding(true)))
        
            writer.WriteLine("HelloПривет");
        
    

【讨论】:

【参考方案8】:

我发现这个答案很有用(感谢@Philipp Grathwohl 和@Nik),但就我而言,我使用 FileStream 来完成任务,因此,生成 BOM 的代码如下所示:

using (FileStream vStream = File.Create(pfilePath))

    // Creates the UTF-8 encoding with parameter "encoderShouldEmitUTF8Identifier" set to true
    Encoding vUTF8Encoding = new UTF8Encoding(true);
    // Gets the preamble in order to attach the BOM
    var vPreambleByte = vUTF8Encoding.GetPreamble();

    // Writes the preamble first
    vStream.Write(vPreambleByte, 0, vPreambleByte.Length);

    // Gets the bytes from text
    byte[] vByteData = vUTF8Encoding.GetBytes(pTextToSaveToFile);
    vStream.Write(vByteData, 0, vByteData.Length);
    vStream.Close();

【讨论】:

我发现new UTF8Encoding(true) 构造函数很有用。【参考方案9】:

读完SteamWriter源代码后,你需要确定你是在新建一个文件,然后字节序标记会添加到文件中。https://github.com/dotnet/runtime/blob/6ef4b2e7aba70c514d85c2b43eac1616216bea55/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs#L267 Flush方法中的代码

如果 (!_haveWrittenPreamble) _haveWrittenPreamble = true; ReadOnlySpan 前导码 = _encoding.Preamble; if (preamble.Length > 0) _stream.Write(序言);

https://github.com/dotnet/runtime/blob/6ef4b2e7aba70c514d85c2b43eac1616216bea55/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs#L129 代码设置_haveWrittenPreamble的值

// 如果我们要追加到已经有数据的 Stream,不要 写 // 序言。 if (_stream.CanSeek && _stream.Position > 0) _haveWrittenPreamble = true;

【讨论】:

【参考方案10】:

使用 Encoding.Default 而不是 Encoding.UTF8 解决了我的问题

【讨论】:

以上是关于StreamWriter 和 UTF-8 字节顺序标记的主要内容,如果未能解决你的问题,请参考以下文章

C#中FileStream和StreamWriter/StreamReader的区别

StreamReader和StreamWriter说明

StreamWriter 的默认缓冲区大小是多少

字节顺序标记——BOM,Byte Order Mark

什么是BOM头(字节顺序标记(ByteOrderMark))

Eclipse * 字节的 UTF-8 序列的字节 * 无效。