BG5BJM HAM 📡

中文信息处理指南

本文介绍了为满足《中华人民共和国通用语言文字法》、GB 18030-2022、《通用规范汉字表》的要求,使用编程语言及数据库软件需要做些什么

1 编程语言中的字符处理

1.1获取准确的字符数

什么是字符?

比如一个拉丁字母“A”、一个汉字“我”、一个标点符号“。”、一个表情符号“😀”等都是字符。

注意,根据不同的场景ZWJ序列可视为一个字符,也可视为多个字符。一般来说ZWJ序列应视为多个字符。 以🧑🏽为例,由U+1F9D1+U+1F3FD组成,两个码点(Code Point)应为两个字符,但是理解成一个字符也没问题。

在信息化系统中有很多场景都需要计算字符数量,我们通常使用编程语言内置的形如String类型的Length属性获取字符数。如果我们处理的都是基本多文种平面(Basic Multilingual Plane,以下简称 BMP)范围内字符的话,并没有什么问题,但是如果处理其他平面内字符的话获取到的Length并不是我们期望的结果。

'A'.length //  输出 1
''.length //  输出 1
'𠅤'.length //  输出 2
'😀'.length //  输出 2

以上代码执行结果中字符A返回了数量1,字符𠅤😀返回了数量2。数量2明显不是我们能接受的。 JavaScript使用UTF-16编码(即UCS-2),UTF-16是一种定长编码,每个代码单元(Code Unit)使用2个字节共16位表示一个字符,1个编码单元可以表示BMP范围\u0000-\uFFFF内所有字符。表示不在BMP范围内的字符则使用代理项对(Surrogate Pair)项寻址,代理项对使用2个代码单元,前导(高代理项)\uD800–\uDBFF紧跟着尾随(低代理项)\uDC00–\uDFFF,它们必须成对出现共使用4个字节32位表示一个字符。

表1 各字符不同编码方案的二进制形式。括号内为UTF16的代理项对码点

字符 Unicode UTF-8 UTF-16BE UTF-32BE
A U+0041 01000001 00000000 01000001 00000000 00000000 00000000 01000001
U+60E0 11100110 10000011 10100000 01100000 11100000|00000000 00000000 01100000 11100000  
𠅤 U+20164(U+D840 U+DD64) 11110000 10100000 10000101 10100100 11011000 01000000 11011101 01100100 00000000 00000010 00000001 01100100
😀 U+1F600(U+D83D U+DE00) 11110000 10011111 10011000 10000000 11011000 00111101 11011110 00000000 00000000 00000001 11110110 00000000

在JavaScript中,String length属性获取到的正是字符串使用的代码单元数量1。如果想获取BMP文字例如拉丁文字、西里尔文字、CJK(中日韩统一表意文字)等常见文字的字符数时使用String length并不会有什么问题,但是如果想获取某些特殊文字比如扩充B-G区内的汉字2、表情符号、数学符号等文字的字符数时,就要考虑换种方式了。
上文的例子中A位于BMP使用1个代码单元,𠅤😀在BMP以外,UTF-16编码需使用2个代码单元,因此获取length属性时分别返回了1和2。

1.1.1 JavaScript 兼容 Unicode 字符数方案

[..."A"].length // 输出 1
[...""].length // 输出 1
[..."𠅤"].length // 输出 1
[..."😀"].length // 输出 1

等同于

Array.from("𠅤").length // 输出 1
Array.from("😀").length // 输出 1

JavaScript中如果想把ZWJ序列计算为一个字符可使用Segmenter分段器

[...new Intl.Segmenter().segment("🧑🏽")].length // 输出 1
各浏览器 maxlength 的差异

以如下文本框为例测试

<input maxlength=1 type=text>

1.1.2 .NET 兼容 Unicode 字符数方案

在.NET中,String对象是Char Struct的顺序集合,Char使用UTF-16编码表示Unicode码点,每个Char表示一个代码单元3,BMP以外的字符和ZWJ序列由多个Char对象表示。因此StringLength属性返回该实例中Char对象的数量4而不是Unicode字符数。因此须使用System.Globalization.StringInfo类来处理Unicode字符。

"惠".Length; // 输出 1
"𠅤".Length; // 输出 2
StringInfo.ParseCombiningCharacters("惠").Length; // 输出 1
StringInfo.ParseCombiningCharacters("𠅤").Length; // 输出 1

1.1.3 Java 兼容 Unicode 字符数方案

在Java中,String对象是Char的集合,Char使用UTF-16表示Unicode码点,每个Char表示一个代码单元,BMP以外的增补字符由代理项对表示,使用2个Char5。获取字符数因使用如下方式。Character相关的方法查看Character#Unicode。有关Java平台增补字符的其他资料可查阅Supplementary Characters in the Java Platform

"惠".length; // 输出 1
"𠅤".length; // 输出 2
"惠".codePointCount(0, "惠".length); // 输出 1
"𠅤".codePointCount(0, "𠅤".length); // 输出 1

1.1.4 MySQL 数据库兼容 Unicode 字符数方案

MySQL 中,LENGTH函数返回字符的字节数,CHAR_LENGTH函数返回Unicode字符数(Number of Code Point)6

SELECT LENGTH('惠'); -- 输出 3
SELECT LENGTH('𠅤'); -- 输出 4
SELECT CHAR_LENGTH('惠'); -- 输出 1
SELECT CHAR_LENGTH('𠅤'); -- 输出 1

1.2 正则表达式 Regular Expression

2 数据库字符集

2.1 MySQL

应使用utf8mb4编码方案。

3 术语

术语 中文 描述
BMP(Basic Multilingual Plane) 基本多文种平面、基本平面 0000...FFFF范围内的字符
code point 码点、代码点 一般对应一个字符。在UTF-16中,使用代理项对来表示扩充字符(表现为两个代码单元,也对应了两个代理项范围的码点,事实上代理项也仅针对UTF-16定义)
code unit 代码单元 特定编码方案使用的位大小。UTF-8的代码单元是1字节8位,UTF-16的代码单元是2字节16位,UTF-32的代码单元是4字节32位
surrogate 代理项 D800...DBFFDC00...DFFF范围的码点组成,在UTF-16中成对出现用来表示BMP以外的扩充字符
character 字符 每个字符都是一个自然语言中的字母、符号、汉字等。每个码点对应一个字符。UTF-16中一个字符使用1~2个代码单元表示一个字符。
byte 字节 8位(比特)组成1字节
bit 位、比特 值包含0和1,计算机中的最小运算单位

4 参考资料

5 脚注

  1. String length - JavaScript https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length 

  2. 国务院2013年颁布的《通用规范汉字表》中包含196个汉字位于BMP之外。2022年发布的GB 18030-2022 也将增补区的196个汉字规定为实现级别2的要求。 

  3. Char Struct - .NET https://learn.microsoft.com/en-us/dotnet/api/system.char 

  4. String - .NET https://learn.microsoft.com/en-us/dotnet/api/system.string.length 

  5. String (Java Platform SE 8) https://docs.oracle.com/javase/8/docs/api/java/lang/String.html 

  6. 12.8 String Functions and Operators https://dev.mysql.com/doc/refman/8.0/en/string-functions.html