某些波斯语文本使用宽字符打印,但其他文本则不然

Wide charectar in print for some Farsi text, but not others

我正在使用 Google Translate 通过 Perl 将一些错误代码转换为波斯语。波斯语就是一个这样的例子,我也在其他语言中发现了这个问题——但对于这个讨论,我将坚持使用一个例子:

“几何数据卡错误”的翻译文本工作正常(示例 1)但翻译“附加默认 111 卡”(示例 2)出现“宽字符”错误。

两个例子都可以从终端运行,它们只是打印出来的。

我试过像这样的 ,但无济于事:

use utf8;
use open ':std', ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

示例 1:有效

perl -Mutf8 -le 'print "\x{d8}\x{ae}\x{d8}\x{b7}\x{d8}\x{a7}\x{db}\x{8c} \x{da}\x{a9}\x{d8}\x{a7}\x{d8}\x{b1}\x{d8}\x{aa} \x{d8}\x{af}\x{d8}\x{a7}\x{d8}\x{af}\x{d9}\x{87} \x{d9}\x{87}\x{d9}\x{86}\x{d8}\x{af}\x{d8}\x{b3}\x{db}\x{8c}"'
خطای کارت داده هندسی

示例 2:这会产生宽字符警告并打印噪音

perl -Mutf8 -le 'print "\x{d8}\x{a7}\x{d9}\x{81}\x{d8}\x{b2}\x{d9}\x{88}\x{d8}\x{af}\x{d9}\x{86} \x{db}\x{8c}\x{da}\x{a9} \x{da}\x{a9}\x{d8}\x{a7}\x{d8}\x{b1}\x{d8}\x{aa} \x{d9}\x{be}\x{db}\x{8c}\x{d8}\x{b4}\x{200c}\x{d9}\x{81}\x{d8}\x{b1}\x{d8}\x{b6} 111"'
Wide character in print at -e line 1.
# <terminal noise, not Farsi text>

使用卷曲

如果我用 curl 做同样的请求,我得到这个:

curl 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=fa&hl=fa&dt=t&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&tk=xxxx&dt=dj&q=%41%70%70%65%6E%64%69%6E%67%20%61%20%64%65%66%61%75%6C%74%20%31%31%31%20%63%61%72%64'
[[["افزودن یک کارت پیش\u200cفرض 111","Appending a default 111 card",null,null,3,null,null,[[]],[[["982c75c78c6c8e6005ec3a4021a7f785","tea_GrecoIndoEuropeA_en2elfahykakumksq_2021q3.md"]]]]],null,"en",null,null,null,1,[],[["en"],null,[1],["en"]]]

注意上面 JSON 输出中的 \u200c 是一个 "‌Zero Width Non-Joiner" unicode 字符。当 JSON::from_json 解析 \u200c 它爆炸了:

perl -Mutf8 -MJSON -e 'print from_json("[\"\u200c\"]")->[0];'
Wide character in print at -e line 1.

我可以这样“修复”它:

my $c = $res->content;
$c =~ s/\u[0-9a-f]{4}//;
my $json = from_json($c);

然后输出文本正确(从右到左):

افزودن یک کارت پیشفرض 111

问题:这是怎么回事?

JSON 对象需要启用 utf8,它将修复 \u200c。感谢@Shawn 为我指明了正确的方向:

my $j = JSON->new;
$j->utf8(1);
my $json = $j->decode($c);

现在,在返回 JSON 散列时,JSON-formatted 文本内容如 \u200c 被正确音译为 \xe2\x80\x8c

这里发生了很多事情。我认为很多,尤其是在前两个示例中,源于不理解 perl 的两种字符串模式(面向字节和面向 Unicode 代码点)之间的区别。

示例 1 是一个原始字节字符串,其中包含恰好采用 UTF-8 编码的字节,并且未更改地通过;只要显示输出的终端需要 UTF-8,它们就会被正确呈现。示例 2 有一个 'wide' 字符(值大于 255),使其成为 Unicode 字符串,其中每个由大于 127 的 \x{NN} 数字表示的字符是一个编码为多个字节的 Unicode 代码点在 UTF-8 中。打印它会导致 mojibake 和警告,因为标准输出是面向字节的,没有转换层。

正如我在评论中建议的那样,阅读 perluniintro(以及其他 unicode-related 文档)是了解事物运作方式的良好开端。


但在实际任务中,从 curl 命令返回的 JSON 中提取文本...如果这是 [=],我会使用 jq 44=] 脚本:

$ curl ... | jq -r '.[0][0][0]'
افزودن یک کارت پیش‌فرض 111

与等效的 perl 比较 one-liner:

$ curl ... | perl -CS -MJSON -lne 'print from_json($_)->[0][0][0]'
افزودن یک کارت پیش‌فرض 111

-CS 参数告诉 perl 标准输入、输出和错误都是 UTF-8 编码的。您也可以使用 -CO 来制作标准输出,并使用 decode_json() 代替,它需要原始 UTF-8 编码字节而不是 Unicode 字符串。

并且在脚本而不是 one-liner 中,使用面向 JSON 的 OO 接口并使用其方法调整输入字符串的编码方式,加上 open 编译指示(或binmodeopen 的编码层)而不是 -C 选项,是要走的路。