Erlang 20

3.在Erlang中使用Unicode | 3. Using Unicode in Erlang

3 在Erlang中使用Unicode

3.1 Unicode实现

实现对Unicode字符集的支持是一个持续的过程。Erlang增强建议(EEP)10概述了Unicode支持的基础知识,并指定了所有支持Unicode的模块将来要处理的二进制文件中的默认编码。

以下是迄今为止所做工作的概述:

  • EEP 10中描述的功能是在Erlang/OTP R13A中实现的。

  • Erlang/OTP R14B01增加了对Unicode文件名的支持,但是它并没有完成,并且在没有给出文件名编码保证的平台上被默认禁用。

  • 使用Erlang / OTP R16A支持UTF-8编码的源代码,许多应用程序的增强功能支持Unicode编码的文件名以及在许多情况下支持UTF-8编码文件。最值得注意的是UTF-8在UTF-8读取文件中的支持,对UTF-8的file:consult/1发布处理程序支持以及对I / O系统中Unicode字符集的更多支持。

  • 在Erlang/OTP 17.0中,Erlang源文件的编码默认值被切换到UTF-8。

  • 在Erlang/OTP 20.0中,原子和函数可以包含Unicode字符。模块名称、应用程序名称和节点名称仍然仅限于ISO拉丁-1范围。

对于归一化的形式加入,支持unicodestring模块现在处理UTF-8编码的二进制文件。

本节概述了当前的Unicode支持,并给出了一些处理Unicode数据的方法。

3.2 了解Unicode

Erlang中Unicode支持的经验表明,理解Unicode字符和编码并不像人们想象的那么容易。由于该领域的复杂性和标准的含义,需要对概念进行彻底的理解。

另外,Erlang的实现需要理解许多(Erlang)程序员从来不会遇到的概念。要理解和使用Unicode字符,即使您是一位有经验的程序员,也需要您彻底研究该主题。

例如,考虑大写字母和小写字母之间的转换问题。阅读该标准使您意识到,并非所有脚本中都存在简单的一对一映射,例如:

  • 在德语中,字母“ß”(尖锐s)是小写字母,但大写字母相当于“SS”。

  • 在希腊语中,字母Σ有两种不同的小写形式,在单词的最后位置上有“ab”,而在其他地方则有“σ”。

  • 在土耳其语中,“i”以小写和大写两种形式存在。

  • 西里尔字母“i”通常没有小写形式。

  • 没有大写(或小写)概念的语言。

  • w意味着error_logger只要在目录列表中“skipped”错误编码的文件名就会发送警告。w是默认值。

  • i意味着错误编码的文件名将被忽略。

  • e 意味着只要遇到错误编码的文件名(或目录名称),API函数就会返回错误。

注意file:read_link/1如果链接指向无效的文件名,则始终返回错误。

在Unicode文件名模式下,给open_port/2带有选项的BIF的文件名{spawn_executable,...}也被解释为Unicode。因此,args使用时可用选项中指定的参数列表spawn_executable。使用二进制文件可以避免参数的UTF-8转换,请参见部分Notes About Raw Filenames

请注意,打开文件时指定的文件编码选项与文件名编码约定无关。您可以很好地打开包含以UTF-8编码的数据的文件,但文件名采用bytewise(latin1)编码或相反。

Erlang驱动程序和NIF共享对象仍然不能用包含代码点> 127的名称来命名。此限制将在未来版本中删除。但是,Erlang模块可以,但它绝对不是一个好主意,仍然被认为是实验性的。

关于原始文件名的注释

在ERTS 5.8.2(Erlang/OTP R14B01)中引入了原始文件名以及Unicode文件名支持。在系统中引入“原始文件名”的原因是能够一致地表示在同一系统上以不同编码指定的文件名。虚拟机自动将非UTF-8文件名转换为Unicode字符列表似乎很实际,但这会打开重复文件名和其他不一致的行为。

