去除UTF-8字符串中占用的闲置空间

在研究 Koha 中的一个非常奇怪的 bug 时,我不得不找出一种方法,将字符串切成特定的最大长度。以字节为单位,而不是以字符为单位,因为在这种情况下使用的是可怕的 USMARC 格式,其规格以两面红旗开头:它是 2000 年 1 月发布的,而且是 “美国国家标准的实现”,所以你可以肯定它只适用于 ASCII,在处理 Unicode 时会……很有趣。但对于较长的字符串来说,它一般都是坏的。

字节与字符

我想你应该知道字节和字符(或代码点)的区别。如果不知道,请阅读此文

您可能还没读过链接,这里有一个非常简短的摘要:

  • UTF8 使用可变长度将字母存储为 0 和 1。旧格式(如 ASCII)使用固定长度(例如一个字节 = 8 位),因此只能表示有限数量的字母。
  • “普通 “字母(如果你说的是英文)需要 1 个字节。
  • 但我们可以处理英语以外的字母。我们有像🚲这样的 “字母”。🚲 需要 4 个字节。在这里可以看到很多细节。
  • 通常,如果普通人谈论字符串的长度,他们会计算字母:”Hello “有 5 个字母。”Hello🚲”有 6 个字母。
  • 不幸的是,计算机有时需要知道字节数(例如要将数据存储到某个地方)。”Hello “需要 5 个字节。”Hello🚲”需要 9 个字节。

我的字符串有多长?

让我们比较一下 “I love to ride my bicycle “和 “I ♥ to ride my 🚲”。

~$ perl -C -Mutf8 -MEncode -E 'say q{"I love to ride my bicycle" vs. "I ♥ to ride my 🚲"}'
   "I love to ride my bicycle" vs. "I ♥ to ride my 🚲"

本脚注2 解释了可能存在的奇怪命令行标志。

默认情况下,Perl 使用 length() 计算字符数:

~$ perl -C -Mutf8 -MEncode -E 'say length("I love to ride my bicycle")'
   25

所以是 25 个字符。

~$ perl -C -Mutf8 -MEncode -E 'say length("I ♥ to ride my 🚲")'
   16

现在只有 16 。

如果我们要计算字节数(通常不应该这样做,因为计算机会帮你处理),我们需要使用 bytes::length()

~$ perl -C -Mutf8 -MEncode -E 'say bytes::length("I love to ride my bicycle")'
   25

也是 25 个字节,因为每个基本英文字母需要一个字节。

~$ perl -C -Mutf8 -MEncode -E 'say bytes::length("I ♥ to ride my 🚲")'
   21

但现在花哨的 Unicode 版本需要 21 个字节,因为 ♥ 需要 3 个字节,🚲 需要 4 个字节,所以比字符多了 3 + 4 – 2 = 5 个字节。

塞进有限的空间

现在假设我们只有 20 个字节来存储这个字符串。因此,我们需要使用 substr() 删除部分文本:

~$ perl -C -Mutf8 -MEncode -E 'say substr("I love to ride my bicycle", 0, 20)'
   I love to ride my bi

有效!

如果我们在花哨的 Unicode 版本上这样做:

~$ perl -C -Mutf8 -MEncode -E 'say substr("I ♥ to ride my 🚲", 0, 20)'
   I ♥ to ride my 🚲

我们会得到相同的字符串!因为默认情况下,Perl 会计算字符数。而字符串只有 16 个字符。但却有 21 个字节。多了 1 个字节。因此,我们需要使用 bytes::substr()

~$ perl -C -Mutf8 -MEncode -E 'say bytes::substr("I ♥ to ride my 🚲", 0, 20)'
   I ⥠to ride my ð

那垃圾是什么?

由于我们离开了常规 Perl 字符串处理和调用字节的安全填充空间,我们不得不更加小心谨慎,并了解一些 Perl 的内部机制(我将略作说明):Perl 会跟踪字符串是否包含 UTF-8。如果调用 bytes,它就会忘记字符串包含 UTF-8,而输出原始字节。但我们希望 Perl 将 bytes::substr 返回的字符串解释为 UTF-8,因此我们必须通过 decode_utf8(从 Encode 导入)明确地告诉它。我觉得 decode_utf8 这个名字有点令人困惑,但(我)这样想会有帮助:将我们知道包含 UTF-8 的字节字符串(或文档中所说的八位字节)解码为 Perl 字符串。那么

~$ perl -C -Mutf8 -MEncode -E 'say decode_utf8(bytes::substr("I ♥ to ride my 🚲", 0, 20))'
   I ♥ to ride my �

耶,我们的 ♥ 又回来了!

另一种方法(因此我们不必使用字节)是将字符串的字节表示传递给 substr,我们可以使用 encode_utf8 生成字符串的字节表示。这又有点令人困惑,除非你这样想:把这个 Perl 字符串用 UTF-8 编码成一堆字节:

perl -C -Mutf8 -MEncode -E 'say decode_utf8(substr(encode_utf8("I ♥ to ride my 🚲"), 0, 20))'
   I ♥ to ride my �

一样,但稍微好一点!

总之,我们又可以用”♥”了。但得到的字符串有点难看,因为它以”�”结尾。由于我们砍掉了 🚲 的几个字节,我们最终得到的确实是一个无效符号,它被渲染为 �。

但这个被混淆的字符串有多长呢?

~$ perl -C -Mutf8 -MEncode -E 'say bytes::length(bytes::substr("I ♥ to ride my 🚲", 0, 20))'
   20

耶,20 个字节,现在合适了!

把它塞进有限的空间里,但不要有拖尾垃圾

现在,也许我们不喜欢结尾的 � 了。有一个很好的标志可以传递给 decode_utf8(),这是我在处理/对抗最初的 bug 时了解到的:Encode::FB_QUIET.

perl -C -Mutf8 -MEncode -E 'say decode_utf8(substr(encode_utf8("I ♥ to ride my 🚲"),0,20),Encode::FB_QUIET)'
   I ♥ to ride my 

不再有 �!因为 FB_QUIET 会告诉 decode_utf8 忽略无效字节

那么它有多长呢?

perl -C -Mutf8 -MEncode -E 'say bytes::length(decode_utf8(substr(encode_utf8("I ♥ to ride my 🚲"),0,20),Encode::FB_QUIET))'
   17

甚至更短,因为现在字符串不包含任何自行车部件 🙂

所有字符串

为了好玩,下面是从 1 到原始字符串的所有子串,不含任何 “半 “字符或无效字符。您可以清楚地看到,🚲 在完全呈现之前需要经过几个步骤:

perl -C -Mutf8 -MEncode -E 'my $s= "I ♥ to ride my 🚲"; for my $l (1 .. bytes::length($s)) { say decode_utf8(substr(encode_utf8($s),0,$l),Encode::FB_QUIET)}'
I
I 
I 
I 
I ♥
I ♥ 
I ♥ t
I ♥ to
I ♥ to 
I ♥ to r
I ♥ to ri
I ♥ to rid
I ♥ to ride
I ♥ to ride 
I ♥ to ride m
I ♥ to ride my
I ♥ to ride my 
I ♥ to ride my 
I ♥ to ride my 
I ♥ to ride my 
I ♥ to ride my 🚲

Footnotes

命令行标志说明:

  • -C 是 -CESL 的缩写,用于打开各种输入和输出流(STDOUT 等)的 UTF-8。参见 perldoc perlrun。
  • -Mutf8 加载 utf8 模块,与在源代码中调用 use utf8; 相同。这会告诉 Perl 源代码本身包含 UTF-8 字符。就像🚲…
  • -MEncode 加载编码。

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注