这里有一个Perl脚本,我期望打印时found
执行时:
#!/usr/bin/perl use warnings; use strict; use utf8; use Encode; use constant filename => 'Bärlauch'; open (my $out, '>', filename) or die; close $out; opendir(my $dir, '.') or die; while (my $filename_read = readdir($dir)) { # $filename_read = encode('utf8', $filename_read); print "found\n" if $filename_read eq filename; }
该脚本首先创build一个名称为常量filename
。 (在运行脚本之后,我可以使用ls
来validation文件的存在,并且文件不是用“funny”字符创build的。)
然后,脚本遍历当前工作目录中的文件,如果有名称与刚刚创build的文件相同的文件,则打印该文件。 这显然应该是这样的。
但是,它不(Ubuntu,bash, LANG=en_US.UTF8
)
如果我将这个常量改为Barlauch
,它就会按预期工作,并found
打印件。
取消注释$filename_read = encode('utf8', $filename_read);
不会改变行为。
有没有解释这个,我该怎么做才能识别Umlaute的文件名呢?
这个问题(正如我解释的那样)是:
为什么不
readdir
返回新创建的文件名? (这里,由设置为Bärlauch
的变量filename
表示)。
(注意: filename
是一个Perl常量变量,所以这就是为什么它在前面缺少$
sigil。)
背景:
首先注意:由于在程序开始时use utf8
语句,所以在编译时filename
名将被升级为Unicode字符串,因为它包含非ASCII字符。 从utf8编译指南的文档:
启用utf8编译指示具有以下效果:源文本中不在ASCII字符集中的字节将被视为文字UTF-8序列的一部分。 这包括大多数文字,例如标识符名称,字符串常量和常量正则表达式模式。
另外,根据perluniintro部分“Perl的Unicode模型” :
一般的原则是,Perl试图尽可能长时间保持数据为八位字节,但是一旦Unicode变得无法避免,数据就会被透明地升级到Unicode。
…
在内部,Perl当前使用平台的本地八位字符集(例如Latin-1)(默认为UTF-8)编码Unicode字符串。
filename
的非ASCII字符是字母ä
。 如果使用ISO 8859-1扩展ASCII编码(Latin-1),则编码为字节值0xE4
,请参阅0xE4
此表 。 但是,如果从filename
删除了ä
字符,则它将只包含ASCII字符,因此即使使用了utf8
编译指示,也不会将其内部升级为Unicode。
所以filename
现在是一个Unicode字符串,内部设置了UTF-8
标志(有关UTF-8
标志的更多信息,请参见utf8编译指示)。 请注意,字母ä
以UTF-8编码为两个字节0xC3 0xA4
。
编写文件:
在编写文件时,文件名会发生什么? 如果filename
是一个Unicode字符串,它将被编码为UTF-8。 但是请注意,不需要先编码filename
( encode_utf8( filename )
)。 有关更多信息,请参阅使用unicode字符创建文件名 。 所以文件名以UTF-8编码的字节写入磁盘。
读回文件名:
当尝试从磁盘读取文件名时,即使文件名包含以UTF-8编码的字节, readdir
也不会返回Unicode字符串(即设置了UTF-8标志的字符串)。 它返回二进制或字节字符串,参见perlunitut讨论字节串vs字符(Unicode)字符串。
为什么不readdir
返回Unicode字符串? 首先,根据perlunicode节“当Unicode不发生时” :
还有很多地方可以将Unicode(在一些编码或其他编码中)作为参数给出,或者以Perl的形式作为结果接收,或者两者都不是。 (……)
以下是这样的接口。 对于所有这些接口,Perl当前(从v5.16.0开始)只是假定字节串作为参数和结果。 (……)
Perl在这些情况下不试图解决Unicode角色的一个原因是答案高度依赖于操作系统和文件系统。 例如,文件名是否可以使用Unicode,以及使用何种编码,并不是一个便携式的概念。 (……)
- chdir,chmod,chown,chroot,exec,link,lstat,mkdir,rename,rmdir, – stat,符号链接,truncate,unlink,utime,-X
- %ENV
- glob(又名<*>)
- 打开,opendir,sysopen
- qx(又名反向运营商),系统
- readdir,readlink
所以readdir
返回字节字符串,因为通常不可能事先知道文件名的编码。 有关为什么这是不可能的背景信息,请参阅例如:
字符串比较:
现在,最后你试着比较读取文件名$filename_read
和变量filename
:
print "found\n" if $filename_read eq filename;
在这种情况下, $filename_read
和filename
之间的唯一区别是$filename_read
没有设置UTF-8标志(这不是Perl内部认为是“Unicode字符串”的东西 )。
现在有趣的是, eq
运算符的结果将取决于$filename_read
的字节是否是纯ASCII。 根据编码模块的文档:
在Perl中引入Unicode支持之前,
eq
运算符只是比较两个标量所代表的字符串。 从Perl 5.8开始,eq
比较了两个字符串,同时考虑了UTF8标志。…
当你解码时,UTF8的标志是打开的 – 除非你能明确地表示数据。
所以在你的情况下, eq
将考虑UTF-8
标志,因为$file_name_read
不包含纯ASCII,因此它会考虑这两个字符串不相等。 如果$filename_read
和filename
在相同的地方并且只包含纯ASCII字节(并且filename
还设置了UTF-8标志, $filename_read
没有设置UTF-8标志),那么eq
会认为这两个字符串是相等的。 请参阅文档中的讨论以编码有关此行为背景的更多信息。
结论:
因此,如果您确信所有的文件名都是UTF-8编码的,您可以通过将从readdir
返回的字符串解码为Unicode字符串(强制设置UTF-8标志)来解决问题。
$filename_read = Encode::decode_utf8( $filename_read );
更多细节
注意:因为Unicode允许多个表示相同的字符,所以在Bärlauch
存在两种形式的ä
(拉丁语小写字母A组合)。 例如,
在我的平台(Linux)上,UTF-8编码的文件名使用NFC形式存储,但是在Mac OS上它们使用NFD形式。 有关更多信息,请参阅Encode::UTF8Mac
。 这意味着如果您在Linux机器上工作,例如克隆由Mac用户创建的Git存储库,则可以轻松地在您的Linux机器上获得NFD编码的文件名。 所以Linux文件系统并不关心编码文件的编码方式, 它只是把它看作一个字节序列。 因此,即使我的语言环境是"en_US.UTF-8"
,我也可以轻松地编写一个创建ISO-Latin-1编码文件名的脚本。 当前的语言环境设置只是应用程序的指导原则,但是如果应用程序忽略语言环境设置,则不会阻止他们这样做。
所以如果你不确定从readdir
返回的文件名是否使用NFC或NFD,你应该在解码完成后分解:
use Unicode::Normalize; print "found\n" if NFD( $filename_read ) eq NFD( filename );
另请参阅Perl Unicode Cookbook部分“始终分解和重构”。
最后,为了更多地了解Locale如何在Perl中与Unicode一起工作,你可以看看: