在现有的GBK编码的MUD开发中我们都习惯直接用 is_chinese() 判断用户输入的字符是不是汉字,也都知道这是一个模拟外部函数,相关代码如下:
int is_chinese(string str)
{
int i;
if( strlen(str) < 2 ) return 0;
for( i= 0; i < strlen(str); i++)
{
if( str[i] < 161 || str[i] == 255 ) return 0;
if( !(i%2) && (str[i] < 176 || str[i] >= 248) ) return 0;
}
return 1;
}
但是你知道为什么这样可以判断一个字符是不是汉字吗?能看懂这段代码的意思吗?
现在进入知识科谱时间,虽然你不学不影响使用,但学习掌握后不只是在MUD中有用,所以最好了解一下,这里涉及到文字编码相关知识,英文、阿拉伯数据和控制字符在计算机中使用的是ASCII编码,而汉字使用的是GB2312、GBK等编码。
ASCII编码
ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
标准ASCII码也叫基础ASCII码,使用7位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0到9、标点符号,以及在美式英语中使用的特殊控制字符。其中:
0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。
32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。
GB2312编码
因为ASCII编码只能存128个字符,而汉字有上万个,常用的也有数千个,怎么办?GB2312-80编码为汉字而生。
GB2312-80 是 1980 年制定的中国汉字编码国家标准。共收录 7445 个字符,其中汉字 6763 个。GB2312 兼容标准 ASCII码,采用扩展 ASCII 码的编码空间进行编码,一个汉字占用两个字节,每个字节的最高位为 1。具体办法是:收集了 7445 个字符组成 94 * 94 的方阵,每一行称为一个“区”,每一列称为一个“位”,区号位号的范围均为 01-94,区号和位号组成的代码称为“区位码”。区位输入法就是通过输入区位码实现汉字输入的。将区号和位号分别加上 20H,得到的 4 位十六进制整数称为国标码,编码范围为 0x2121~0x7E7E。为了兼容标准 ASCII 码,给国标码的每个字节加 80H,形成的编码称为机内码,简称内码,是汉字在机器中实际的存储代码GB2312-80 标准的内码范围是 0xA1A1~0xFEFE。
上面介绍这么多,关键就是二点:
- 一个汉字占用两个字节,每个字节的最高位为 1
- 编码范围是 0xA1A1~0xFEFE
16进制A1为十进制161,16进制FE为十进制254,简单的判断方法就出来了,GB2312编码下如果获取的字符的编码小于161或大于254就肯定不是中文了。关键代码:
if( str[i] < 161 || str[i] == 255 ) return 0;
但是,GB2312编码中并不全是汉字,7445个字符只有6763个汉字,具体汉字编码范围是0xB0A1~0xF7FE,如果一个字符的首字节编码小于0xB0(十进制176)或大于等于F8(十进制248)就不是汉字。关键代码:
if( !(i%2) && (str[i] < 176 || str[i] >= 248) ) return 0;
再回过头看看这个函数代码:
int is_chinese(string str)
{
int i;
if( strlen(str) < 2 ) return 0;
for( i= 0; i < strlen(str); i++)
{
if( str[i] < 161 || str[i] == 255 ) return 0;
if( !(i%2) && (str[i] < 176 || str[i] >= 248) ) return 0;
}
return 1;
}
已经明白了吗?因为汉字是2个字节,驱动提供的外部函数strlen对中文字符返回长度是2,而LPC中字符串也可以当字符数组操作,通过str[i]即可取得字符编码进行判断。
GBK编码
因为GB2312编码收录的汉字并不全,后来国家又发明了GBK编码,GBK编码是对GB2312编码的扩展,因此完全兼容GB2312-80标准。GBK编码依然采用双字节编码方案,其编码范围:8140-FEFE,剔除xx7F码位,共23940个码位。共收录汉字和图形符号21886个,其中汉字(包括部首和构件)21003个,图形符号883个。GBK编码支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字。GBK编码方案于1995年12月15日正式发布,这一版的GBK规范为1.0版。
GBK编码汉字增加了非常多,包括繁体字等,而且兼容GB2312编码,如果你想让你的MUD接受玩家输入繁体字、生僻字,那可以参考GBK编码中汉字的编码范围改造is_chinese模拟外部函数。只是因为在我们游戏中用GB2312编码判断汉字就足够了,不需要允许玩家用各种冷门的文字,没有折腾的必要。
UTF-8编码
GBK编码是中国用的,不是世界统一的,如果别人用的不是GBK编码,那就出现乱码,比如用GBK编码打开非GBK编码的内容可能出现“烫烫烫”和“屯屯屯”。为了世界大统一,就出现了Unicode万国码,这是一种把全世界的字符都收录了的一套字符编码方案,每个字符都有唯一的编号,此方案的字符编号兼容ASCII编码,在Unicode中规定了中文基本汉字范围为4E00-9FA5,其这每个编号对应哪个汉字我们可以看这里:https://www.unicode.org/charts/PDF/U4E00.pdf
但是Unicode只是字符集,不是具体编码方案,UTF-8编码是此方案的最广泛使用的具体实现。UTF-8编码规则:如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。
从Unicode字符集中规定0x4E00代表汉字一,4E00的十进制是19968,二进制是100 1110 0000 0000,根据UTF-8的编码规则,需要三个字节才能存储,转化结果为11100100 10111000 10000000,其十六进制为0xE4 0xB8 0x80。
关于 mudos 、 fluffos v2017 和 fluffos v2019 的编码说明
MUDOS和FluffOS v2017都不支持UTF8编码,FluffOS v2019支持UTF-8编码。
mudos 和 fluffos v2017 的字符编码支持是一致的,都是使用系统内码ANSI(简体中文系统下是GBK),英文字符占1个字节,中文字符占2个字节,strlen和strwidth返回一致(英文1,中文2),使用str[0]和str[1]可以取一个汉字的二个字节编码。
但是在 fluffos v2019 中所有文字统一为utf-8编码,对strlen和strwidth做了调整,strlen返回的是字符个数,strwidth返回字符宽度,要注意的是使用str[0]直接返回这个字符的unicode编码,如果要取得汉字的GBK(2字节)或utf-8(3字节)编码,需要使用string_encode转化为buffer后操作。如下代码:
int main(object me, string arg)
{
if (!arg)
{
arg = "一";
}
printf("Unicode: %O\n", arg[0]);
printf("GBK: %O\n", string_encode(arg, "GBK"));
printf("UTF-8: %O\n", string_encode(arg, "utf-8"));
return 1;
}
输出结果为:
Unicode: 19968
GBK: BUFFER ( /* sizeof() == 2 */
0xd2,
0xbb
)
UTF-8: BUFFER ( /* sizeof() == 3 */
0xe4,
0xb8,
0x80
)
因为可以直接用str[0]取得字符的Unicode编码,所以,在fluffos v2019下,判断汉字的方法可以用如下代码:
int is_chinese(string str)
{
if (strwidth(str) < 2)
return 0;
for (int i = 0; i < strlen(str); i++)
{
if (str[i] < 19968 || str[i] > 40869)
return 0;
}
return 1;
}
不过,在v2019中判断中文有更简单的方式,可以直接使用pcre正则表达式判断中文,不需要自己通过字符编码范围判断。代码如下:
int is_chinese(string str)
{
if (!str)
return 0;
return pcre_match(str, "^\\p{Han}+$");
}