关于Unicode,字符集,字符编码,每个程序员都应该知道的事#

作者:Jack47

2017.12.18 Update 2年以后,以Go语言作为主力开发语言后,看到了Rob Pike的 这篇文章,虽然是Go语言相关,但是里面涉及的语句非常简单,深入浅出的把下文中的几个概念讲清楚了,最重要的是非常非常简单的代码实例,能够让你一次把这些东西都搞懂,非常值得一看!

李笑来的文章如何判断一个人是否聪明?中提到:

必要、清晰、且准确的概念,是一切思考的基石。所谓思考,很大程度上,就是在建立那些概念与概念之间的关联。概念是必要、清晰、且准确的,它们之间的关联也应该是准确的。

确实很认同这两句话,搞清楚字符集,字符编码,Unicode等关键词的意义,基本上也就能搞明白遇到的编码问题了。本文力求通俗易懂,但涉及的内容比较多,而且编码问题又不是那么容易理解的,所以如果大家看完之后还是对编码问题一知半解,那也不要灰心,下次遇到编码问题时回过头来再看看本文。我也是断断续续花了很长时间才理解清楚编码问题的。

基本概念##

字符[character]###

字符代表了字母表中的字符,标点符号和其他的一些符号。在计算机中,文本是由字符组成的。

字符集合[character set]###

由一套用于特定用途的字符组成,例如支持西欧语言的字符集合,支持中文的字符集合。字符集合只定义了符号和他们的语意,其实跟计算机没有直接关系。

现实生活中,不同的语系有自己的字符集合,例如藏文有自己的字符集合,汉文有自己的字符集合。到计算机的世界中,也有各种字符集合,例如ASCII字符集合GB2312字符集合GBK字符集合。还有一个其他字符集合的超集--Unicode字符集定义了几乎绝大部分现存语言需要的字符,是一种通用的字符集,来支持多语言环境(可以同时处理多种语言混合的情况)。各个国家和地区在制定编码标准的时候,“字符集合”和“字符编码”一般都是同时制定的。所以像ASCII字符集合一样,它也同时代表了一种字符的编码。

字符编码[character encoding]###

是一套规则,定义了在计算机内存中如何表示字符,是字符集中的每个字符与计算机内存中字节之间的转换关系,也可以认为是把字符数字化,规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储。例如ASCII编码[你没看错,它既是一种字符集合,也是一种字符编码],定义了英文字母和符号在计算机中的表示方式,是用一个字节来表示。Unicode字符集合,有好几种字符编码方式,例如变长度编码的UTF8UTF16等。中文字符集也有很多字符编码,例如上文提到的GB2312编码,GBK编码等。

知乎上的这篇介绍字符编码,字体,iconv的文章很赞,内容浅显易懂。还有一篇很有名的有关Unicode和字符集的文章可以看看:The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!),网上有中文版。

UCS和ISO 10646标准###

ISO 10646标准定义了通用字符集UCS[Universal Character Set],是其他所有字符集合的超集。它保证了和其他字符集合之间可以来回转换,不会丢失信息。

UCS不仅给每个字符做了编码,而且还定义了一个官方的名称。用来表示一个UCS或者Unicode的十六进制数字通常是用"U+"来作为前缀的,例如用"U+0041"来表示拉丁文中的大写字母A。

UCS[Universial Character Set]和Unicode的关系###

简单粗暴的总结一下,就是两拨人搞的同一套标准。具体经过如下:

在1980年代后期,有独立的两拨人想创建一个通用的字符集合。一个是国际化标准组织ISO[Internaltional Organization for Standardization],另外一个是最初成员大部分是美国多语言软件服务提供商的财团发起的Unicode项目。幸运的是在1991年左右,两个项目的成员都意识到世界不需要两个统一的字符集。于是他们一起合作制定了一个字符表。虽然两个项目至今仍然存在并独立发布各自的标准,但是Unicode财团和国际化标准组织都已经同意会让Unicode和ISO 10646标准互相兼容并会在未来紧密协作。具体两者之间的区别,见这里

