从一个 bug ,引出 UTF-8 编码的讨论。
去年 8 月份,我尝试自己写代码解析 Linux Man-Pages 的页面。
Man-Pages 使用的是 groff 文本标记语言,其最大的特色就是格式标记宏置于行首,例如 man(7) 第 35~48 的内容为:
1 | .SH NAME |
每一行开头出现的字段就是标记宏,依次的含义为:
宏 | 说明 |
---|---|
.SH |
标题 |
.B |
加粗 |
.I |
斜体 |
.LP |
开始段落 |
经过解析后,这段文本在 man 程序里看起来会是这个样子。当然,.B
、.I
的效果在代码框里无法显示。
1 | NAME |
既然是 *nix 的产物,我在写代码解析这些文件的时候,第一反应就是使用 UTF-8 编码。附图 1 Unicode Logo
什么是 UTF-8 编码?这得从计算机如何表达文本说起。当人们说起“文本”,他们通常指显示在屏幕上的字符或者其他的记号;但是计算机不能直接处理这些字符和标记;它们只认识位(bit)和字节(byte)。
实际上,从屏幕上的每一块文本都是以某种字符编码(character encoding)的方式保存的。粗略地说就是,字符编码提供一种映射,使屏幕上显示的内容和内存、磁盘内存储的内容对应起来。有许多种不同的字符编码,有一些是为特定的语言,比如俄语、中文或者英语,设计、优化的,另外一些则可以用于多种语言的编码。
早在 1963 年,ANSI 发布了基于电报码的 ASCII 字符编码,它用 7 位 (bit) 表示了 27 = 128 个字符,只能勉强覆盖英文字符。后来随着电脑技术的传播,人们呼吁把字符编码扩充到 8 位也就是一个字节 (byte) ,可以涵盖 28 = 256 个字符。于是 ISO 在 1980 年代中期推出了 ISO 8859,256 个字符显然也不能满足需要,所以 8859 被分为十几个部分:从 8859-1 (西欧语言) 、8859-2 (中欧语言) ,直到 8859-16 (东南欧语言) ,覆盖了大部分使用拉丁字母的语言文字。
但人类的语言文字种类实在复杂,即使是 ISO 8859-16 也无法表达很多地区的文字。在这前后还出现了很多编码,例如 IBM 的 OEM 代码页、微软的 ANSI 代码页,还有中国大陆的 GB2312 和 GBK ,等等。人们也逐渐认识到编码不统一带来的种种不便。于是 ISO 和 统一码联盟合作推出了 Unicode 编码,Unicode 编码系统为表达任意语言的任意字符而设计。Unicode 编码一共提出了三种版本:
IETF 要求所有网络协议都支持 UTF-8,互联网电子邮件联盟 (Internet Mail Consortium,IMC) 也建议所有电子邮件软件都支持 UTF-8,所以它已成为互联网上的事实标准。*nix 系统中的默认文件编码也是 UTF-8。这也是为什么我第一反应是使用 UTF-8 编码来解析所有 Man-Pages 文件。
然而,出乎我意料的是,我的代码在对这些文件的解析过程中会间歇性遇到 UnicodeDecodeError
错误。很快我就确定这些导致出错的文件一定是使用了其他的编码方式。导致这个情况的原因也可想而知 —— 这些文档的编写者遍布世界各地,可能使用了不同的本地化编码方案,从而编码方式也参差不齐。然而,这种情况并不是一件好事。文档编码的混乱会给文档解析带来不便。
发现了这个问题后,我访问了 Linux Man-Pages 项目的 bugzilla 页面。通过搜索,并没有人提出过类似的问题。于是我决定给该项目提交 bug 。
附图 2 Linux Man Pages我写了个 Python 脚本,用于找出所有不是使用 UTF-8 编码的文档:
1 | #!/bin/python3 |
我的方法是直接使用 UTF-8 编码来递归遍历所有文档,如果遇到无法使用 Unicode 解析的字符,则将该文件打印出来。
得到这类文件的列表后,我就将这个列表连同说明 提交了上去:
I found that not all the pages are encoded using utf-8. It may cause problems once we try to parse them.
These files are:
- man3/fflush.3
- man3/toupper.3
- man3/updwtmp.3
- man3/encrypt.3
- man3/lockf.3
- man3/rand.3
- man3/fclose.3
- man3/strtok.3
- man2/close.2
- man2/getdomainname.2
- man2/madvise.2
- man2/umask.2
- man2/sysinfo.2
- man2/getrlimit.2
- man5/utmp.5
- man7/cp1251.7
- man7/iso_8859-2.7
- man7/armscii-8.7
- man7/suffixes.7
- man7/iso_8859-4.7
- man7/iso_8859-8.7
- man7/iso_8859-16.7
- man7/hier.7
- man7/iso_8859-13.7
- man7/koi8-u.7
- man7/environ.7
- man7/iso_8859-15.7
- man7/iso_8859-9.7
- man7/iso_8859-11.7
- man7/iso_8859-14.7
- man7/iso_8859-10.7
- man7/iso_8859-6.7
- man7/iso_8859-1.7
- man7/iso_8859-7.7
- man7/koi8-r.7
- man7/iso_8859-5.7
- man7/iso_8859-3.7
大概过了三个月,Man-Pages 的一位名为 Peter Schiffer 的 Contributor 写了两个脚本作为回复:
Peter 的第一个脚本只有寥寥几行:
1 | if [[ $# -lt 1 ]]; then |
其中,获取文件编码用到了 file
命令:
1 | enc=$(file -bi <文件名> | cut -d = -f 2) # 获取文件的编码类型,并赋给 enc 变量 |
如果 enc
变量不为 ASCII 的话,那么将文件的编码格式和首行的编码信息打印出来(如果有的话):
1 | # 获取文件首行的编码信息(如果有的话) |
Peter 的脚本可以得到更加丰富的信息,由于检测的是非 ASCII 文件,得到的文件数量也比我的结果多一些:
1 | $ ./print_encoding.sh man?/* |
Peter 的第二个脚本也很好理解,找出所有非 ASCII 编码文件,使用 iconv
命令转换到 UTF-8 编码:
1 | if [[ $# -lt 2 ]]; then |
Peter 的两个脚本看起来已经足以解决编码问题了。不过这个 bug 却一直拖到了今年的情人节&元宵节(也有人说是缘消节?>_<
)才有了进一步进展。Man-Pages 项目的主要维护者 Michael Kerrisk 现身提出了两个疑问:
Peter,
Sorry to be slow following up on this. Thanks for the scripts.
As some background, I’ll just note that the current encoding markers in the iso_8859* pages were added in response to this 2009 bug report: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=519209
It seems a reasonable idea to convert everything to UTF-8, but I have some concerns/questions.
Is the encoding line:
'\" t -*- coding: UTF-8 -*-
really needed, or does modern groff just work this out?I’m concerned about backward compatibility issues. As in: what if someone loads the man pages onto a system with old groff. Now, as far as I can work out, groff added input unicode support in v1.20, 2009 ( http://lists.gnu.org/archive/html/groff/2009-01/msg00011.html ). So, perhaps that’s long enough ago that we don’t need to worry too much about these issues.
Any thoughts?
Michael 的主要疑问有两点:
'\" t -*- coding: UTF-8 -*-
,这一行是否真的有必要,还是 groff 本身就可以自己判断编码?Michael 的疑虑不无道理。转换整个项目的编码不是一件太 trivial 的事情,小心使得万年船。他于是又在 linux-man 邮件列表里发帖征求其他维护者的意见。
经过大家的讨论,后来大家达成了以下意见:
两天后我收到了 Michael 发来的邮件,这个 bug 随着新版本的推出,在 2014 年 2 月 16 日被成功关闭。最新的 3.59 和 3.60 两个版本就是对针对这个 bug 的解决和后期完善。这也是我作为一个多年 Linux 用户,对这个社区所做的一个小贡献。这个过程中,我看到了这些 contributors 的严谨和负责。今后会继续努力,向他们看齐!:-p