软件安装
- 需要安装Notepad++(或SublineText)等相关软件查看或修改编码格式,方便学习。
- 本文使用的是Notepad++
GB2312的局限性
- GB是"国标"二字的拼音简称,GB2312是最早的国标码。
- GB2312里面收录了6763个汉字,其中一级汉字3755个,二级汉字3008个,同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。
- GB2312收录的6763个汉字是简体中文,不是繁体中文,能够表示日常用语,而某些古诗文则不能用GB2312来表示。
- 从上面第二条对GB2312的定义可以看出,GB2312的编码表里面没有阿拉伯文,没有希腊文,没有北欧系文字,连中国的繁体文字都是没有的。
- 后来诞生了GBK,GB18030等编码。和GB2312的主要区别就是收录的汉字更多了,例如GBK总共收录了21003个汉字、883个符号,并将简体和繁体融于一库。
- 结论:GB2312不能用于游戏项目多国语言配置表工作。
Unicode简介
- Unicode又称统一码、万国码、单一码。是一种国际标准
- Unicode每个字符占4个字节
- 由于Unicode每个字符占4个字节,太浪费存储空间了,因此诞生了4种编码方式,分别是UTF-7, UTF-8, UTF-16, UTF-32
- 其中UTF-32是完整的Unicode编码,但由于太浪费存储空间了,因此互网联领域基本上没人用,仅用于学术领域或某些特殊领域。
此Unicode非彼Unicode
- 通常意义上,我们所指的Unicode需要结合语义的上下文来进行判断,基本上有2种情况。
- 情况1:是指"统一码",即说明的是"一种标准"。 例如:“请问你在Coding的过程中是否使用Unicode?”
- 情况2:专指UTF-16。 例如:“C#的CLR虚拟机中,所有的string都是Unicode编码”
- 在C#编程中,查看Encoding中的编码格式,发现没有UTF-16,只有Unicode。即
System.Text.Encoding.Unicode
- 在C#编程中,内存、硬编码固定采用UTF-16,这在《CLR Via C#》这本书中也有说到,即CLR虚拟机采用UTF-16
- 面对一个中国程序员,“请问你在Coding的过程中是否使用Unicode?” 的意思是 “请问你是否使用UTF-8?”。
- 因此,此Unicode非彼Unicode,一定要注意区别。
Unicode到底是几个字节
- Unicode是一种标准,Unicode的编码目前只有4种规范,即UTF-7, UTF-8, UTF-16, UTF-32
- 其中UTF-8是1到4个字节,绝大部分中文占用3个字节,极少数中文占用4个字节
- UTF-16是2到4个字节,绝大部分中文占用2个字节,极少数中文占3个字节,有些外国文字占用4个字节
- 可以认为UTF-32是完整格式,UTF-8和UTF-16都是压缩格式,不能压缩成1个字节的,就是2个字节。不能压缩成2个字节的就是3个字节,以此类推
Unicode和ANSI的关系
- 这个世界上一共有3种字符代码规则(世界级规范的),一种是ASCII,一种是ANSI,最后一种是Unicode,他们是不同的编码规则
- ASCII是单字节的,而ANSI是当单字节不够用时,延伸成2个字节的编码,当2个字节依然不够时延伸成3个字节的编码。因此可以认为ASCII是ANSI的子集。
- 不同国家和地区定义了不同ANSI标准,由此产生了GB2312、BIG5、JIS等标准。
- 在简体中文系统中,ANSI编码代表GB2312编码,反之亦然(即GB2312就代表ANSI)。
- ANSI和Unicode都是兼容ASCII码的,简单的说就是编码的值完全相等。只是Unicode会在前面补0,对齐编码格式,仅此而已。
- ANSI和Unicode之间的值则是不兼容的,需要一张转换表,才能使得ANSI中的编码对应Unicode中的编码。
利用BOM头来固化Excel和WPS对配置文件的读写编码格式
- Unicode标准认为0xFF,0xFE是不可见字符,在任何一国的文字中都没有意义,因此可以利用它来给文件打一个标记,表示这个文件是Unicode文件
- 不带BOM,会让Excel或WPS执行编码识别流程(这个流程可能与各种规则相关,包括但不限于Windows注册表规则,用户配置规则,软件的扫描识别编码规则,总之在不同的电脑上识别起来不太稳定),带BOM会直接命令Excel使用对应的格式打开文件(免费版WPF依然不听话,付费版的我没有测试过)。
- Excel可根据下列表中的内容判断BOM头和具体的Unicode编码格式
字符编码 | BOM(十六进制) |
---|
UTF-8 | EF BB BF |
UTF-16(BE)大端 | FE FF |
UTF-16(LE)小端 | FF FE |
UTF-32(BE)大端 | 00 00 FE FF |
UTF-32(LE)小端 | FF FE 00 00 |
GB-18030 | 84 31 95 33 |
生成带BOM的文件
- 使用Notepad++等软件进行转换即可
- 转换之后可以发现文件会多出BOM头,用C#的读取所有字节可以看到BOM头
C#的Encoding.Unicode.GetString(byte[] bytes)不会自动处理BOM头的问题
- Encoding.Unicode.GetString不会自动处理BOM头,但在断点调试的时候会显示为正确的字符串(非常容易引起Crash还怎么都找不出来),是因为前面的BOM头被隐藏了,特此说明。因此如果需要通过GetString函数获取到正确的字符串,请使用GetString的另一个重载函数。
- Encoding.Unicode.GetBytes不会自动添加BOM头,道理很简单,我们有可能会不停的写文件,每次写一点点,微软不可能每次都给我们带一个BOM头
- Encoding.Unicode.GetPreamble可以得到BOM头字节数组。因此,我们可以自行拼接BOM头和Encoding.Unicode.GetBytes的内容。
- File.ReadAllText明确传入编码格式时会处理BOM头,返回的字符串不会有问题
- File.WriteAllText明确传入编码格式时会处理BOM头,写入的文件不会有问题(带BOM头)
- File.ReadAllText和File.WriteAllText明确传入编码格式时会处理BOM头,是因为Encoding.Unicode.GetPreamble是虚函数,即不同编码的GetPreamble得到的字节数组是不一样的。当没有传入明确编码时,即Encoding等于null,则不会带BOM头。
- File.ReadAllBytes、File.WriteAllBytes不会处理BOM头。这很好理解,代码并不知道你读写的是什么数据,是结构体还是自定义的二进制数据,因此API不会去乱读写。
示例程序
- "卡ID Hi"字符串使用UTF-8来表示共8个字节。其中"ID Hi"共5个字节,"卡"占用3个字节。
- 因此,使用File.WriteAllText应该是11个字节,即多了3个字节的BOM头。
public class Program
{
static void Main(string[] args)
{
//WriteAllText(自动处理了BOM头的问题,应该等于11个字节)
File.WriteAllText("./a.txt", "卡ID Hi", Encoding.UTF8);
//Read (自动处理了BOM头的问题)
string content = File.ReadAllText("./a.txt", Encoding.UTF8);
if (content == "卡ID Hi") { Console.WriteLine("Yes"); }
//Read Bytes
byte[] preArr = Encoding.UTF8.GetPreamble();
byte[] bytes = File.ReadAllBytes("./a.txt");
content = Encoding.UTF8.GetString(bytes, preArr.Length, bytes.Length - preArr.Length);
if (content == "卡ID Hi") { Console.WriteLine("Yes"); }
}
}
自动处理BOM头
- 编写一个Utility.File类,采用UTF-8格式。
- 读写都自动处理BOM头问题。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public static partial class Utility
{
/// <summary>
/// 文件操作相关实用函数
/// </summary>
public static class File
{
/// <summary>
/// 读取文件所有文本,支持共享读取(只支持UTF8格式)
/// </summary>
/// <param name="path">要读取文件的文件路径</param>
/// <returns>读取到的文件内容</returns>
public static string ReadFileAllText(string path)
{
if (!System.IO.File.Exists(path))
{
return string.Empty;
}
//开辟缓存
FileInfo info = new FileInfo(path);
byte[] byArr = new byte[info.Length];
//读取全部文件内容(共享读取)
FileStream stream = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.Read(byArr, 0, byArr.Length);
stream.Close();
//去BOM头,转为String
string str = ReadTextRegular(byArr);
return str;
}
/// <summary>
/// 读取文件所有内容,支持共享读取
/// </summary>
/// <param name="path">要读取文件的文件路径</param>
/// <returns>读取到的文件内容</returns>
public static byte[] ReadFileAllBytes(string path)
{
if (!System.IO.File.Exists(path))
{
throw new Exception("File Not Found");
}
//开辟缓存
FileInfo info = new FileInfo(path);
byte[] byArr = new byte[info.Length];
//读取全部文件内容(共享读取)
FileStream stream = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.Read(byArr, 0, byArr.Length);
stream.Close();
return byArr;
}
/// <summary>
/// 将内容写入文件,默认采用UTF8-BOM模式
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="content">文件内容</param>
public static void WriteAllText(string path, string content)
{
System.IO.File.WriteAllText(path, content, Encoding.UTF8);
}
/// <summary>
/// 从bytes字节数组中读取文本,会自动处理BOM头的问题
/// </summary>
/// <param name="bytes">字符数组</param>
/// <returns>读取到的文本</returns>
public static string ReadTextRegular(byte[] bytes)
{
string text = null;
//去UTF8-BOM头
//注意:Encoding.UTF8.GetString不会自动去掉BOM头
byte[] preArr = Encoding.UTF8.GetPreamble();
if (preArr[0] == bytes[0] && preArr[1] == bytes[1] && preArr[2] == bytes[2])
{
text = Encoding.UTF8.GetString(bytes, preArr.Length, bytes.Length - preArr.Length);
}
else
{
text = Encoding.UTF8.GetString(bytes);
}
return text;
}
}
}