C# 中如何正确处理 UTF-8 with BOM 的读写问题
昨天把项目中的打包工具做了一些调整,原本正常工作的代码因为新增的部分代码执行流程出现了问题,而且问题比较隐晦。最终通过调试跟踪,发现问题出在解析一个 JSON 文件的时候,该文件中只是存放了一个 JSON 数组而已。之前的版本中解析的文件是直接从服务器下载下来的,而服务器上的文件是在另一个工程中生成的,调整后的项目中,该 JSON 文件是自己生成的。
这个问题的关键在于我们通过以下的代码读取出来的字符串中有特殊字符:
string filePathUTF8 = "/Users/helihua/Temp/poerty_utf8.txt";
byte[] bytesUTF8 = File.ReadAllBytes(filePathUTF8);
string decodedPoetryUTF8 = System.Text.Encoding.UTF8.GetString(bytesUTF8);
直接使用 JSON 解析库解析读取出来的文本会出错,因为这段文本的第一个字符实际上是一个特殊不可见字符(表达零宽度非换行空格的意义,是不是很牛逼,调试的时候输出日志是发现不了的,调试的时候直接查看整个字符串的内容也是查看不了的,只能通过判断字符串长度和字符串第一个 char 才可以分辨噢),是 UTF-8 的 BOM,也就是 U+FEFF 这货,在 UTF-8 编码的文件中表现为前三个字节为: 239 187 191。
仔细分析后,我发现问题出在新调整的代码中,生成新的 JSON 文件使用了 File.WriteAllText(string path, string contents,Encoding encoding)
方法,传入的 encoding 为 System.Text.Encoding.UTF8
。而这货默认是开启 BOM 的,那么这就意味着我们新创建的 JSON 文件是 UTF-8 with BOM 编码格式的(其实就是在写入文本的字节数据之前,添加了一个 BOM 块,也就是文件头部多了 3 个字节)。
然而后续读取该 JSON 文件的时候我并没有使用 File.ReadAllText
方法来进行文本内容的读取,而是先通过 File.ReadAllBytes
方法将 JSON 文本文件的所有数据读取为字节数组,后续通过了 UTF8Encoding.GetString
方法将字节数组转化为字符串。而 Encoding.GetString
方法是不会自己去过滤我们获得到的文件字节数组中的 BOM 头对应的 3 个字节的,所以就将其解析成了一个零宽度非换行空格
了,最终导致解析 JSON 失败,整个程序流程出错了。
那么后续我们应该如何来规避类似的问题,正确地处理 UTF-8 with BOM 的读写问题呢?我的建议是:
- 尽可能让写文件和读文件采用对应的方法,例如写入文本文件的时候,使用
File.WriteAllText
方法,那么在读取文本文件的时候,就应该使用File.ReadAllText
方法(C# 默认的实现非常鸡贼,在写入文件的时候会按照你传入的 Encoding 中声明是否需要写入 BOM 来写入文件,但是在读取文本的时候,不论传入的 Encoding 中是否声明带有 BOM,它都会检测 BOM,并且会把 BOM 从读取出来的字符串中移除掉,也就是说我们通过 File.ReadAllText 读取出来的文本字符串肯定是干净的,不会出现 BOM 这种奇怪的捣乱分子的); - 在使用上面的方法的同时,使用相同的编码方式,并且建议使用不带 BOM 的编码方式(BOM 更多是为了给文本编辑器检测文件编码用的,对于 BOM 的各种争端,就如同编辑器 VIM 和 Emacs 之间的圣战般激烈,在此我表个态,我认为尽可能不用 BOM),如此一来所有的 UTF-8 都是不带 BOM 的,也就不存在在某些场景下读取文件的人没意识到文件头部可能会有 BOM ,编码过程中完全没有考虑到该问题,最终读取出来的文本中含有特殊字符与预期结果不一致的可能性;
- 如果在实际开发中,无法确定文件是什么类型,或者写文件和读文件的代码模块不由同一个人来开发维护,那么就需要非常明确地通过文档规范来声明读写文件应该采用什么方式,通用的方式当然就是通过 StreamWriter 来写入字节,通过 StreamReader 来读取字节,至于需要如何应用这些字节,可以交给上层的应用自行来处理。