3.在Erlang中使用Unicode | 3. Using Unicode in Erlang
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范围。
对于归一化的形式加入,支持unicode
和string
模块现在处理UTF-8编码的二进制文件。
本节概述了当前的Unicode支持,并给出了一些处理Unicode数据的方法。
3.2 了解Unicode
Erlang中Unicode支持的经验表明,理解Unicode字符和编码并不像人们想象的那么容易。由于该领域的复杂性和标准的含义,需要对概念进行彻底的理解。
另外,Erlang的实现需要理解许多(Erlang)程序员从来不会遇到的概念。要理解和使用Unicode字符,即使您是一位有经验的程序员,也需要您彻底研究该主题。
例如,考虑大写字母和小写字母之间的转换问题。阅读该标准使您意识到,并非所有脚本中都存在简单的一对一映射,例如:
- 在德语中,字母“ß”(尖锐s)是小写字母,但大写字母相当于“SS”。
- 在希腊语中,字母Σ有两种不同的小写形式,在单词的最后位置上有“ab”,而在其他地方则有“σ”。
- 在土耳其语中,“i”以小写和大写两种形式存在。
- 西里尔字母“i”通常没有小写形式。
- 没有大写(或小写)概念的语言。
所以,一个转换函数一次不仅要知道一个字符,还要知道整个句子,要翻译的自然语言,输入和输出字符串长度的差异等等。Erlang/OTP目前没有Unicode 大写/小写功能和语言特定处理,但公开可用的库解决了这些问题。
另一个例子是重音字符,其中相同的字形具有两种不同的表示。瑞典字母“ö”就是一个例子。Unicode标准有一个代码点,但您也可以将它写为“o”,后跟“U + 0308”(合并Diaeresis,简写的意思是最后一个字母应该有“¨”)。它们具有相同的字形,用户感知的字符。他们对于大多数目的是相同的,但有不同的表述。例如,MacOS X将所有文件名转换为使用组合Diaieresis,而大多数其他程序(包括Erlang)通过做相反的事情(例如列出目录)来尝试隐藏该文件。然而,这样做是正常化这些字符以避免混淆通常很重要。
例子列表可以做很长时间。当程序只考虑一种或两种语言时,需要一种不需要的知识。在构建通用标准时,人类语言和脚本的复杂性无疑是一个挑战。在您的程序中正确支持Unicode将需要付出努力。
3.3 什么是Unicode
Unicode是为所有已知的,活的或死的脚本定义代码点(数字)的标准。原则上,任何语言中使用的每个符号都有一个Unicode代码点。Unicode代码点由Unicode Consortium定义并发布,Unicode Consortium是一个非营利组织。
Unicode在整个计算世界中的支持越来越多,因为在全球环境中使用程序时,一个普通字符集的好处是压倒性的。除了标准的基础之外,所有脚本的代码点都有一些编码标准。
了解编码和Unicode字符之间的区别至关重要。Unicode字符是根据Unicode标准的代码点,而编码是代表这些代码点的方式。编码仅仅是表示的标准。例如,UTF-8可用于表示Unicode字符集(例如ISO-Latin-1)或完整Unicode范围的非常有限的部分。它只是一种编码格式。
只要所有字符集限制为256个字符,每个字符都可以存储在一个单独的字节中,因此对字符或多或少只有一个实际的编码。在一个字节中编码每个字符非常常见,甚至没有命名编码。Unicode系统有256个以上的字符,因此需要一种常用的方法来表示这些字符。表示代码点的常见方式是编码。这意味着对程序员来说一个全新的概念,即字符表示的概念,这在早期是一个非问题。
不同的操作系统和工具支持不同的编码。例如,Linux和MacOS X选择了UTF-8编码,后者与7位ASCII后向兼容,因此影响程序以最简单的英文写成。Windows支持UTF-16的有限版本,即所有代码平面中的字符可以存储在一个单一的16位实体中,其中包括大多数生活语言。
以下是最广泛传播的编码:
Bytewise表示
这不是一个合适的Unicode表示,而是用于Unicode标准之前的字符的表示。它仍然可以用于表示Unicode标准中字符代码点,其数字<256,与ISO Latin-1字符集完全对应。在Erlang中,这通常表示为latin1 编码,这稍微有误导性,因为ISO Latin-1是字符代码范围,而不是编码。
UTF-8
根据代码点,每个字符存储在一到四个字节中。编码向后兼容7位ASCII的字节表示,因为所有7位字符都以UTF-8格式存储在单个字节中。代码点127以外的字符存储在更多字节中,让第一个字符中的最高有效位表示多字节字符。有关编码的详细信息,RFC是公开可用的。
请注意,UTF-8 与128到255代码点的字节表示不兼容,因此ISO Latin-1按字节表示通常与UTF-8不兼容。
UTF-16
这种编码与UTF-8有许多相似之处,但基本单元是16位数字。这意味着所有字符至少占用两个字节,而某些高数字占用四个字节。某些声称使用UTF-16的程序,库和操作系统只允许可以存储在一个16位实体中的字符,这通常足以处理活动语言。由于基本单元不止一个字节,所以会出现字节顺序问题,这就是为什么UTF-16存在于大端和小端变体中的原因。
在Erlang中,适用时支持完整的UTF-16范围,如unicode 模块和位语法。
UTF-32
最直接的表示。每个字符都存储在一个32位数字中。一个角色不需要逃生或任何可变数量的实体。所有Unicode代码点可以存储在一个32位实体中。与UTF-16一样,存在字节顺序问题。UTF-32既可以是big-endian也可以是little-endian。
UCS-4
基本上与UTF-32相同,但是没有由IEEE定义的一些Unicode语义,并且几乎没有用作单独的编码标准。对于所有正常(可能异常)的使用,UTF-32和UCS-4是可以互换的。
Unicode标准中没有使用某些数字范围,某些范围甚至被认为是无效的。最显着的无效范围是16#D800-16#DFFF,因为UTF-16编码不允许对这些数字进行编码。这可能是因为UTF-16编码标准从一开始就希望能够将所有Unicode字符保存在一个16位实体中,但随后被扩展,在Unicode范围中留下了一个漏洞以处理向后兼容性。
代码点16#FEFF用于字节顺序标记(BOM),在其他上下文中不鼓励使用该字符。尽管如此,作为字符“ZWNBS”(零宽度非打破空间)是有效的。物料清单用于识别预先未知这些参数的程序的编码和字节顺序。材料清单比预期更少使用,但可以更广泛地传播,因为它们为程序提供了对某个文件的Unicode格式进行有根据的猜测的方法。
3.4 Unicode支持的领域
为了在Erlang中支持Unicode,已经解决了各个领域的问题。本节稍后会在本用户指南中简要说明每个区域。
表示
要处理Erlang中的Unicode字符,需要在列表和二进制文件中共同表示。EEP(10)和Erlang/OTP R13A中的后续初始实现在Erlang中确定了Unicode字符的标准表示形式。
操纵
Unicode字符需要Erlang程序处理,这就是为什么库函数必须能够处理它们。在某些情况下,功能已添加到已有的接口中(因为字符串模块现在可以处理任何代码点的字符串)。在某些情况下,添加了新的功能或选项(如io模块,文件处理,unicode模块和位语法)。今天,内核和STDLIB中的大多数模块以及VM都支持Unicode。
File I/O
I/O是迄今为止Unicode最成问题的领域。文件是存储字节的实体,编程知识一直是将字符和字节视为可互换的。使用Unicode字符时,如果要将数据存储在文件中,则必须决定编码。在Erlang中,你可以用一个编码选项打开一个文本文件,这样你就可以从中读取字符而不是字节,但是你也可以打开一个字节I/O的文件。
Erlang I/O系统的设计(或者至少是使用)是希望任何I/O服务器处理任何字符串数据。但是,使用Unicode字符时不再是这种情况。Erlang程序员现在必须知道数据结束时设备的功能。另外,Erlang中的端口是面向字节的,所以如果没有首先将其转换为选择的编码,则任意(Unicode)字符串都不能发送到端口。
Terminal I/O
终端I/O比文件I/O稍微简单。输出用于人类阅读,通常是Erlang语法(例如,在shell中)。存在任何Unicode字符的语法表示而不显示字形(而不是写为 \x { HHH})。因此,即使终端不支持整个Unicode范围,通常也可以显示Unicode数据。
文件名
取决于底层操作系统和文件系统,文件名可以以不同方式存储为Unicode字符串。这可以通过程序很容易地处理。当文件系统的编码不一致时出现问题。例如,Linux允许以任何字节序列命名文件,留给每个程序解释这些字节。在使用这些“透明”文件名的系统上,必须通过启动标志将Erlang通知文件名编码。默认是按字节解释,通常是错误的,但允许解释所有文件名。
如果在不是默认的平台上启用Unicode文件名转换(+ fnu),则可以使用“原始文件名”的概念来处理错误编码的文件名。
源代码编码
Erlang源代码支持UTF-8编码和字节编码。Erlang/OTP R16B的默认值是按字节(latin1)编码。它在Erlang/OTP 17.0中被更改为UTF-8。您可以通过文件开头的注释来控制编码:
%% -*- coding: utf-8 -*-
这当然也需要你的编辑器支持UTF-8。相同的注释也可以通过file:consult / 1,释放处理程序等函数来解释 ,以便您可以使用UTF-8编码在源目录中包含所有文本文件。
语言
UTF-8中的源代码还允许您编写字符串文字,函数名称和包含代码点> 255的Unicode字符的原子。模块名称,应用程序名称和节点名称仍限于ISO Latin-1范围。使用type/utf8的二进制文字 也可以使用Unicode字符> 255来表示。使用不同于7位ASCII的字符的模块名称或应用程序名称可能会导致操作系统遇到文件命名方案不一致的问题,并且可能会影响可移植性,所以不推荐。
EEP 40建议该语言也允许在变量名称中使用大于255的Unicode字符。是否实施该EEP还未确定。
3.5 标准Unicode表示
在Erlang中,字符串是整数列表。一个字符串直到Erlang/OTP R13定义为在ISO拉丁-1(ISO 8859-1)字符集中编码,即Unicode代码点的代码点,即Unicode字符集的子范围。
因此,字符串的标准列表编码很容易扩展以处理整个Unicode范围。Erlang中的一个Unicode字符串是一个包含整数的列表,其中每个整数都是有效的Unicode代码点,并且代表Unicode字符集中的一个字符。
ISO Latin-1中的Erlang字符串是Unicode字符串的子集。
只有当一个字符串包含<256的代码点时,它才可以通过使用例如erlang:iolist_to_binary / 1直接转换为二进制文件, 或者可以直接发送到端口。如果字符串包含Unicode字符> 255,则必须确定编码,并使用unicode将字符串转换为首选编码中的二进制 :characters_to_binary/1,2,3。字符串通常不是字节列表,因为它们在Erlang/OTP R13之前,它们是字符列表。字符通常不是字节,它们是Unicode代码点。
二进制文件比较麻烦。出于性能原因,程序通常将文本数据存储在二进制文件而不是列表中,主要是因为它们更紧凑(每个字符一个字节,而不是每个字符两个字,列表就是这种情况)。使用 erlang:list_to_binary/1,可以将ISO Latin-1 Erlang字符串转换为二进制文件,从而有效地使用按字节编码:每个字符一个字节。这对于那些有限的Erlang字符串很方便,但对于任意Unicode列表无法完成。
由于UTF-8编码广泛传播,并且在7位ASCII范围内提供了一些向后兼容性,因此它被选为Erlang二进制文件中Unicode字符的标准编码。
只要Erlang中的库函数要处理二进制文件中的Unicode数据,就会使用标准的二进制编码,但当进行外部通信时当然不会执行该编码。存在函数和位语法来编码和解码二进制文件中的UTF-8,UTF-16和UTF-32。但是,处理二进制文件和Unicode的库函数通常只处理默认编码。
字符数据可以从许多来源合并,有时可以混合使用字符串和二进制文件。Erlang长久以来就有了iodata或iolist的概念 ,其中二进制文件和列表可以合并为一个字节序列。同样,支持Unicode的模块通常允许二进制和列表的组合,其中二进制文件具有用UTF-8编码的字符,列表中包含表示Unicode代码点的二进制或数字:
unicode_binary() = binary() with characters encoded in UTF-8 coding standard
chardata() = charlist() | unicode_binary()
charlist() = maybe_improper_list(char() | unicode_binary() | charlist(),
unicode_binary() | nil())
unicode模块甚至支持与包含除UTF-8以外的其他编码的二进制文件类似的混合,但这是一种特殊情况,可用于转换外部数据和从外部数据转换:
external_unicode_binary() = binary() with characters coded in a user-specified
Unicode encoding other than UTF-8 (UTF-16 or UTF-32)
external_chardata() = external_charlist() | external_unicode_binary()
external_charlist() = maybe_improper_list(char() | external_unicode_binary() |
external_charlist(), external_unicode_binary() | nil())
3.6 基本语言支持
从Erlang/OTP R16开始,Erlang源文件可以用UTF-8或按字节(latin1)编码编写。有关如何声明Erlang源文件的编码的信息,请参阅epp(3)模块。从Erlang/OTP R16开始,可以使用Unicode编写字符串和注释。从Erlang/OTP 20中,也可以使用Unicode编写原子和函数。模块,应用程序和节点仍然必须使用ISO Latin-1字符集中的字符进行命名。(这些语言的限制与源文件的编码无关。)
位语法
位语法包含用于处理三种主要编码中的二进制数据的类型。这些类型被命名为utf8,utf16和utf32。在UTF16和UTF32类型可以是大端或小端的变体:
<<Ch/utf8,_/binary>> = Bin1,
<<Ch/utf16-little,_/binary>> = Bin2,
Bin3 = <<$H/utf32-little, $e/utf32-little, $l/utf32-little, $l/utf32-little,
$o/utf32-little>>,
为了方便起见,可以使用以下(或类似的)语法在二进制文件中使用Unicode编码对文字字符串进行编码:
Bin4 = <<"Hello"/utf16>>,
字符串和字符文字
对于源代码,syntax\OOO(反斜杠后跟三个八进制数)和\x HH(反斜杠后跟x,后跟两个十六进制字符)的扩展名为\x{H ...}(反斜杠后跟x,后面是左花括号,任意数量的十六进制数字,以及终止右花括号)。即使源文件的编码是按字节顺序(拉丁文1),也允许在字符串中直接输入任何代码点的字符。
在shell中,如果使用Unicode输入设备,或者以UTF-8格式存储的源代码中,$可以直接跟随一个产生整数的Unicode字符。在下面的例子中,西里尔的代码点с是输出:
7> $с.
1089
启发式字符串检测
在某些输出函数中以及在shell中返回值的输出中,Erlang会尝试检测列表和二进制文件中的字符串数据。通常情况下,您会在如下情况下看到启发式检测:
1> [97,98,99].
"abc"
2> <<97,98,99>>.
<<"abc">>
3> <<195,165,195,164,195,182>>.
<<"åäö"/utf8>>
在这里,shell以字节或UTF-8编码检测包含可打印字符或包含可打印字符的二进制文件的列表。但是什么是可打印的字符?一种观点认为,任何Unicode标准认为是可打印的,根据启发式检测也是可打印的。结果是,几乎任何整数列表都被认为是一个字符串,并且所有类型的字符都被打印出来,也可能是您的终端在其字体集中缺少的字符(导致一些未被欣赏的通用输出)。另一种方法是保持向后兼容,以便只使用ISO Latin-1字符集来检测字符串。第三种方法是让用户确定哪些Unicode范围将被视为字符。
从Erlang/OTP R16B开始,您可以分别选择启动标志+ pc latin1或 + pc unicode来选择ISO Latin-1范围或整个Unicode范围。为了向后兼容, latin1是默认的。这只能控制启发式字符串检测的完成方式。预计将来会增加更多的范围,从而可以将启发式语言与用户相关的语言和区域相适应。
以下示例显示了两个启动选项:
$ erl +pc latin1
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> [1024].
[1024]
2> [1070,1085,1080,1082,1086,1076].
[1070,1085,1080,1082,1086,1076]
3> [229,228,246].
"åäö"
4> <<208,174,208,189,208,184,208,186,208,190,208,180>>.
<<208,174,208,189,208,184,208,186,208,190,208,180>>
5> <<229/utf8,228/utf8,246/utf8>>.
<<"åäö"/utf8>>
$ 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> [1024].
"Ѐ"
2> [1070,1085,1080,1082,1086,1076].
"Юникод"
3> [229,228,246].
"åäö"
4> <<208,174,208,189,208,184,208,186,208,190,208,180>>.
<<"Юникод"/utf8>>
5> <<229/utf8,228/utf8,246/utf8>>.
<<"åäö"/utf8>>
在这些示例中,您可以看到默认的Erlang shell仅将ISO Latin1范围内的字符解释为可打印,并且仅检测包含字符串数据的那些“可打印”字符的列表或二进制文件。包含俄语单词“Юникод”的有效UTF-8二进制文件不会打印为字符串。当使用所有可打印的Unicode字符(+ pc unicode)启动时,shell将任何包含可打印的Unicode数据(以二进制文件形式,以UTF-8或按字节编码)输出为字符串数据。
这些启发式也被io:format/2,io_lib:format/2使用,也被当modifier t用〜p或 〜p时的friends使用:
$ erl +pc latin1
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> io:format("~tp~n",[{<<"åäö">>, <<"åäö"/utf8>>, <<208,174,208,189,208,184,208,186,208,190,208,180>>}]).
{<<"åäö">>,<<"åäö"/utf8>>,<<208,174,208,189,208,184,208,186,208,190,208,180>>}
ok
$ 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:format("~tp~n",[{<<"åäö">>, <<"åäö"/utf8>>, <<208,174,208,189,208,184,208,186,208,190,208,180>>}]).
{<<"åäö">>,<<"åäö"/utf8>>,<<"Юникод"/utf8>>}
ok
请注意,这仅影响输出上列表和二进制文件的启发式解释。例如,〜ts格式序列总是输出一个有效的字符列表,而不管 + pc设置如何,因为程序员明确要求字符串输出。
3.7 交互式Shell
交互式Erlang shell在启动到终端或开始在Windows上使用命令werl时,可以支持Unicode输入和输出。
在Windows上,正确的操作需要安装并选择合适的字体供Erlang应用程序使用。如果系统上没有合适的字体,请尝试安装免费提供的 DejaVu字体,然后在Erlang shell应用程序中选择该字体。
在类Unix操作系统上,终端将能够处理输入和输出的UTF-8(例如,通过现代版本的XTerm,KDE Konsole和Gnome终端完成),并且您的区域设置必须是正确。作为一个例子,一个LANG环境变量可以设置如下:
$ echo $LANG
en_US.UTF-8
大多数系统在LANG之前处理变量LC_CTYPE,所以如果设置了它,它必须设置为UTF-8:
$ echo $LC_CTYPE
en_US.UTF-8
LANG或LC_CTYPE设置要与什么终端能够保持一致。Erlang没有可移植的方式向终端询问它的UTF-8容量,我们必须依赖语言和字符类型设置。
为了调查Erlang对终端的看法, 可以在shell启动时使用call io:getopts():
$ LC_CTYPE=en_US.ISO-8859-1 erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,latin1}
2> q().
ok
$ LC_CTYPE=en_US.UTF-8 erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,unicode}
2>
当(最后?)一切都按照语言环境设置,字体顺序排列。和终端模拟器,你可能已经找到了一种方法来输入你想要的脚本中的字符。对于测试,最简单的方法是为其他语言添加一些键盘映射,通常在桌面环境中使用一些小程序完成。
在KDE环境中,选择KDE控制中心(个人设置) > 区域和辅助功能 > 键盘布局。
在Windows XP中,选择控制面板 > 区域和语言选项,选择标签语言,并单击按钮 详细...在广场命名为文字服务和输入语言。
您的环境可能提供了更改键盘布局的类似方法。如果您不习惯这样做,请确保您有方法在键盘之间来回切换。例如,使用西里尔文字符集输入命令不容易在Erlang shell中完成。
现在您已经设置了一些Unicode输入和输出。最简单的做法是在shell中输入一个字符串:
$ erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,unicode}
2> "Юникод".
"Юникод"
3> io:format("~ts~n", [v(2)]).
Юникод
ok
4>
虽然字符串可以作为Unicode字符输入,但语言元素仍然局限于ISO Latin-1字符集。只允许字符常量和字符串超出该范围:
$ erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.10.1 (abort with ^G)
1> $ξ.
958
2> Юникод.
* 1: illegal character
2>
3.8 Unicode文件名
大多数现代操作系统都以某种方式支持Unicode文件名。有很多不同的方法可以做到这一点,Erlang默认以不同的方式处理不同的方法:
强制性的Unicode文件命名
Windows,并且对于大多数常见用途,MacOS X强制Unicode支持文件名。在文件系统中创建的所有文件都具有可以一致解释的名称。在MacOS X中,所有文件名都以UTF-8编码检索。在Windows中,每个系统调用处理文件名都有一个特殊的可识别Unicode的变体,它们具有相同的效果。这些系统上没有不是Unicode文件名的文件名。因此,Erlang VM的默认行为是以“Unicode文件名翻译模式”工作。这意味着可以将文件名指定为Unicode列表,该列表会自动转换为底层操作系统和文件系统的专有名称编码。
例如, 在其中一个系统上执行 file:list_dir/1操作可以返回code points>255的Unicode列表,具体取决于文件系统的内容。
透明的文件命名
大多数Unix操作系统采用了一种更简单的方法,即Unicode文件命名不是强制执行的,而是按照惯例。这些系统通常对Unicode文件名使用UTF-8编码,但不强制执行。在这样的系统中,包含代码点从128到255的字符的文件名可以命名为普通ISO Latin-1或使用UTF-8编码。由于没有执行一致性,Erlang虚拟机无法对所有文件名进行一致的转换。
默认情况下,如果终端支持UTF-8,则Erlang以utf8文件名模式启动,否则以latin1 模式启动。
在latin1模式下,文件名按字节编码。这允许在系统中列出所有文件名的表示。但是,名为“Östersund.txt”的file:list_dir/1中出现为“Östersund.txt”(如果文件名是由创建文件的程序按字节顺序ISO Latin-1编码的),或者更可能是 [195,150,115,116,101,114,115,117,110,100 ],这是一个包含UTF-8字节的列表(不是你想要的)。如果您在这样的系统上使用Unicode文件名转换,非文件名的UTF-8将被诸如file:list_dir/1之类的函数忽略。它们可以通过函数文件:list_dir_all/1进行检索 ,但错误编码的文件名显示为“原始文件名”。
Unicode文件命名支持在Erlang/OTP R14B01中引入。以Unicode文件名翻译模式运行的虚拟机可以处理任何语言或字符集名称的文件(只要它受底层操作系统和文件系统支持)。Unicode字符列表用于表示文件名或目录名称。如果列出文件系统内容,则还会将Unicode列表作为返回值。支持位于内核和STDLIB模块中,这就是为什么大多数应用程序(不明确要求文件名位于ISO Latin-1范围内)从Unicode支持中受益而不用更改。
在具有强制Unicode文件名的操作系统上,这意味着您更容易符合其他(非Erlang)应用程序的文件名。您也可以处理至少在Windows上无法访问的文件名(因为名称不能用ISO Latin-1表示)。此外,您可以避免在MacOS X上创建难以理解的文件名,因为操作系统的vfs层接受所有文件名,因为UTF-8不会重写它们。
对于大多数系统,即使使用透明文件命名,开启Unicode文件名转换也不成问题。很少有系统具有混合文件名编码。一致的UTF-8命名系统在Unicode文件名模式下完美工作。然而,在Erlang / OTP R14B01中它仍然被认为是实验性的,并且在这样的系统中仍然不是默认的。
Unicode文件名翻译通过switch + fnu打开。在Linux上,启动虚拟机时未明确说明文件名转换模式默认为latin1作为本地文件名编码。在Windows和MacOS X上,默认行为是Unicode文件名转换的行为。因此 ,默认情况下,file:native_name_encoding/0会在这些系统上返回utf8(Windows在文件系统级别上不使用UTF-8,但Erlang程序员可以安全地忽略它)。默认行为,如前所述,使用,变更选项+fnu或+fnl解放力量的VM,看到erl程序。如果VM以Unicode文件名转换模式启动,则 file:native_name_encoding/0将返回原子utf8。Switch+ fnu后面跟着w,i或e来控制如何报告错误编码的文件名。
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_lib
和string
有时需要转换或扩展才能处理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/1
和string: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数据输出到encoding
除latin1
(按字节编码)之外的文件,请使用该模块。例如,file:open(Name,[read,{encoding,utf8}])
使用打开的文件无法正常读取file:read(File,N)
,但使用io
模块从其中检索Unicode数据,这有点令人困惑。其原因是,file:read
和file: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
,返回unicode
或latin1
。为了与未来(预期的)设置扩展兼容,而是根据设置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
根据环境变量LANG
和LC_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与-oldshell
or 启动时-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!
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com