Erlang & Unicode

Posted by Elton's Blog on November 13, 2012

Erlang的string实际上就是整数项组成的list,注意string的编解码使用是使用ISO-latin-1字符集,即:每8字节当成一个整体进行解读;这个字符集是Unicode的子集.Erlang list编解码很容易扩展到整个unicode编码:由于编码是整数和字符的对应关系,只要list中的整函数是有效的Unicode codepoint就可以找到对应的字符;

二进制数据处理起来就麻烦一些了,二进制数据是紧凑排列的:一个字节代表一个字符,而不是两个字(word)一个字符,这里如果存在疑问可以查看erlang官方文档中关于内存消耗的列表:http://www.erlang.org/doc/efficiency_guide/advanced.html .平时我们使用的erlang:list_to_binary,常规的Erlang string(ISO-latin编码的string)可以逐字节逐字符顺利转成binary.但是超出ISO-latin编码范围就会出错了,看下面的例子:

1> L=[10,12,23,45].
[10,12,23,45]
2> list_to_binary(L).
<<10,12,23,45>>
3> L2= "中国".      
[20013,22269]
4> list_to_binary(L2).
** exception error: bad argument
     in function  list_to_binary/1
        called as list_to_binary([20013,22269])
5> unicode:characters_to_list(L2).
[20013,22269]
6> unicode:characters_to_binary(L2). %%注意"中国"用二进制占用了6个字节
<<228,184,173,229,155,189>>

UTF-8是Erlang二进制处理的标准编码形式,一旦出现需要处理Unicode二进制数据的场景,默认就会选择UTF8编码.比特语法支持使用其它的编解码方式,但是erlang类库中处理二进制都是使用UTF-8编码.字符串可以接受Unicode字符,但是Erlang的语言元素编写还是限制在ISO-latin-1的范围内.Erlang编译过程依然是使用ISO-latin-1编码,这样的影响是什么呢?代码中出现的Unicode字符会有部分无法在ISO-latin-1找到对应的字符,那怎么办呢?没关系,找不到对应的字符就按照整形数去处理就好了.

Erlang Shell对unicode的支持要强一些,但是也并不完善,下面我们通过一系列实验来看上面的问题,在test模块里面我们准备两条测试数据:

data()->
   "hello 中国 ren".
data2()->
   <<"hello 中国 ren">>.

启动Erlang Shell,我们对比一下数据之间的差异:

1> "hello 中国 ren".  %%在shell中输入包含中文的string,可以看到它就是一个List,注意中文字符对应的数值
[104,101,108,108,111,32,20013,22269,32,114,101,110]
2> test:data().        %%注意下面的数据,中文部分数值已经被切割成两组数据
[104,101,108,108,111,32,228,184,173,229,155,189,32,114,101,
110]   
3> <<"hello 中国 ren">>.  %%而这样的数据在shell中直接出错了 (注意:windows下可能是正常的)
** exception error: bad argument
4> test:data2().   %%看看这里二进制的输出,数值上是和v(2)的数值上是一致的
<<104,101,108,108,111,32,228,184,173,229,155,189,32,114,
  101,110>>
5> unicode:characters_to_binary(v(1)).  %%把v(1)的结果转成二进制,为什么不用list_to_binary?往下看
<<104,101,108,108,111,32,228,184,173,229,155,189,32,114,
  101,110>>
6> io:format("~ts~n",[v(1)]). %%注意这里格式化的时候使用的修饰符是~ts
hello 中国 ren
ok
7> io:format("~ts~n",[v(2)]).  %%v(2)输出的内容并不是我们期望的                       
hello 中国 ren
ok
8> io:getopts().  %%是不是觉得少检查了点什么?是的 看看环境编码
[{expand_fun,#Fun},
{echo,true},
{binary,false},
{encoding,unicode}]
9> list_to_binary(v(1)). %%看到了吧,这样会异常的
** exception error: bad argument
     in function  list_to_binary/1
        called as list_to_binary([104,101,108,108,111,32,20013,22269,32,114,101,110])
10> list_to_binary(v(2)).
<<104,101,108,108,111,32,228,184,173,229,155,189,32,114,
  101,110>>
14> 
</pre>

进行到这里,下面这个问题就有答案了,我们在Shell中执行下面的语句:
14>  re:run("hello 中国 ren", "[\x{4e00}-\x{9fff}]+", [unicode]).
{match,[{6,6}]}
15> 
然后我们把这条语句放在模块代码中执行:
re() ->
   re:run("hello 中国 ren", "[\x{4e00}-\x{9fff}]+", [unicode]).
执行结果:
15> test:re().
nomatch
16>
答案就是:在模块文件进行编译的时候使用的是ISO-latin-1,其中的中文并不在其字符集中,所以转成了两组数字!被转成两组数字之后,也就无法被正则表达式命中了.而在Erlang Shell中,中文字符可以被正确编码,所以会被正则命中.而仔细关注一下正则表达式,其实就是大致上覆盖了中文字符在unicode字符集中对应的数值区间. 对于这种情况只要让unicode避开编译阶段就可以了,比如把这类文本放在外部文本中,下面立涛给的这份代码样例中演示了从外部文件读取文本内容,并匹配中文. https://gist.github.com/2768621 一个细节"~ts" 修饰符 The Erlang compiler will interpret the code as ISO-8859-1 encoded text, which limits you to Latin characters."translation modifier" when working with Unicode texts. The modifier is "t". When applied to the "s" control character in a formatting string, it accepts all Unicode codepoints and expect binaries to be in UTF-8.
1> io:format("~ts",[unicode:characters_to_binary([20013])]).
中ok
2> io:format("~ts",[unicode:characters_to_binary([20013,22269])]).
中国ok
3>  L=[229,136,157,231,186,167], io:format("~ts", [list_to_binary(L)]).             
初级ok