介绍

信息 = 位 + 上下文 《深入理解计算机系统》

与语言类似,只有符号是看不懂的,你必须要先经过学习,了解符号的意义,也就是这个语言的上下文。

因此可以将简认为每个字符都用一个数字表示,不同的编码就是使用不同的方案将数字表示出来。

历史

随着计算机使用范围变得越来越广泛,越来越多的语言需要在计算机中显示与处理。最开始的方案是每一种语言一套字符集,但是不同的地区有不同的字符集,同一个文件到另一个地方由于字符集不同,可能显示完全是错的。

因此,Unicode 诞生了,可以将不同的语言放到同一个字符集中,因此也就可以同时显示不同语言而不会出问题了。

Unicode

英文页面比中文页面内容要多不少,建议都看一下。

Unicode 对现代编码模型划分的层次,清晰地定义了不同内容,使得原来含混不清的概念易于理解:

在Unicode Technical Report (UTR) #17中,现代编码模型分为5个层次,所用的术语列在下面:\

  1. 抽象字符表(Abstract character repertoire)是一个系统支持的所有抽象字符的集合。\
  2. 编码字符集(CCS:Coded Character Set)是将字符集{\displaystyle C}中每个字符映射到1个坐标(整数值对:x, y)或者表示为1个非负整数{\displaystyle N}。字符集及码位映射称为编码字符集。\
  3. 字符编码表(CEF:Character Encoding Form),也称为"storage format",是将编码字符集的非负整数值(即抽象的码位)转换成有限比特长度的整型值(称为码元code units)的序列。这对于定长编码来说是个到自身的映射(null mapping),但对于变长编码来说,该映射比较复杂,把一些码位映射到一个码元,把另外一些码位映射到由多个码元组成的序列。\
  4. 字符编码方案(CES:Character Encoding Scheme),也称作"serialization format"。将定长的整型值(即码元)映射到8位字节序列,以便编码后的数据的文件存储或网络传输。\
  5. 传输编码语法(transfer encoding syntax),用于处理上一层次的字符编码方案提供的字节序列。

通过上述细致的划分,你就可以清楚地知道在处理问题时是在哪一层次,也就知道了具体要解决哪些问题。

Unicode转换格式

UTF-8

UTF-8 成为最广泛使用的编码最大的原因是因为较好的兼容了之前的编码 ASCII。直接将旧编码作为自己的子集。

而且 UTF-8 在二进制数据被破坏时,可以较快恢复,而不会由于一个错误导致整个文件都解析不了。

UTF-16 与 UTF-32

UTF-16 与 UTF-32 分别使用 16 位与 32 位存储字符,但实际上 UTF-16 最多会用到 32 位存储字符,因为 16 位空间(65536)不足以保存所有 Unicode。

UTF-16 与 UTF-32 都存在一个字节序的问题,见下面介绍。

Unicode 字节序

字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指电脑内存中或在数字通信链路中,组成多字节的字的字节的排列顺序。

UTF-16 和 UTF-32 有字节序问题,为了识别不同的字节序,需要使用 Byte Order Mark (BOM)

UTF-8 虽然也有 BOM,但是只是为了标识自身,并不是必须或推荐使用的。

注意:UTF-8 的 BOM 只允许出现在文件开头,如果在处理文件时未将其去除就将包含 BOM 的数据与其他文本合并,会导致解析时出错。

乱码

编码或解码时使用了不同或不兼容的字符集。

为了避免乱码,现在建议统一使用 UTF-8,因为它较好地解决了空间占用与兼容性的问题。

UTF-8 is by far the most common encoding for the World Wide Web, accounting for 96.0% of all web pages, and up to 100% for some languages, as of 2021.

考虑到国际化,现在 UTF-8 是首选,可以规避大多数问题。

编程语言支持

内部如何存储

C# 使用 UTF-16 来保存字符

外部输入输出

使用文件读取写入时要指定编码。

很有意思的是 File.WriteAllText 在未指定编码参数时使用 UTF-8 无 BOM,反而在指定 UTF-8 编码参数时增加 BOM。

This method uses UTF-8 encoding without a Byte-Order Mark (BOM), so using the GetPreamble method will return an empty byte array. If it is necessary to include a UTF-8 identifier, such as a byte order mark, at the beginning of a file, use the WriteAllText(String, String, Encoding) method overload with UTF8 encoding.

不同平台支持

Windows 上 wchar_t 是 UTF-16,Windows 命令行使用的却是 cp936(GBK),因此在涉及到对应平台操作的时候,一定要注意编码转换。