什么是UTF8###

Unicode/UCS只是字符集合,虽然为每个字符分配了一个唯一的整数值,但具体怎么用字节来表示每个字符,是由字符编码决定的。Unicode的字符编码方式有UTF-8, UTF-16, UTF-32。由于UTF-16和UTF-32编码中包含"\0",或者"/"这样对于文件名和其他C语言库函数来说具有特殊意义的字符,所以不适合在Unix下用来做文件名称,文本文件和环境变量的Unicode编码。UTF-8没有这样的问题,它有很多优点:可以向前兼容ASCII码,是变长的编码,由于编码没有状态,所以很容易重新同步,在传输过程中丢失了一些字节后,具有鲁棒性。

POSIX语系[locale]机制###

语系[locale]就是软件运行时的语言环境,它是语言和文化规则的一个集合,包含字符编码,日期/时间的表示方式,字符排序的规则等。语系的名称通常是由ISO 639-1规定的语言[language]和ISO 3166-1规定的国家代码[country code]以及额外的字符编码名称[character encoding]共同组成,例如zh_TW.UTF-8语系,zh代表语言是汉语,TW是台湾地区,UTF-8是字符编码。而zh_CN.GBK中,CN是指中国大陆地区,采用GBK编码。

Linux下语系由几个类别的环境变量组成,指定了在软件中跟语言惯例相关的行为信息。例如LC_CTYPE决定字符编码方式,LC_COLLATE决定字符排序的规则。LANG环境变量用来设置所有类别的默认语系,但是LC_*这些变量能够覆盖每个单独的类别。

理解了上述概念,咋们就可以去实践一下了。

实战##

C语言对Unicode和UTF-8的支持###

多字节字符和宽字符####

C语言中用单独的一个char类型的变量是无法唯一地表示像汉语这样的自然语言的。C语言标准支持两种不同的方式来处理扩展的自然语言编码方式:宽字符[wide characters]和多字节字符[multibyte characters]。

  1. 宽字符是一种内部表示方式,每个字符是用一个单独的wchar_t类型来表示的。
  2. 多字节字符是用来做输入和输出的,每个字符用C语言中char类型的序列来表示。所以每个字符会用一个或多个(最多MB_LEN_MAX)字节来表示

wchar_t这种类型是从GNU glibc 2.2开始引入的,目的是在运行时用单个的对象来表示字符,跟当前使用的语系无关。ISO C99标准要求通过宏__STDC_ISO_10646__来告诉程序支持wchar_t类型,并且保证所有的宽字符处理函数都会把宽字符当作Unicode字符。C语言中处理宽字符的函数多数是在处理char类型字符的函数名基础上,添加了"w"或者是把"str"替换成"wcs",例如wprintf(),wscpy()等。字符串常量之前添加L前缀就可以告诉让编译器用wchar_t类型来存储字符串常量,例如printf("%ls\n", L"Schöne Grüße"),如果用宽字符来表示字符串,此时的字符串长度就是以wchar_t为单位的,而不是字节;

2011版的C和C++标准都各自引入了固定大小的字符类型char16_tchar32_t来明确提供16位和32位Unicode编码格式,让wchar_t成为实现相关的类型。ISO 10646:2003 Unicode 4.0标准说:

wchar_t类型的宽度是由编译器指定的,可以小到只有8位。因此对于需要在C或C++编译器之间可移植的程序不应该使用wchar_t来存储Unicode文本。wchar_t类型的目的是存储编译器定义的宽字符,有可能不是用Unicode编码的。

多字节字符的字符编码方式,是由当前系统的语系[locale]来决定的,例如当前语系中字符编码是UTF-8,那么多字节字符编码就是UTF-8。因此语系也控制着宽字符和多字节之间的转换。