考虑一个包含ISO Latin-1中名为“björn”的文件的目录,而Erlang VM以Unicode文件名模式运行(因此需要UTF-8文件命名)。ISO Latin-1名称不是有效的UTF-8,例如,人们可能会认为自动转换file:list_dir/1是一个好主意。但是如果我们稍后尝试打开文件并将名称作为Unicode列表(从ISO Latin-1文件名奇妙地转换),会发生什么?VM将文件名转换为UTF-8,因为这是预期的编码。实际上,这意味着试图打开名为<<“björn/utf8 >>的文件。该文件不存在,即使它存在,也不会与列出的文件相同。我们甚至可以创建两个名为“björn”的文件,一个以UTF-8编码命名,另一个不命名。如果file:list_dir/1会自动将ISO Latin-1文件名转换为列表,我们会得到两个相同的文件名作为结果。为了避免这种情况,我们必须区分根据Unicode文件命名约定(即UTF-8)正确编码的文件名和在编码下无效的文件名。通过常用函数file:list_dir/1,在Unicode文件名翻译模式下,错误编码的文件名会被忽略,但通过函数file:list_dir_all/1,具有无效编码的文件名将作为“原始”文件名返回,即作为二进制文件返回。

file模块接受原始文件名作为输入。open_port{spawn_executable, ...} ...)也接受他们。如前所述,选项列表中指定的参数open_port{spawn_executable, ...} ...)将与文件名进行相同的转换,这意味着可执行文件也以UTF-8的参数提供。通过将参数作为二进制文件进行处理,可以避免这种翻译与文件名的处理方式一致。

在非默认的系统上强制Unicode文件名翻译模式在Erlang/OTP R14B01中被认为是实验性的。这是因为最初的实现没有忽略编码错误的文件名,所以原始文件名可能会在系统中意外传播。从Erlang/OTP R16B开始,错误编码的文件名只能通过特殊功能(如file:list_dir_all/1)检索。由于对现有代码的影响因此低得多,现在支持。Unicode文件名翻译预计将在未来的版本中被默认。

即使您在未使用由VM自动完成的Unicode文件命名转换的情况下运行,也可以使用以UTF-8编码的原始文件名来访问和创建名称采用UTF-8编码的文件。由于使用UTF-8文件名的惯例正在蔓延,所以在某些情况下,无论启动Erlang VM的模式如何,都可以强制执行UTF-8编码。

关于MacOS X的注记

所述vfs的MacOS X的层强制在一个积极方式UTF-8的文件名。较早的版本通过拒绝创建非UTF-8符合的文件名来做到这一点,而较新版本用序列“%HH”替换违规字节,其中HH是以十六进制符号表示的原始字符。由于默认情况下在MacOS X上启用Unicode转换,遇到这种情况的唯一方法是使用标志启动VM +fnl或使用bytewise(latin1)编码使用原始文件名。如果使用原始文件名(包含127至255字符的字节编码)来创建文件,则无法使用与创建文件相同的名称打开该文件。对于这种行为没有补救办法,除了保持正确的编码文件名。

MacOS X重新组织文件名,以便重音符号等的表示使用“组合字符”。例如,字符ö表示为代码点[111,776],其中111是字符,o并且776是特殊口音字符“组合Diaieresis”。这种正常化Unicode的方式很少使用。Erlang在检索时以相反的方式对这些文件名进行归一化处理,以便使用组合重音符的文件名不会传递给Erlang应用程序。在Erlang中,文件名“björn”被检索为[98,106,246,114,110],而不是[98,106,117,776,114,110],尽管文件系统可以有不同的想法。访问文件时,重新组合标准化会重做,所以Erlang程序员通常可以忽略它。

3.9环境和参数中的Unicode

环境变量及其解释的处理方式与文件名相同。如果启用Unicode文件名,则环境变量以及Erlang虚拟机的参数预计将采用Unicode。

如果Unicode文件名被启用,调用os:getenv/0,1,os:putenv/2以及os:unsetenv/1处理Unicode字符串。在类Unix平台上,内置函数将UTF-8的环境变量转换为Unicode字符串/从Unicode字符串转换,可能代码点> 255.在Windows上,使用Unicode版本的环境系统API,code points>255被允许。

在类Unix操作系统上,如果启用了Unicode文件名,那么参数预计为UTF-8,无需转换。

3.10 Unicode识别模块

Erlang/OTP中的大多数模块都是Unicode-unaware,因为它们没有Unicode的概念,不应该有。通常它们处理非文本或面向字节的数据(如gen_tcp)。

