Bin Joy's Blog

There is more than one way to do it...

[翻译] 宫川达彦的 Perl UTF-8 闪电教程

原文链接: Perl UTF-8 crash course

我还是常常看到 Perl 程序员对于 “Perl 以非常简单(有时 bug 频出但容易修复)的处理 Unicode 字串的方式” 理解不够的情况出现。

我坦承六七年前我对此也有着同样的误解,所以不愿看到别人再重蹈覆辙。那就赶紧拿出五分钟时间,忘记你所知道的一切,学一下这个简单的教程吧。

一) print($a, $b) 和 print($a . $b);

让我们暂时忘却 utf-8 标识、操作符重载,还有其它所有那些疯狂的东东。

当你看到 print($a, $b) 和 print($a . $b) 时,你觉得它们打印出的内容会一致吗?

1
2
3
4
5
> perl -le 'print "foo", "bar"'
foobar

> perl -le 'print "foo". "bar"'
foobar

它们会打印出相同的内容,否则就会让人疑惑不解。这一点 Perl 和你所见略同。

二) 默认 Latin-1 编码

然后,你再运行一下这个试试:

1
2
3
4
5
6
7
8
9
a) > perl -Mutf8 -MEncode -le \
        'print "テスト", encode_utf8("テスト")'
Wide character in print at -e line 1.
テストテスト

b) > perl -Mutf8 -MEncode -le \
        'print "テスト". encode_utf8("テスト")'
Wide character in print at -e line 1.
テストãã¹ã

哇,这次你向逗号版本和句点版本传递了相同的参数,但得到不同的结果。这让人疑惑不解,每一个都有可能是错的。你觉得哪个是正确的行为呢?

一些砖家也许会略过细节,说:“噢,utf-8 标志位和自动转换在 b 中起了作用。所以这是 bug 所在。” 如果你也这么认为,看来你对 perl 解释器非常了解,以至于适得其反。或者可以直接说,你错了。

看到控制台输出的那堆乱码,是否说明 a) 是 Perl 解释器的“正确行为”?

错,b) 才是正确的行为。因为后半部分 encode_utf8(“テスト”) 产生了9字母字节的字串给 perl 解释器,perl 解释器以为它是以 latin-1 编码的。因此它在控制台的输出是正确的(尽管看上去像垃圾)。Perl 字面上是以 latin-1 为默认编码进行处理的,这正是你想要的。

a) 代表了 Perl 的 bug(由于历史原因造成的),它根据参数的字符范围而改变了默认输出的编码格式。

1
2
3
4
use utf8;
use Encode;
print "テスト";               # <- 输出宽字符(Wide characters),以 UTF-8 编码打印
print encode_utf8("テスト")'; # <- 输出 Latin-1 字符,以 Latin-1 编码打印

perl 解释器对于产生了完全相同的八位位组(octet)序列以不同的编码打印时,结果是完全不同的字符串。

perl 解释器的这个行为可以通过使用 binmode 指定输出编码的格式來修正,命令行可以通过加上 -C 实现。 (主要是对 STDOUT 使用 utf8 编码):

1
2
3
4
5
6
7
c) > perl -C -Mutf8 -MEncode -le \
        'print "テスト", encode_utf8("テスト")'
テストãã¹ã

d) > perl -C -Mutf8 -MEncode -le \
        'print "テスト". encode_utf8("テスト")'
テストãã¹ã

这下逗号版本和句点版本都“正确地”显示了字符。

三) Latin-1 和 ASCII

现在我们退一步,看下没有宽字符时会打印出什么。

1
2
3
4
5
6
7
e) > perl -Mutf8 -MEncode -MData::Dump=dump -le \
        'print dump("テスト"). encode_utf8("テスト")' 
"\x{30C6}\x{30B9}\x{30C8}"テスト

f) > perl -C -Mutf8 -MEncode -MData::Dump=dump -le \
        'print dump("テスト"). encode_utf8("テスト")'
"\x{30C6}\x{30B9}\x{30C8}"ãã¹ã

这次同样以相同的代码(都是句点的方式),打印相同的变量,然后在输出时你得到了不同的结果。

正如我们前面所见,perl 解释器以 latin-1 编码打印 latin-1 字符串是一个 bug(例子e),在 f)中使用 -C 参数得到的是正确的结果。

总结

如果你向一个文件句柄打印八位位组而不指定任何一种编码格式(如本例中的 STDOUT),这时你还以为原始的编码会被保留,那你就是想倚重 perl 解释器的 bug。因为 perl 解释器的 bug 以及输出和终端编码不匹配的原因,碰巧你会看到原始的八位位组,看到它们出错了。

Perl 以 latin-1 为默认编码处理字符串。所以不管爽不爽,你都得这么做。这意味着你要告诉 perl 解释器八位位组是用哪种编码格式做的 decode。

latin-1 到 utf-8 的自动转换并非 bug 所在。自动转换是预期的动作,恰是你想要的。避免自动转换的思路既非是通过避免字符串连接(见例子 a. 使用逗号,而没有使用句点),更不是通过跳脱其它的变量(见e)。这些只是隐藏了问题所在,而非解决了问题本身。

自动转换会导致所谓的 “bug” 的唯一情形是,当你真正想向一个原始的文件句柄中打印字节流时。举个例子,当你创建了一份 JPEG 的二进制数据,打印到一个本地文件中。如果你创建了完整的 JPEG 数据,包括在它的 EXIF 区域的宽字符,perl 解析器突然以为这是 latin-1 字串而把它转换成 UTF-8 数据。

因为没有办法告诉 perl 解释器给它的字串其实是八位位组而非字串(我真希望能有办法),所以你不得不确认对所有打印到原始文件句柄的字符进行了正确的编码。混入没有正确编码的宽字符的话,问题在你,而非 perl 解释器。

如果你没有正确处理,就会看到 “Wide characters in print…” 的警告。出现这个时肯定是有原因的。

另见:

很巧合,Dave Cross 遇到了类似的讨论,在他的博客发了这篇《Unicode and Perl》简化版的小测试。 (译注:可以看我的译文:查看

Comments