glibc2.2及更高版本完整地实现ISO C语言多字节转换函数(mbsrtowcs(), wcsrtomb()等)。这些函数用来在wchar_t和任何语系相关的多字节编码,包括UTF-8,ISO 8859-1等之间进行转化。

建议是使用这些函数中可重启动的[restartable,函数名中有字母r],是多线程安全的函数,例如wcsrtombs(), mbsrtowcs()

使用这些函数的好处是:

  • 是跟厂商无关的标准
  • 函数会根据用户的语系做正确的事情。程序需要做的是在程序开头调用setlocale(LC_ALL, "")来根据环境变量来设置用户语系

例如可以写出如下代码:

#include <stdio.h>
#include <locale.h> int main()
{
if (!setlocale(LC_CTYPE, "")) {
fprintf(stderr, "Can't set the specified locale! "
"Check LANG, LC_CTYPE, LC_ALL.\n");
return 1;
}
printf("%ls\n", L"Schöne Grüße");
return 0;
}

setlocale(LC_CTYPE, "")函数,会依次测试环境变量 LC_ALL, LC_CTYPELANG的值,如果有值,就用这个值来决定用哪个语系数据来加载LC_CTYPE这个分类(控制着多字节转换的函数)。

printf中的%ls格式说明符是用来指定把宽字符形式的字符串参数转化成由语系决定的多字节编码来输出。printf函数是不知道输出的字符的编码方式的,它会把传给它的字节原封不动地输出出去。在显示的时候,操作系统会根据当前的语系来将这些字节解码到对应的字符,所以只有当传给printf的字符编码方式和用户环境变量指定的字符编码方式相同,用printf打印出的字符才不会乱码。

使用这些函数的坏处:

  • 有些函数是非线程安全的,因为两次函数调用之间有隐藏的内部状态
  • 不能同时支持多种语系或编码方式

通过上述的分析可以看到,如果全部都使用C语言库中多字节的函数来进行外部字符编码和程序内部使用的wchar_t类型之间的转换,那么C语言库会根据环境变量LC_CTYPE的值来选择正确的字符编码,你的程序甚至不用显示地知道当前多字节编码是什么。

