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》简化版的小测试。 (译注:可以看我的译文:查看

[翻译] Unicode 与 Perl

Perl Weekly 第109期的主题是 “Unicode and Perl”,其中推荐了两篇关于 Unicode 的博文。一篇来自 perlhacks.com 的 Dave Cross,另一篇则来自重量级人物 Tatsuhiko Miyagawa(宫川达彦)。非常有趣的是两个博主都在博文最后引用了对方博文的地址。

这两篇博文虽说都是关于 Unicode 的,但是风格不尽相同。Dave Cross 的文章很短,就是给出了一个快速测试题,并称几天后会给出相关答案。但宫川的文章则是相对详细的示例分析和总结。

其实笔者在 2011 年自从读了 Effective Perl Programming 一书中关于 Unicode 的部分后感觉对 Perl 的 Unicode 问题已经明晰了很多,但是在日常编程中偶尔也会被编码问题困扰。比如最近在使用 Dancer 的过程中就遇到此类问题。看来编码这个问题一定要彻底搞清楚才可以。所以在阅读学习两篇博文的同时尽力做了翻译,以便有这方面学习需要的同学可以更快的理解。

两篇文字都会翻译,先翻译 Dave Cross 这篇短的。

正文

原文地址:http://perlhacks.com/2013/08/unicode-perl/

在过去几天我就遇到好几拨讨论,很显然有些人对于 Perl 如何处理 Unicode 还不甚了了。这篇文档条理清晰内容详实(这里还有一个不错的教程),但不知何故,人们仍旧会陷入误区。

下面是一个快速小测验。你能详细的说明这四个命令行程序的来龙去脉吗?为了取得一个好的绩效考核,在我们的代码中应该效仿哪一个呢?

1
2
3
4
5
6
7
8
$ perl -E'say "£"'
£
$ perl -Mutf8 -E'say "£"'
$ perl -C -E'say "£"'
£
$ perl -C -Mutf8 -E'say "£"'
£

为了统一环境,假设系统 locale 都设置为 en_US.UTF-8。

几天后我会公布详解。

更新:巧合的是, Miyagawa 在他的 blog 也发了一些非常相似的内容

译注:可以看我的译文 查看 另我查看了 perlhacks 的博客,并没有公布答案,其实看了宫川达彦的那篇后,也许你就不纠结他没有公布答案了:)

Dancer2 试用小记

安装 Dancer2

注:本部分参考 Dancer2::Manual

安装 Dancer2 的方式和安装其它 Perl 模块没有什么区别,使用 cpan 命令或是源码安装的方式都可以。

1
perl -MCPAN -e 'install Dancer2'

当然如果你已经在机器上安装了 cpanm 这个模块你可以直接 cpanm Dancer2 就可以了。如果没有安装 cpanm 你在类 unix 机器上可以使用下面这行命令方便的使用它:

1
wget -O - http://cpanmin.us | sudo perl - Dancer2

如果你没有 root 权限这里不要写 sudo 。

使用 Dancer2

Dancer2 没有它自己的生成器,要使用 Dancer1 的 dancer 命令来生成应用的目录结构。

1
dancer -a myapp

然后进入到 myapp 这个目录,执行:

1
for f in $(find {lib,bin} -type f) ; do cat $f | sed -e 's/Dancer/Dancer2/g' > $f.2 && mv -f $f.2 $f;chmod +x $f; done

这样就会将文件中的 Dancer 修改为 Dancer2。可以直接启动:

更新:现在 Dancer2 已经具有自己的生成器了,安装好 Dancer2 后,直接 dancer2 -a appname 就可以生成一个项目了。

1
./bin/app.pl

也可以使用 plackup 命令启动它,如果你已经安装了 Task::Plack 这个模块的话:

1
plackup ./bin/app.pl -p 5000

这样在浏览器输入本地地址和相应的端口就可以访问到 Dancer 的默认页面。

为什么使用 Dancer2

直接引述 Dancer 作者的话:减少了和其它模块的命名空间碰撞,引入 Moo 的威力,框架本身代码更少但性能更高,而且如果你想深入研究 DSL 的话,Dancer2 背后提供的是一个很强 OO 特性的 API。

注意点

  • 编码问题:Dancer2 的要在配置文件中指定模板的编码,否则会出现乱码的情况,尤其是我们使用中文。比如我在模板中设置使用 UTF-8 的编码,配置文件也写了 charset: UTF-8 这样的指定,但是页面的中文还是乱码,Dancer2::Cookbook 中说还要指定模板的编码,这和 Dancer1 还是有些不同的。正确的设置是这样的:
Dancer2 encoding configurationDancer2 CPAN DOC
1
2
3
4
charset: UTF-8
engines:
  template_toolkit:
    ENCODING: utf8
  • 依然是编码问题 – –||| : Dancer 的 to_json 函数在遇到汉字时前端会乱码,但使用 JSON 模块的 to_json 就不会,具体原因还待深入 。

  • 在开发 爱微链 过程中发现,默认模板 Template::Simple 在遇到多层数据结构时可能不能正确解析出想要的数据,所以建议即使是开发过程中也还是先设置好用 TT 吧。