例如使用 Python 读取 Windows 上运行的 Unity 日志,需要指定输出编码是 GB18030:

1
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='gb18030')

问题

Lua 获取字符串长度问题

string.len and # count bytes, not chars.
In Lua 5.3+, use utf8.len.

If you use unicode strings, there are at least five different notions of length. Beware of using the wrong one.
Byte count. string.len offers that.
Count of unicode code units. Just divide string.len by bytes per codeunit according to your encoding.
Count of unicode codepoints. In Lua 5.3, utf8.len counts them for UTF-8. There is no function for UTF16-(LE|BE), though you can easily construct one using string.gsub or string.gmatch. For UTF-32-(LE|BE), equals codeunits.
Count of unicode graphemes. These correspond most strongly with your average user’s notion of character count. You need an external library with full unicode tables (big!). Only sorting and normalizing need more effort.
Display size. Ask your output library how wide your string rendered in your font is. Obviously needs an external library.

现在 xLua 使用的 Lua 版本:

1、虚拟机升级:lua5.3.4 -> lua5.3.5,luajit2.1b2 -> luajit2.1b3;

看一下下面这个 Lua 获取 UTF 字符串长度示例代码,就会发现有很多问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function UICommon.getUTFLen(s)
    local sTable = UICommon.stringToTable(s)
    local len = 0
    local charLen = 0

    for i=1,#sTable do
        local utfCharLen = string.len(sTable[i])
        -- 长度大于1可认为为中文
        if utfCharLen > 1 then
            charLen = 2         --将charLen设为1,可获取中文,英文的字符个数,以下举例,将其方法命名为:function getNewUTFLen(s)
        else
            charLen = 1
        end
        -- charLen = 1
        len = len + charLen
    end
    return len
end

function UICommon.stringToTable(s)
    local tb = {}

    --[[
    UTF8的编码规则:
    1. 字符的第一个字节范围: 0x00-0x7F(0-127),或者 0xC2-0xF4(194-244);
        UTF8 是兼容 ascii 的,所以 0~127 就和 ascii 完全一致
    2. 0xC0, 0xC1,0xF5-0xFF(192, 193 和 245-255)不会出现在UTF8编码中
    3. 0x80-0xBF(128-191)只会出现在第二个及随后的编码中(针对多字节编码,如汉字)
    ]]
    for utfChar in string.gmatch(s, "[%z\1-\127\194-\244][\128-\191]*") do
        table.insert(tb, utfChar)
    end

    return tb
end

计算字符串显示的长度应该交给显示层去处理,而不是使用一个中文汉字宽度等于两个英文字母宽度来约定,现实中涉及到本地化时不同语言字符显示的宽度完全不同。

一个字符只有进行光栅化的时候才能知道占用的确切宽度;另外有可能文字在显示时要加上粗体、高亮、描边、调整字间距或行间距等等操作,这些都会最终影响到文字显示的宽度,所以单纯使用一个汉字等于两个字母宽是不正确的。

这里面居然使用正则表式式去查找汉字,这是其一;在保存上一步查找结果时新建了表保存结果,这就其二。这些操作实际太浪费资源了,如果使用得过多会导致游戏卡顿。

Unity 字符显示问题

NGUI 在调用 Unity 渲染字体时使用 Unity 4.x 的 Font 类接口生成需要的字符纹理。

1
public void RequestCharactersInTexture(char character, int size = 0)

在 iOS 中输入 Emoji 表情后,游戏内只会显示两个 ?号,就算是字体中存在 Emoji 字符也无法显示。原因是因为 char 内部以 16 位存储字符,那么对于 Emoji 这种占用 32 位的字符来说就有问题了。

A single Char object usually represents a single code point; that is, the numeric value of the Char equals the code point. For example, the code point for the character “a” is U+0061. However, a code point might require more than one encoded element (more than one Char object).

由于可能字符需要多个 char 保存,也就是说使用一个 char 就不会支持 Emoji 显示。因此,Unity 4.x 根本无法支持 Emoji 显示,要显示的话需要自行封装字体渲染接口去渲染 Emoji。

而在 Unity 5 中这个接口参数发生了变化,接受的是 string 参数,最起码从理论上说,Unity 接口本身不会限制显示 Emoji 了。

1
public void RequestCharactersInTexture(string characters, int size = 0, FontStyle style = FontStyle.Normal);

参考资料

其实字符编码非常复杂,有兴趣的话还是建议多去了解些相关资料,下面有一些文字与音频都是相当不错的,强烈推荐。

文字

音频

《字谈字畅》(TypeChat)是全球首家用华语制作的字体排印主题播客节目