然而,有一些情况下你可能不会全部都用C语言库中的多字节函数,此时程序不得不知道当前语系是什么。此时需要首先在程序开始处调用setlocale(LC_TYPE, ""函数来根据环境变量设置语系。之后利用函数nl_langinfo(CODESET)函数来获得当前语系指定的字符编码的名称。

C语言如何书写采用了某种字符编码的字符串常量###

对于一坨字节数据来说,字符编码就相当于是有色眼镜一样,我们可以戴上UTF-8编码的眼镜去解读这片字节数据,也可以戴上GBK编码的眼镜去解读它。只有当我们采用了跟写入时的编码一致的编码去解读,才能读取出有意义的字符串,否则可能就是乱码了。

转义序列####

转义序列[escape sequences]:转义是以多个字符的有序组合来表示原本很难直接表示出来的字符的技术。转义序列指在转义时使用的有序字符组合。

需要了解C语言中如下的几个转义方式:

'\798':值为十进制值798的字符

'\x7D':值为十六进制7D的字符

'\u0041':代表字符名称中名为U+0041的这个Unicode字符,可能最终编译器会用几个字节来存储这个字符。这种方式只有C99以后才支持。由编译器来决定具体用什么方式存储。

有了这几个转义字符这样就很容易书写出特定编码的字符串了,例如"我是Jack47",采用各种编码形式的值如下:

char gbk_name[] = "\xced2\xcac7Jack47";

char unicode_name[] = "\u6211\u662FJack47"

char utf8_name[] = "\xe6\x88\x91\xe6\x98\xafJack47"

上述的这种方式,是直接把编码后的字节写入到了数组里,是一种"硬编码"[hard code]的方式。

知道了上述的知识后,问题就来了,当前软件要支持UTF8,要如何修改?

如何修改软件来支持UTF8###

有两种办法,可以这样划分:

1.	软转换:数据在所有地方都是以UTF-8的形式存储的。
2. 硬转换:程序读取的输入是UTF-8数据,在程序内部转换成宽字符后进行处理,只有在最终输出的时候转换成UTF-8编码。在内部一个字符是一个固定大小的内存对象。

也可以这样划分:

1. 硬编码的方法
把UTF-8相关的信息硬编码到程序中。这样能够在某些场景下显著提高程序执行效率。这或许是那些只需要支持ASCII和UTF-8编码的程序的最好办法。 2. 取决于语系的方法
C语言提供了可以处理任意特定语系,采用多字节编码的字符串的处理函数。依赖于这些函数的程序员可以不用感知到UTF-8编码的实际细节。通过仅仅改变语系设置,就可以自动支持其他的多字节编码(例如EUC)。

如果使用了UTF-8或者其他类似的多字节编码,需要程序员清楚地区分以下概念:

1. 字节[Byte]
2. 字符[Character]
3. 显示时候的宽度

如何在不同编码间转换###

可以使用iconv函数在两个不同的编码之间进行转换,例如从GBK编码转换到UTF-8编码。

Java与Unicode###

Java语言内部使用的就是Unicode编码。char类型表示一个Unicode字符[这是跟C语言不一样的地方],java.lang.String类表示一个从Unicode字符构建的字符串。

java.io.DataInputjava.io.DataOutput接口分别有叫做readUTFwriteUTF的方法。但记住他们使用的不是UTF-8;他们用的是修改后的UTF-8编码:NUL字符不是用一个字节的0x00来表示,而是用两个字节的0xC0 0x80来表示的,在最后添加一个字节的0x00。这样编码,字符串包含NUL字符而不需要增加表示字符串长度的前缀字段--这样C语言<string.h>中定义的strlen()strcpy这些函数就可以用来操作这些数据了。

一些练习###

  1. 如何处理输入的中文参数,例如中文参数的字符个数打印出来?
  2. 在json串中遇到了这样的字符串,是什么意思呢?"\u82f9\u679c\u624b\u673a"

参考资料##

  1. UTF-8 and Unicode FAQ for Unix/Linux

在POSIX系统上(Linux, Unix)如何使用Unicode/UTF-8的一站式信息的文章,内容丰富,比较长,可以挑着看。

  1. C语言中转义字符

  2. C语言中的反斜线转义符

  3. FreeBSD 多字节操作

  4. Making your programs Unicode aware

  5. 中文编码杂谈


如果您看了本篇博客,觉得对您有所收获,请点击右下角的“推荐”,让更多人看到!

资助Jack47写作,打赏一个鸡蛋灌饼钱吧
微信打赏
支付宝打赏

关于Unicode,字符集,字符编码,每个程序员都应该知道的事的更多相关文章

  1. 每个php程序员都应该知道的15个最佳PHP库

    PHP是一种功能强大的web站点脚本语言,通过PHP,web网站开发者可以更容易地创建动态的引人入胜的web页面.开发人员可以使用PHP代码与一些网站模板和框架来提升功能和特性.然而,编写PHP代码是 ...

  2. 每个JavaScript程序员都需要知道的5个数组方法

    Array.forEach() .forEach() 方法能够方便的让你 遍历数组里的每个元素,你可以在回调函数里对每个元素进行操作..forEach()方法没有返回值,你不需要在回调函数里写retu ...

  3. 程序员必须要知道的Hadoop的一些事实

    程序员必须要知道的Hadoop的一些事实.现如今,Apache Hadoop已经无人不知无人不晓.当年雅虎搜索工程师Doug Cutting开发出这个用以创建分布式计算机环境的开源软...... 1: ...

  4. 每一位想有所成就的程序员都必须知道的15件事(走不一样的路,要去做,实践实践再实践,推销自己,关注市场)good

    从 为之漫笔作者:为之漫笔 有超过 100 人喜欢此条目 原文地址:How to advance your career? Read the Passionate Programmer! 我刚看完Ch ...

  5. 每个新手程序员都必须知道的Python技巧

    当下,Python 比以往的任何时候都更加流行,人们每天都在实践着 Python 是多么的强大且易用. 我从事 Python 编程已经有几年时间了,但是最近6个月才是全职的.下面列举的这些事情,是我最 ...

  6. 刨根究底字符编码之十——Unicode字符集的编码方式以及码点、码元

    Unicode字符集的编码方式以及码点.码元 一.字符编码方式CEF的选择 1. 由于Unicode字符集非常大,有些字符的编号(码点值)需要两个或两个以上字节来表示,而要对这样的编号进行编码,也必须 ...

  7. zzy:java采用的是16位的Unicode字符集作为编码方式------理解

    java语言使用16位的Unicode字符集作为编码方式,是疯狂Java中的原话. 1,编码方式只是针对字符类型的(不包括字符串类,数值类型int等,这些只是在解释[执行]的时候放到Jvm的不同内存块 ...

  8. 正则表达式: javascript Unicode 中文字符 编码区间:\u4e00-\u9fa5

    正则表达式: javascript Unicode 中文字符  编码区间:\u4e00-\u9fa5 RegExp 对象 javascript Unicode 中文字符的 编码区间: \u4e00-\ ...

  9. Java后端程序员都做些什么?

    这个问题来自于QQ网友,一句两句说不清楚,索性写个文章. 我刚开始做Web开发的时候,根本没有前端,后端之说. 原因很简单,那个时候服务器端的代码就是一切:接受浏览器的请求,实现业务逻辑,访问数据库, ...

随机推荐

  1. gdb的可视化工具安装

    红帽推出的insight https://www.sourceware.org/insight/index.php http://wiki.ubuntu.org.cn/Insight%E7%9A%84 ...

  2. cout输出控制——位数和精度控制

    刷到一道需要控制输出精度和位数的题目 刚开始以为单纯使用 iomanip 函数库里的 setprecision 就可以,但 OJ 给我判了答案错误,后来一想这样输出并不能限制位数只能限制有效位数. 比 ...

  3. Mac下搭建php开发环境[翻译]+自己总结(红字)

    原英文链接:http://www.codeweblog.com/mac-os-x-to-configure-apache-php-mysql/ Mac OS X 内置了Apache 和 PHP,这样使 ...

  4. AutoCAD 2007-2012 长度统计工具

    长度统计工具 下载 1 解压到磁盘 2 CAD 中输入命令 netload 3 选择文件 "CADLittleProgram.dll" 4 点击 Ps:后续会打包并支持2013-2 ...

  5. Git小记

    Git简~介 Git是一个分布式版本控制系统,其他的版本控制系统我只用过SVN,但用的时间不长.大家都知道,分布式的好处多多,而且分布式已经包含了集中式的几乎所有功能.Linus创造Git的传奇经历就 ...

  6. bing的简单英文字典工具

    今天看到园友心白水撰写的<简单翻译工具--必应字典第三方API使用方法>,感觉很不错,所以用Python也写了一个.源码如下: import urllib.request import j ...

  7. Python for Infomatics 第13章 网页服务三(译)

    注:文章原文为Dr. Charles Severance 的 <Python for Informatics>.文中代码用3.4版改写,并在本机测试通过. 13.6 应用程序接口API 现 ...

  8. spark shuffle 相关细节整理

    1.Shuffle Write 和Shuffle Read具体发生在哪里 2.哪里用到了Partitioner 3.何为mapSideCombine 4.何时进行排序 之前已经看过spark shuf ...

  9. Java 语句循环

    编写程序,显示Welcome to Java 五次. public class Welcome{ public static void main(String[] args){ int i;(定义变量 ...

  10. Asp.net 配置web.Config 在出错时跳转到相应页面

    <!--<customErrors mode="On" defaultRedirect="error.aspx">      <erro ...