perlunicook - Perl 中处理 Unicode 的食谱示例
此手册页包含简短的食谱,演示如何在 Perl 中处理常见的 Unicode 操作,以及结尾处的一个完整程序。假设各个食谱中任何未声明的变量都已在其中具有先前的适当值。
除非另有说明,否则以下所有示例都需要此标准前导才能正常工作,其中 #!
已调整为在您的系统上工作
#!/usr/bin/env perl
use v5.36; # or later to get "unicode_strings" feature,
# plus strict, warnings
use utf8; # so literals and identifiers can be in UTF-8
use warnings qw(FATAL utf8); # fatalize encoding glitches
use open qw(:std :encoding(UTF-8)); # undeclared streams in UTF-8
use charnames qw(:full :short); # unneeded in v5.16
这确实让 Unix 程序员也对二进制流使用 binmode
,或使用 :raw
打开它们,但无论如何,这是获得它们的唯一便携方式。
警告:use autodie
(2.26 之前)和 use open
彼此不兼容。
始终在输入时分解,然后在输出时重新组合。
use Unicode::Normalize;
while (<>) {
$_ = NFD($_); # decompose + reorder canonically
...
} continue {
print NFC($_); # recompose (where possible) + reorder canonically
}
从 v5.14 开始,Perl 区分 UTF-8 警告的三个子类。
use v5.14; # subwarnings unavailable any earlier
no warnings "nonchar"; # the 66 forbidden non-characters
no warnings "surrogate"; # UTF-16/CESU-8 nonsense
no warnings "non_unicode"; # for codepoints over 0x10_FFFF
如果没有至关重要的 use utf8
声明,在字面量和标识符中放置 UTF-8 将无法正常工作。如果您使用了上面给出的标准前导,则此操作已完成。如果您这样做了,则可以执行以下操作
use utf8;
my $measure = "Ångström";
my @μsoft = qw( cp852 cp1251 cp1252 );
my @ὑπέρμεγας = qw( ὑπέρ μεγας );
my @鯉 = qw( koi8-f koi8-u koi8-r );
my $motto = "👪 💗 🐪"; # FAMILY, GROWING HEART, DROMEDARY CAMEL
如果您忘记use utf8
,高字节将被误认为是单独的字符,并且没有任何内容能够正常工作。
ord
和chr
函数对所有代码点透明地工作,而不仅仅是 ASCII,实际上,甚至不仅仅是 Unicode。
# ASCII characters
ord("A")
chr(65)
# characters from the Basic Multilingual Plane
ord("Σ")
chr(0x3A3)
# beyond the BMP
ord("𝑛") # MATHEMATICAL ITALIC SMALL N
chr(0x1D45B)
# beyond Unicode! (up to MAXINT)
ord("\x{20_0000}")
chr(0x20_0000)
在内插文字中,无论是双引号字符串还是正则表达式,您都可以使用\x{HHHHHH}
转义字符通过其数字指定字符。
String: "\x{3a3}"
Regex: /\x{3a3}/
String: "\x{1d45b}"
Regex: /\x{1d45b}/
# even non-BMP ranges in regex work fine
/[\x{1D434}-\x{1D467}]/
use charnames ();
my $name = charnames::viacode(0x03A3);
use charnames ();
my $number = charnames::vianame("GREEK CAPITAL LETTER SIGMA");
使用\N{charname}
表示法,通过该名称获取字符,以便在内插文字(双引号字符串和正则表达式)中使用。在 v5.16 中,有一个隐式的
use charnames qw(:full :short);
但在 v5.16 之前,您必须明确指定您想要哪一组字符名称。:full
名称是官方 Unicode 字符名称、别名或序列,它们都共享一个名称空间。
use charnames qw(:full :short latin greek);
"\N{MATHEMATICAL ITALIC SMALL N}" # :full
"\N{GREEK CAPITAL LETTER SIGMA}" # :full
其他任何内容都是 Perl 特有的便捷缩写。如果您想要特定于脚本的短名称,请按名称指定一个或多个脚本。
"\N{Greek:Sigma}" # :short
"\N{ae}" # latin
"\N{epsilon}" # greek
v5.16 版本还支持:loose
导入,用于对字符名称进行松散匹配,其工作方式与对属性名称进行松散匹配的方式相同:即,它忽略大小写、空格和下划线
"\N{euro sign}" # :loose (from v5.16)
从 v5.32 开始,您还可以使用
qr/\p{name=euro sign}/
在正则表达式中获取官方 Unicode 命名字符。始终对这些进行松散匹配。
这些看起来就像字符名称,但返回多个代码点。请注意printf
中的%vx
向量打印功能。
use charnames qw(:full);
my $seq = "\N{LATIN CAPITAL LETTER A WITH MACRON AND GRAVE}";
printf "U+%v04X\n", $seq;
U+0100.0300
使用:alias
为现有字符提供您自己的词法作用域昵称,甚至为未命名的专用字符提供有用的名称。
use charnames ":full", ":alias" => {
ecute => "LATIN SMALL LETTER E WITH ACUTE",
"APPLE LOGO" => 0xF8FF, # private use character
};
"\N{ecute}"
"\N{APPLE LOGO}"
像“東京”这样的汉字会返回CJK UNIFIED IDEOGRAPH-6771
和CJK UNIFIED IDEOGRAPH-4EAC
的字符名称,因为它们的“名称”各不相同。CPAN Unicode::Unihan
模块有一个大型数据库,用于对这些字符进行解码(以及更多内容),前提是您知道如何理解其输出。
# cpan -i Unicode::Unihan
use Unicode::Unihan;
my $str = "東京";
my $unhan = Unicode::Unihan->new;
for my $lang (qw(Mandarin Cantonese Korean JapaneseOn JapaneseKun)) {
printf "CJK $str in %-12s is ", $lang;
say $unhan->$lang($str);
}
打印
CJK 東京 in Mandarin is DONG1JING1
CJK 東京 in Cantonese is dung1ging1
CJK 東京 in Korean is TONGKYENG
CJK 東京 in JapaneseOn is TOUKYOU KEI KIN
CJK 東京 in JapaneseKun is HIGASHI AZUMAMIYAKO
如果您心中有一个特定的罗马化方案,请使用特定模块
# cpan -i Lingua::JA::Romanize::Japanese
use Lingua::JA::Romanize::Japanese;
my $k2r = Lingua::JA::Romanize::Japanese->new;
my $str = "東京";
say "Japanese for $str is ", $k2r->chars($str);
打印
Japanese for 東京 is toukyou
在罕见的情况下,例如数据库读取,您可能会收到需要解码的编码文本。
use Encode qw(encode decode);
my $chars = decode("shiftjis", $bytes, 1);
# OR
my $bytes = encode("MIME-Header-ISO_2022_JP", $chars, 1);
对于所有编码都相同的流,不要使用编码/解码;相反,在打开文件时或在稍后使用 binmode
(如下所述)设置文件编码。
$ perl -CA ...
or
$ export PERL_UNICODE=A
or
use Encode qw(decode);
@ARGV = map { decode('UTF-8', $_, 1) } @ARGV;
# cpan -i Encode::Locale
use Encode qw(locale);
use Encode::Locale;
# use "locale" as an arg to encode/decode
@ARGV = map { decode(locale => $_, 1) } @ARGV;
使用命令行选项、环境变量,或显式调用 binmode
$ perl -CS ...
or
$ export PERL_UNICODE=S
or
use open qw(:std :encoding(UTF-8));
or
binmode(STDIN, ":encoding(UTF-8)");
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
# cpan -i Encode::Locale
use Encode;
use Encode::Locale;
# or as a stream for binmode or open
binmode STDIN, ":encoding(console_in)" if -t STDIN;
binmode STDOUT, ":encoding(console_out)" if -t STDOUT;
binmode STDERR, ":encoding(console_out)" if -t STDERR;
在没有编码参数的情况下打开的文件将采用 UTF-8
$ perl -CD ...
or
$ export PERL_UNICODE=D
or
use open qw(:encoding(UTF-8));
$ perl -CSDA ...
or
$ export PERL_UNICODE=SDA
or
use open qw(:std :encoding(UTF-8));
use Encode qw(decode);
@ARGV = map { decode('UTF-8', $_, 1) } @ARGV;
指定流编码。这是处理编码文本的正常方式,而不是调用低级函数。
# input file
open(my $in_file, "< :encoding(UTF-16)", "wintext");
OR
open(my $in_file, "<", "wintext");
binmode($in_file, ":encoding(UTF-16)");
THEN
my $line = <$in_file>;
# output file
open($out_file, "> :encoding(cp1252)", "wintext");
OR
open(my $out_file, ">", "wintext");
binmode($out_file, ":encoding(cp1252)");
THEN
print $out_file "some text\n";
除了编码之外,还可以指定更多的层。例如,咒语 ":raw :encoding(UTF-16LE) :crlf"
包括隐式 CRLF 处理。
Unicode 大小写与 ASCII 大小写有很大不同。
uc("henry ⅷ") # "HENRY Ⅷ"
uc("tschüß") # "TSCHÜSS" notice ß => SS
# both are true:
"tschüß" =~ /TSCHÜSS/i # notice ß => SS
"Σίσυφος" =~ /ΣΊΣΥΦΟΣ/i # notice Σ,σ,ς sameness
在 CPAN Unicode::CaseFold 模块中也可用,v5.16 中新的 fc
“foldcase” 函数授予与 /i
模式修饰符始终使用的相同的 Unicode 折叠大小写功能
use feature "fc"; # fc() function is from v5.16
# sort case-insensitively
my @sorted = sort { fc($a) cmp fc($b) } @list;
# both are true:
fc("tschüß") eq fc("TSCHÜSS")
fc("Σίσυφος") eq fc("ΣΊΣΥΦΟΣ")
Unicode 换行符匹配双字符 CRLF 分形图素或任何七个垂直空白字符。适用于处理来自不同操作系统的文本文件。
\R
s/\R/\n/g; # normalize all linebreaks to \n
查找数字代码点的通用类别。
use Unicode::UCD qw(charinfo);
my $cat = charinfo(0x3A3)->{category}; # "Lu"
禁用 \w
、\b
、\s
、\d
以及 POSIX 类在 Unicode 上正常工作,无论是在此范围内还是仅在一个正则表达式中。
use v5.14;
use re "/a";
# OR
my($num) = $str =~ /(\d+)/a;
或者使用特定的非 Unicode 属性,如 \p{ahex}
和 \p{POSIX_Digit
}。无论使用哪种字符集修饰符(/d /u /l /a /aa
),属性仍正常工作。
这些都匹配具有给定属性的单个代码点。使用 \P
代替 \p
来匹配缺少该属性的代码点。
\pL, \pN, \pS, \pP, \pM, \pZ, \pC
\p{Sk}, \p{Ps}, \p{Lt}
\p{alpha}, \p{upper}, \p{lower}
\p{Latin}, \p{Greek}
\p{script_extensions=Latin}, \p{scx=Greek}
\p{East_Asian_Width=Wide}, \p{EA=W}
\p{Line_Break=Hyphen}, \p{LB=HY}
\p{Numeric_Value=4}, \p{NV=4}
在编译时定义你自己的自定义字符属性以用于正则表达式。
# using private-use characters
sub In_Tengwar { "E000\tE07F\n" }
if (/\p{In_Tengwar}/) { ... }
# blending existing properties
sub Is_GraecoRoman_Title {<<'END_OF_SET'}
+utf8::IsLatin
+utf8::IsGreek
&utf8::IsTitle
END_OF_SET
if (/\p{Is_GraecoRoman_Title}/ { ... }
通常在输入时呈现为 NFD,在输出时呈现为 NFC。使用 NFKC 或 NFKD 函数可提高搜索召回率,假设你已经对要搜索的文本执行了相同的操作。请注意,这不仅仅是预先组合的兼容性字形;它还根据规范组合类重新排列标记,并清除单例。
use Unicode::Normalize;
my $nfd = NFD($orig);
my $nfc = NFC($orig);
my $nfkd = NFKD($orig);
my $nfkc = NFKC($orig);
除非你使用了 /a
或 /aa
,否则 \d
匹配的不仅仅是 ASCII 数字,但 Perl 的隐式字符串到数字转换当前无法识别这些数字。以下是如何手动转换此类字符串。
use v5.14; # needed for num() function
use Unicode::UCD qw(num);
my $str = "got Ⅻ and ४५६७ and ⅞ and here";
my @nums = ();
while ($str =~ /(\d+|\N)/g) { # not just ASCII!
push @nums, num($1);
}
say "@nums"; # 12 4567 0.875
use charnames qw(:full);
my $nv = num("\N{RUMI DIGIT ONE}\N{RUMI DIGIT TWO}");
程序员可见的“字符”是 /./s
匹配的代码点,但用户可见的“字符”是 /\X/
匹配的音节。
# Find vowel *plus* any combining diacritics,underlining,etc.
my $nfd = NFD($orig);
$nfd =~ / (?=[aeiou]) \X /xi
# match and grab five first graphemes
my($first_five) = $str =~ /^ ( \X{5} ) /x;
# cpan -i Unicode::GCString
use Unicode::GCString;
my $gcs = Unicode::GCString->new($str);
my $first_five = $gcs->substr(0, 5);
按代码点反转会搞乱变音符号,错误地将 crème brûlée
转换为 éel̂urb em̀erc
而不是 eélûrb emèrc
;因此,按音节反转。无论字符串处于何种规范化状态,这两种方法都可以正常工作
$str = join("", reverse $str =~ /\X/g);
# OR: cpan -i Unicode::GCString
use Unicode::GCString;
$str = reverse Unicode::GCString->new($str);
字符串 brûlée
有六个音节,但最多有八个代码点。这是按音节计数,而不是按代码点计数
my $str = "brûlée";
my $count = 0;
while ($str =~ /\X/g) { $count++ }
# OR: cpan -i Unicode::GCString
use Unicode::GCString;
my $gcs = Unicode::GCString->new($str);
my $count = $gcs->length;
Perl 的 printf
、sprintf
和 format
认为所有代码点都占用 1 个打印列,但许多代码点占用 0 或 2 个。这里为了表明规范化没有区别,我们打印出两种形式
use Unicode::GCString;
use Unicode::Normalize;
my @words = qw/crème brûlée/;
@words = map { NFC($_), NFD($_) } @words;
for my $str (@words) {
my $gcs = Unicode::GCString->new($str);
my $cols = $gcs->columns;
my $pad = " " x (10 - $cols);
say str, $pad, " |";
}
生成此内容以表明无论规范化如何,它都能正确填充
crème |
crème |
brûlée |
brûlée |
按数字代码点排序的文本不遵循合理的字母顺序;使用 UCA 对文本进行排序。
use Unicode::Collate;
my $col = Unicode::Collate->new();
my @list = $col->sort(@old_list);
请参阅 Unicode::Tussle CPAN 模块中的 ucsort 程序,以获取此模块的便捷命令行界面。
指定强度为 1 级,以忽略大小写和变音符号,仅查看基本字符。
use Unicode::Collate;
my $col = Unicode::Collate->new(level => 1);
my @list = $col->sort(@old_list);
一些语言环境有特殊的排序规则。
# either use v5.12, OR: cpan -i Unicode::Collate::Locale
use Unicode::Collate::Locale;
my $col = Unicode::Collate::Locale->new(locale => "de__phonebook");
my @list = $col->sort(@old_list);
上面提到的 ucsort 程序接受一个 --locale
参数。
cmp
对文本进行操作,而不是代码点代替此方法
@srecs = sort {
$b->{AGE} <=> $a->{AGE}
||
$a->{NAME} cmp $b->{NAME}
} @recs;
使用此方法
my $coll = Unicode::Collate->new();
for my $rec (@recs) {
$rec->{NAME_key} = $coll->getSortKey( $rec->{NAME} );
}
@srecs = sort {
$b->{AGE} <=> $a->{AGE}
||
$a->{NAME_key} cmp $b->{NAME_key}
} @recs;
使用排序器对象按字符而不是代码点比较 Unicode 文本。
use Unicode::Collate;
my $es = Unicode::Collate->new(
level => 1,
normalization => undef
);
# now both are true:
$es->eq("García", "GARCIA" );
$es->eq("Márquez", "MARQUEZ");
相同,但在特定区域设置中。
my $de = Unicode::Collate::Locale->new(
locale => "de__phonebook",
);
# now this is true:
$de->eq("tschüß", "TSCHUESS"); # notice ü => UE, ß => SS
根据 Unicode 规则将文本分成多行。
# cpan -i Unicode::LineBreak
use Unicode::LineBreak;
use charnames qw(:full);
my $para = "This is a super\N{HYPHEN}long string. " x 20;
my $fmt = Unicode::LineBreak->new;
print $fmt->break($para), "\n";
如果任何代码点不适合一个字节,则使用常规 Perl 字符串作为 DBM 哈希的键或值将触发宽字符异常。以下是手动管理转换的方法
use DB_File;
use Encode qw(encode decode);
tie %dbhash, "DB_File", "pathname";
# STORE
# assume $uni_key and $uni_value are abstract Unicode strings
my $enc_key = encode("UTF-8", $uni_key, 1);
my $enc_value = encode("UTF-8", $uni_value, 1);
$dbhash{$enc_key} = $enc_value;
# FETCH
# assume $uni_key holds a normal Perl string (abstract Unicode)
my $enc_key = encode("UTF-8", $uni_key, 1);
my $enc_value = $dbhash{$enc_key};
my $uni_value = decode("UTF-8", $enc_value, 1);
以下是隐式管理转换的方法;所有编码和解码都是自动完成的,就像附加了特定编码的流一样
use DB_File;
use DBM_Filter;
my $dbobj = tie %dbhash, "DB_File", "pathname";
$dbobj->Filter_Value("utf8"); # this is the magic bit
# STORE
# assume $uni_key and $uni_value are abstract Unicode strings
$dbhash{$uni_key} = $uni_value;
# FETCH
# $uni_key holds a normal Perl string (abstract Unicode)
my $uni_value = $dbhash{$uni_key};
这是一个完整的程序,展示了如何利用区分区域设置的排序、Unicode 大小写以及管理打印宽度(当某些字符占用零或两列,而每次不只占用一列时)。运行后,以下程序将生成此对齐良好的输出
Crème Brûlée....... €2.00
Éclair............. €1.60
Fideuà............. €4.20
Hamburger.......... €6.00
Jamón Serrano...... €4.45
Linguiça........... €7.00
Pâté............... €4.15
Pears.............. €2.00
Pêches............. €2.25
Smørbrød........... €5.75
Spätzle............ €5.50
Xoriço............. €3.00
Γύρος.............. €6.50
막걸리............. €4.00
おもち............. €2.65
お好み焼き......... €8.00
シュークリーム..... €1.85
寿司............... €9.99
包子............... €7.50
以下是该程序。
#!/usr/bin/env perl
# umenu - demo sorting and printing of Unicode food
#
# (obligatory and increasingly long preamble)
#
use v5.36;
use utf8;
use warnings qw(FATAL utf8); # fatalize encoding faults
use open qw(:std :encoding(UTF-8)); # undeclared streams in UTF-8
use charnames qw(:full :short); # unneeded in v5.16
# std modules
use Unicode::Normalize; # std perl distro as of v5.8
use List::Util qw(max); # std perl distro as of v5.10
use Unicode::Collate::Locale; # std perl distro as of v5.14
# cpan modules
use Unicode::GCString; # from CPAN
my %price = (
"γύρος" => 6.50, # gyros
"pears" => 2.00, # like um, pears
"linguiça" => 7.00, # spicy sausage, Portuguese
"xoriço" => 3.00, # chorizo sausage, Catalan
"hamburger" => 6.00, # burgermeister meisterburger
"éclair" => 1.60, # dessert, French
"smørbrød" => 5.75, # sandwiches, Norwegian
"spätzle" => 5.50, # Bayerisch noodles, little sparrows
"包子" => 7.50, # bao1 zi5, steamed pork buns, Mandarin
"jamón serrano" => 4.45, # country ham, Spanish
"pêches" => 2.25, # peaches, French
"シュークリーム" => 1.85, # cream-filled pastry like eclair
"막걸리" => 4.00, # makgeolli, Korean rice wine
"寿司" => 9.99, # sushi, Japanese
"おもち" => 2.65, # omochi, rice cakes, Japanese
"crème brûlée" => 2.00, # crema catalana
"fideuà" => 4.20, # more noodles, Valencian
# (Catalan=fideuada)
"pâté" => 4.15, # gooseliver paste, French
"お好み焼き" => 8.00, # okonomiyaki, Japanese
);
my $width = 5 + max map { colwidth($_) } keys %price;
# So the Asian stuff comes out in an order that someone
# who reads those scripts won't freak out over; the
# CJK stuff will be in JIS X 0208 order that way.
my $coll = Unicode::Collate::Locale->new(locale => "ja");
for my $item ($coll->sort(keys %price)) {
print pad(entitle($item), $width, ".");
printf " €%.2f\n", $price{$item};
}
sub pad ($str, $width, $padchar) {
return $str . ($padchar x ($width - colwidth($str)));
}
sub colwidth ($str) {
return Unicode::GCString->new($str)->columns;
}
sub entitle ($str) {
$str =~ s{ (?=\pL)(\S) (\S*) }
{ ucfirst($1) . lc($2) }xge;
return $str;
}
请参阅以下手册页,其中一些是 CPAN 模块:perlunicode、perluniprops、perlre、perlrecharclass、perluniintro、perlunitut、perlunifaq、PerlIO、DB_File、DBM_Filter、DBM_Filter::utf8、Encode、Encode::Locale、Unicode::UCD、Unicode::Normalize、Unicode::GCString、Unicode::LineBreak、Unicode::Collate、Unicode::Collate::Locale、Unicode::Unihan、Unicode::CaseFold、Unicode::Tussle、Lingua::JA::Romanize::Japanese、Lingua::ZH::Romanize::Pinyin、Lingua::KO::Romanize::Hangul。
Unicode::Tussle CPAN 模块包含许多程序来帮助处理 Unicode,包括以下程序,用于全部或部分替换标准实用程序:使用 tcgrep 替换 egrep,使用 uniquote 替换 cat -v 或 hexdump,使用 uniwc 替换 wc,使用 unilook 替换 look,使用 unifmt 替换 fmt,使用 ucsort 替换 sort。要了解 Unicode 字符名称和字符属性,请参阅其 uniprops、unichars 和 uninames 程序。它还提供以下程序,它们都是执行 Unicode 操作的通用过滤器:unititle 和 unicaps;uniwide 和 uninarrow;unisupers 和 unisubs;nfd、nfc、nfkd 和 nfkc;以及 uc、lc 和 tc。
最后,请参阅已发布的 Unicode 标准(页码来自版本 6.0.0),包括以下特定附件和技术报告
Tom Christiansen <[email protected]> 撰写了本文档,偶尔会得到 Larry Wall 和 Jeffrey Friedl 的建议。
版权所有 © 2012 Tom Christiansen。
本程序是免费软件;您可以在与 Perl 相同的条款下重新分发或修改它。
这些示例大部分取自“骆驼书”的当前版本;也就是说,取自《Perl 编程》的第 4 版,版权所有 © 2012 Tom Christiansen <et al.>,2012-02-13 由 O’Reilly Media 出版。代码本身可以自由重新分发,并且我们鼓励您移植、折叠、纺锤和修改本手册中的任何示例,以便将其包含到您自己的程序中,而没有任何负担。通过代码注释进行确认是礼貌的,但不是必需的。
v1.0.0 – 首次公开发布,2012-02-27