处理文本数据的模块(例如io_libstring有时需要转换或扩展才能处理Unicode字符。

幸运的是,大多数文本数据已经存储在列表中,范围检查功能很少,所以模块string对于Unicode字符串很适合,而且几乎不需要转换或扩展。

然而,有些模块被更改为显式地识别Unicode。这些单元包括:

unicode

unicode模块显然是可识别Unicode的。它包含用于在不同的Unicode格式和用于识别字节顺序标记的一些实用程序之间转换的函数 在没有这个模块的情况下,很少有处理Unicode数据的程序可以幸免。

io

io模块已经与实际的I/O协议一起扩展以处理Unicode数据。这意味着许多函数需要二进制文件处于UTF-8格式,并且还有一些修饰符用于格式化控制序列以允许输出Unicode字符串。

file**,** group**,** user

整个系统中的I/O服务器可以处理Unicode数据,并具有用于在输出到设备或从设备输入时转换数据的选项。如前所述,shell模块支持Unicode终端,file模块允许在磁盘上进行各种Unicode格式的翻译。

然而,使用Unicode数据读写文件并不是最好的file模块,因为它的接口是面向字节的。使用Unicode编码打开的文件(如UTF-8)最好使用该io模块读取或写入。

re

re模块允许匹配Unicode字符串作为特殊选项。由于库以二进制文件为中心,Unicode支持以UTF-8为中心。

wx

图形库wx广泛支持Unicode文本。

string模块完美适用于Unicode字符串和ISO Latin-1字符串,但与语言相关的函数string:uppercase/1string:lowercase/1。这两个函数对于当前形式的Unicode字符无法正确运行,因为在案例之间转换文本时需要考虑语言和区域设置问题。在国际环境中转换案件是OTP尚未解决的一个重大课题。

3.11文件中的Unicode数据

尽管Erlang可以处理许多形式的Unicode数据,但并不意味着任何文件的内容都可以是Unicode文本。外部实体(如端口和I/O服务器)通常不支持Unicode。

端口始终是面向字节的,因此在将不确定的数据发送到端口的字节编码之前,请确保将其编码为适当的Unicode编码。有时这意味着只有部分数据必须被编码为UTF-8。某些部分可以是二进制数据(如长度指示符)或其他不能进行字符编码的其他部分,因此不存在自动翻译。

I/O服务器的行为有点不同。连接到终端(或stdout)的I/O服务器通常可以处理Unicode数据,而不考虑编码选项。当人们想要一个现代化的环境,但在写入一个古老的终端或管道时不想崩溃时,这很方便。

一个文件可以有一个编码选项,使得它通常可以被io模块使用(例如{encoding,utf8}),但默认情况下打开为一个面向字节的文件。该file模块是面向字节的,因此只能使用该模块写入ISO Latin-1字符。io如果要将Unicode数据输出到encodinglatin1(按字节编码)之外的文件,请使用该模块。例如,file:open(Name,[read,{encoding,utf8}])使用打开的文件无法正常读取file:read(File,N),但使用io模块从其中检索Unicode数据,这有点令人困惑。其原因是,file:readfile:write(和朋友)纯粹是以字节为导向的,应该是,因为这是以字节为单位访问除文本文件以外的文件的方式。与端口一样,您可以通过“手动”将数据转换为选择的编码(使用unicode模块或位语法),然后将其输出到bytewise(latin1)编码文件中,将编码数据写入文件。

建议:

  • 使用file模块打开以字节方式访问的文件({encoding,latin1})。

  • 使用io任何其他编码访问文件时使用该模块(例如{encoding,uf8})。

从文件中读取 Erlang 语法的函数coding:可以识别注释,因此可以处理输入中的 Unicode 数据。将 Erlang 条款写入文件时,建议在适用时插入这些注释:

$ erl +fna +pc unicode Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.10.1 (abort with ^G) 1> file:write_file("test.term",<<"%% coding: utf-8\n[{\"Юникод\",4711}].\n"/utf8>>). ok 2> file:consult("test.term"). {ok,[[{"Юникод",4711}]]}

3.12备选方案摘要

Unicode支持由命令行开关,一些标准环境变量和您正在使用的OTP版本控制。大多数选项主要影响如何显示Unicode数据,而不是标准库中API的功能。这意味着Erlang程序通常不需要关心这些选项,它们更适合于开发环境。一个Erlang程序可以编写,以便它可以很好地工作,而不管系统的类型或有效的Unicode选项。

下面是影响Unicode的设置的摘要:

LANG和LC_CTYPE 环境变量

操作系统中的语言设置主要影响shell。{encoding, unicode}只有当环境告诉它UTF-8被允许时,终端(即组长)才会运行。此设置与您正在使用的终端相对应。

如果Erlang以标志+fna(Erlang/OTP 17.0默认)启动,环境也会影响文件名解释。

你可以通过调用来检查这个设置io:getopts(),它给你一个包含{encoding,unicode}或的选项列表{encoding,latin1}

The+pc{**unicode**|**latin1**} flag toerl(1)

这个标志会影响壳做启发式检测字符串时什么被解释为字符串数据io/ io_lib:format"~tp"~tP格式化指令,如前面所述。

您可以通过调用选中此选项io:printable_range/0,返回unicodelatin1。为了与未来(预期的)设置扩展兼容,而是根据设置io_lib:printable_list/1检查列表是否可打印。该功能考虑到从中返回的新可能的设置io:printable_range/0

+fn** {* * l** | * * u** | * * a** } {* * w** | * * i** | * * e** }标志**erl(1)

该标志影响如何解释文件名。在具有透明文件命名的操作系统上,必须指定这个名称以允许以Unicode字符命名文件(并且正确解释包含字符> 255的文件名)。

  • +fnl 意味着按字节顺序解释文件名,这是在UTF-8文件命名广泛传播之前表示ISO Latin-1文件名的常用方法。

  • +fnu 意味着文件名以UTF-8编码,而UTF-8是现在常用的方案(虽然没有强制执行)。

  • +fna意味着你自动之间进行选择+fnl,并+fnu根据环境变量LANGLC_CTYPE。这确实是一种乐观的启发式方法,没有什么能够强制用户使用与文件系统相同的编码,但这通常是这种情况。这是除MacOS X之外的所有类Unix操作系统的默认设置。

文件名翻译模式可以用函数读取,函数file:native_name_encoding/0返回latin1(按字节编码)或utf8

epp:default_encoding/0

该函数在当前运行的版本中返回Erlang源文件的默认编码(如果不存在编码注释)。在Erlang/OTP R16B中,latin1返回了(按字节编码)。从Erlang/OTP 17.0开始,utf8返回。

可以使用注释指定每个文件的编码,如epp(3)模块。

io:setopts/1,2 和-oldshell** / * *-noshell标志

当Erlang与-oldshellor 启动时-noshell,I/O服务器standard_io默认设置为按字节编码,而交互式shell默认为环境变量所说的内容。

您可以使用函数设置文件或其他I/O服务器的编码io:setopts/2。这也可以在打开文件时设置。standard_io无条件地将终端(或其他服务器)设置为选项{encoding,utf8}意味着将UTF-8编码字符写入设备,而不管Erlang是如何启动的或用户的环境。

使用已知编码编写或读取文本文件时,使用encoding选项打开文件非常方便。

您可以使用功能检索I/O服务器的encoding设置io:getopts()

3.13 Recipes

从Unicode开始时,人们常常会在一些常见问题上出现问题。本节描述了处理Unicode数据的一些方法。

字节顺序标记

在文本文件中识别编码的常用方法是首先在文件中添加字节顺序标记(BOM)。BOM是以与剩余文件相同的方式编码的代码点16#FEFF。如果要读取这样的文件,则前几个字节(取决于编码)不是文本的一部分。该代码概述了如何打开被认为具有BOM的文件,并设置文件的编码和位置以便进一步顺序读取(最好使用io模块)。

注意,代码中省略了错误处理:

open_bom_file_for_reading(File) -> {ok,F} = file:open(File,[read,binary]), {ok,Bin} = file:read(F,4), {Type,Bytes} = unicode:bom_to_encoding(Bin), file:position(F,Bytes), io:setopts(F,[{encoding,Type}]), {ok,F}.

unicode:bom_to_encoding/1功能标识至少四个字节的二进制编码。它返回一个适合于设置文件编码的术语,即BOM的字节长度,以便相应地设置文件位置。请注意,该函数file:position/2始终对字节偏移起作用,因此需要BOM的字节长度。

首先打开一个用于写入和放置BOM的文件甚至更简单:

open_bom_file_for_writing(File,Encoding) -> {ok,F} = file:open(File,[write,binary]), ok = file:write(File,unicode:encoding_to_bom(Encoding)), io:setopts(F,[{encoding,Encoding}]), {ok,F}.

在这两种情况下,最好使用io模块,因为该模块中的函数可以处理超出ISO拉丁-1范围的代码点。

格式化I/O

在读取和写入支持Unicode的实体(如为Unicode转换打开的文件)时,可能需要使用io模块或io_lib模块中的函数来设置文本字符串的格式。出于向后兼容的原因,这些函数不接受任何列表作为字符串,但在处理Unicode文本时需要特殊的翻译修饰符。修饰符是t。应用于控制s格式化字符串中的字符时,它接受所有Unicode代码点,并希望二进制文件处于UTF-8状态:

1> io:format("~ts~n",[<<"åäö"/utf8>>]). åäö ok 2> io:format("~s~n",[<<"åäö"/utf8>>]). åäö ok

很明显,第二个io:format/2给出了不希望的输出,因为UTF-8二进制文件不在latin1。为了向后兼容,未加前缀的控制字符s需要二进制文件中包含字节编码的ISO Latin-1字符,列表中只包含<256的代码点。

只要数据总是列表,修饰符t就可以用于任何字符串,但是当涉及二进制数据时,必须注意正确选择格式化字符。一个字节编码的二进制也被解释为一个字符串,并且即使在使用~ts时也被打印,但它可能被误认为是一个有效的UTF-8字符串。因此,~ts如果二进制文件包含按字节编码的字符而不是UTF-8,则应避免使用该控件。

功能io_lib:format/2行为相似。它被定义为返回一个深度的字符列表,并且输出可以很容易地转换为二进制数据,以通过简单的方式在任何设备上输出erlang:list_to_binary/1。使用翻译修饰符时,列表可以包含不能存储在一个字节中的字符。然后呼叫erlang:list_to_binary/1失败。但是,如果要与之通信的I/O服务器支持Unicode,则返回的列表仍可以直接使用:

$ erl +pc unicode Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.10.1 (abort with ^G) 1> io_lib:format("~ts~n", ["Γιούνικοντ"]). ["Γιούνικοντ","\n"] 2> io:put_chars(io_lib:format("~ts~n", ["Γιούνικοντ"])). Γιούνικοντ ok

Unicode字符串以Unicode列表的形式返回,因为Erlang shell使用Unicode编码(并且所有Unicode字符均视为可打印)。Unicode列表是功能的有效输入io:put_chars/2,因此可以在任何支持Unicode的设备上输出数据。如果设备是终端,则字符以格式\x{H 输出,}如果编码为latin1。否则,在UTF-8中(对于非交互式终端:“oldshell”或“noshell”)或适合正确显示字符的任何内容(对于交互式终端:常规shell)。

因此,您始终可以将Unicode数据发送到standard_io设备。但是,如果文件encoding被设置为其他值,文件只接受超出ISO Latin-1的Unicode代码点latin1

UTF-8的启发式识别

尽管强烈建议在处理之前已知二进制数据中字符的编码,但这并非总是可行。在一个典型的Linux系统上,有一个UTF-8和ISO Latin-1文本文件的组合,文件中很少有任何BOM用于识别它们。

UTF-8被设计为使得在解码为UTF-8时,具有超出7位ASCII范围的数字的ISO Latin-1字符很少被认为是有效的。因此,通常可以使用启发式来确定文件是否使用UTF-8或者使用ISO Latin-1编码(每个字符一个字节)。该unicode模块可用于确定数据是否可以解释为UTF-8:

heuristic_encoding_bin(Bin) when is_binary(Bin) -> case unicode:characters_to_binary(Bin,utf8,utf8) of Bin -> utf8; _ -> latin1 end.

如果您没有完整的文件内容二进制文件,您可以通过该文件分块并逐个检查一部分。{incomplete,Decoded,Rest}函数的返回元组unicode:characters_to_binary/1,2,3派上用场。从文件中读取的一个数据块的不完整休息被预先添加到下一个块中,因此,当以UTF-8编码读取字节块时,我们避免了字符边界问题:

heuristic_encoding_file(FileName) -> {ok,F} = file:open(FileName,[read,binary]), loop_through_file(F,<<>>,file:read(F,1024)). loop_through_file(_,<<>>,eof) -> utf8; loop_through_file(_,_,eof) -> latin1; loop_through_file(F,Acc,{ok,Bin}) when is_binary(Bin) -> case unicode:characters_to_binary([Acc,Bin]) of {error,_,_} -> latin1; {incomplete,_,Rest} -> loop_through_file(F,Rest,file:read(F,1024) Res when is_binary(Res) -> loop_through_file(F,<<>>,file:read(F,1024)) end.

另一种选择是尝试以UTF-8编码读取整个文件并查看是否失败。在这里我们需要使用函数读取文件io:get_chars/3,因为我们必须读取代码点> 255的字符:

heuristic_encoding_file2(FileName) -> {ok,F} = file:open(FileName,[read,binary,{encoding,utf8}]), loop_through_file2(F,io:get_chars(F,'',1024)). loop_through_file2(_,eof) -> utf8; loop_through_file2(_,{error,_Err}) -> latin1; loop_through_file2(F,Bin) when is_binary(Bin) -> loop_through_file2(F,io:get_chars(F,'',1024)).

UTF-8字节表

由于各种原因,有时您可以有一个UTF-8字节的列表.。这不是Unicode字符的常规字符串,因为每个List元素都不包含一个字符。相反,您可以获得二进制文件中的“原始”UTF-8编码。通过首先将每个字节转换为二进制,然后将UTF-8编码字符的二进制转换回Unicode字符串,这很容易转换为适当的Unicode字符串:

utf8_list_to_string(StrangeList) -> unicode:characters_to_list(list_to_binary(StrangeList)).

双UTF-8编码

在使用二进制文件时,你可以得到可怕的“双重UTF-8编码”,在你的二进制文件或文件中编码奇怪的字符。换句话说,您可以获得第二次编码为UTF-8的UTF-8编码二进制文件。一种常见的情况是你逐字节读取文件的位置,但内容已经是UTF-8。如果您随后将字节转换为UTF-8,例如使用unicode模块或通过写入使用选项打开的文件{encoding,utf8},则您将输入文件中的每个字节编码为UTF-8,而不是原始文本的每个字符(一个字符可以用许多字节编码)。除了确定哪些数据以哪种格式进行编码以外,没有任何真正的补救措施,并且从不再将UTF-8数据(可能逐字节地从文件中读取)转换为UTF-8。

到目前为止,发生这种情况的最常见的情况是,当您获取UTF-8的列表而不是正确的Unicode字符串时,然后在二进制文件或文件中将它们转换为UTF-8:

wrong_thing_to_do() -> {ok,Bin} = file:read_file("an_utf8_encoded_file.txt"), MyList = binary_to_list(Bin), %% Wrong! It is an utf8 binary! {ok,C} = file:open("catastrophe.txt",[write,{encoding,utf8}]), io:put_chars(C,MyList), %% Expects a Unicode string, but get UTF-8 %% bytes in a list! file:close(C). %% The file catastrophe.txt contains more or less unreadable %% garbage!

在将二进制文件转换为字符串之前,请确保您知道二进制文件包含了什么。如果没有其他选项,请尝试启发式:

if_you_can_not_know() -> {ok,Bin} = file:read_file("maybe_utf8_encoded_file.txt"), MyList = case unicode:characters_to_list(Bin) of L when is_list(L) -> L; _ -> binary_to_list(Bin) %% The file was bytewise encoded end, %% Now we know that the list is a Unicode string, not a list of UTF-8 bytes {ok,G} = file:open("greatness.txt",[write,{encoding,utf8}]), io:put_chars(G,MyList), %% Expects a Unicode string, which is what it gets! file:close(G). %% The file contains valid UTF-8 encoded Unicode characters!