Wed Oct 8 22:14:40 CST 2025
今天终于彻底搞明白了 perl 的字符串是怎么工作的。
Perl 里字符串(string)和二进制(octet)都是用标量存储(scalar)的,没有类型上的区别。
可以在脑袋里维持一个思维模型:字符串是 u32 数组,二进制是 u8 数组。二进制用指定的编码解码后,可以得到 Perl 认可的字符串;字符串被编码之后,Perl 就会把它当成二进制来对待。
具体来说,`length('哈哈')` 的值是 2,而 `length(Encode::encode_utf8('哈哈'))` 的值就是 6 了。前者被视为拥有两个字符的字符串,后者被视为拥有六个字节的二进制。
其实这也并是不完全正确。Perl 有个 utf8 的 pragma 开关,当使用它的时候源代码里嵌入的 utf8 字符串会被当成字符串,而当不打开这个开关时,嵌入的 utf8 字符串会被认为是字节流。所以如果不使用 `use utf8;` 的话,`length('哈哈')` 会直接是 6……为了兼容性。
新的代码在任何情况下都应该打开 `use utf8;` 。
`use utf8;` 开关只影响源文件内嵌入的字符串。`@ARGV` 和默认打开文件后读取的东西、以及反引号捕获的进程输出还是会被视为二进制。如果需要的话得手动解码成字符串。
因为字符串可以被认为是 u32 数组,所以它的内容经常被称为 `Wide character`。
写文件的时候默认写的数据是二进制。如果写的内容里出现了字符串,就会报警告。需要使用 `binmode(STDOUT, ':utf8')` 之类的给文件加上编码 utf8 的 layer 解决。
Perl 的标题是可以拼接的。如果二进制拼接了二进制,结果还是二进制;如果二进制拼了 ASCII 字符串,结果是二进制;如果二进制拼了有 unicode 字符的字符串,比如 `Encode::encode_utf8('哈哈') . '哈哈'`,那么结果就是字符串了。
在二进制拼 unicode 字符串的情况下,虽然最后结果是一个字符串,但是实际上它的内部表示处于一种一半二进制一半字符串的情况。不管对它编码还是对它解码都是不对的。不要这么拼。
到目前为止,为了让字符串和二进制和谐共处,需要注意以下事项:
1) 在开头加上一些固定操作
use open qw/:std :utf8/; # 给 stdin 和 stdout 加上 utf8 的编解码层(如果输入是图片之类的就不加)
use utf8; # 将源文件中的所有字符串以 utf8 解码
utf8::decode($_) for @ARGV; # 将 @ARGV 的所有参数以 utf8 解码
PS. 如果是二进制的话,最好使用 `binmode STDIN`。因为它在 Windows 上面会自动转换 CRLF。不过应该没人用 Windows 吧。
2) 打开文件时使用三参数的 open,手动指定 utf8 解码层
open my $fd, '<:utf8', 'file.txt';
通过 `use open OUT => ':utf8'` 可以将 utf8 设置成默认的编解码层。但是这样做不直观,并且在需要读写二进制的时候会导致麻烦的操作。不注意还可能导致编码/解码两次的情况出现。不要这样做。
3) 注意反引号产生的输入
my $text = `cat file.txt`;
utf8::decode($text);
4) 将字符串传入需要二进制的地方时,记得预先编码
my $text = '哈哈';
my $octet = $text;
utf8::encode($octet);
my $sum = sha1_sum $octet;
5) 自己函数需要二进制作为输入时,预先 downgrade
my ($octet) = @_;
utf8::downgrade($octet);
这样我们写的代码就不会受到 utf8 的问题困扰了。
虽然我们的代码已经完美了,但是仍有一些古老的库不关心 utf8。比如它们可能会从互联网或者本地读取一个 utf8 编码的文件,不解码直接处理,完事后扔给我们。这样我们就得到了一个古老库认为它们是 ascii 字符串,我们觉得它是一个二进制的东西了。也可能我们给古老库传入了一个字符串,但是它悄悄地移除了所有的 utf8 标记,然后把结果返回给我们。
古老库通常不会注明它们不会解码 utf8,现代库也不太会注明它们会不会解码 utf8(`JSON` 库是好的,它同时提供了面向 utf8 二进制和字符串的接口。赞美 `JSON`)。想要知道他们到底支不支持 utf8,只能检查代码了。
+-----------------------------------------------------------------+
| |
| Decoded Text |
| |
| |
| +--------------------+ downgrade +--------------------+ |
| | Internally encoded | --------------> | Internally encoded | |
| | as UTF-8 | | as iso-8859-1 | |
| | (is_utf8 = 1) | <-------------- | (is_utf8 = 0) | |
| +--------------------+ upgrade +--------------------+ |
| |
+-----------------------------------------------------------------+
| ^
| |
encode | | decode
| |
v |
+-----------------------------------------------------------------+
| |
| Bytes or |
| Encoded Text |
| |
| |
| +--------------------+ downgrade +--------------------+ |
| | Internally encoded | --------------> | Internally encoded | |
| | as UTF-8 | | as iso-8859-1 | |
| | (is_utf8 = 1) | <-------------- | (is_utf8 = 0) | |
| +--------------------+ upgrade +--------------------+ |
| |
+-----------------------------------------------------------------+
检查库是否会解码 utf8 其实没有什么好方法……