在 Perl 中,如何在标量中创建 "mixed-encoding" 字符串(或原始字节序列)?

In Perl, how to create a "mixed-encoding" string (or a raw sequence of bytes) in a scalar?

在我的 Perl 脚本中,我必须将 UTf-8 和原始字节混合写入文件。

我有一个大字符串,其中所有内容都编码为 UTF-8。在那个“源”字符串中,UTF-8 字符就像它们应该的那样(即 UTF-8 有效字节序列),而“原始字节”已被存储,就好像它们是原始字节。因此,在源字符串中,一个 0x50 的“原始”字节将存储为一个 0x50 字节;而 0xff 的“原始”字节将存储为 0xc3 0xbf 两字节 utf-8 有效序列。当我写回这些“原始”字节时,我需要将它们恢复为单字节形式。

我有其他数据结构可以让我知道字符串的哪些部分代表哪种数据。字段、类型、长度等的列表

在普通文件中写入时,我依次写入每个字段,要么直接(如果它是 UTF-8),要么通过将其值编码为 ISO-8859-1(如果它是原始字节)。它完美运行。

现在,在某些情况下,我需要将值不直接写入文件,而是作为 BerkeleyDB(Btree,但这几乎无关紧要)数据库的记录。 为此,我需要在一次写入操作中写入构成我的记录的所有值。这意味着我需要一个包含 UTF-8 和原始字节混合的标量。


示例:

输入标量(所有十六进制值):61 C3 8B 00 C3 BF

预期输出格式:2 个 UTF-8 字符,然后是 2 个原始字节。

预期输出:61 C3 8B 00 FF


起初,我通过连接我从一个空字符串写入文件的相同值来创建一个字符串。我尝试在不添加编码的情况下将该字符串写入“标准”文件。我有 '?'字符而不是我所有超过 0x7f 的原始字节(因为,很明显,Perl 决定将我的字符串视为 UTF-8)。


然后,为了尝试告诉 Perl 它已经被编码,并且“请不要对此自作聪明”,我尝试将 UTF-8 部分编码为“UTF-8”,对二进制文件进行编码部分到“ISO-8859-1”,并连接一切。然后我写了。这一次,字节看起来很完美,但是已经是UTF-8的部分被“双重编码”了,即多字节字符的每个字节都被视为它的代码点...


我认为 Perl 不应该将“内部”UTF-8 重新编码为“编码”UTF-8,如果它在内部标记为 UTF-8。包含 UTF-8 中所有值的字符串来自 C API,它设置 UTF-8 标记(或者至少应该设置),让 Perl 知道它是 已经解码。

知道我错过了什么吗?

有没有办法告诉 Perl 我想做的只是一个接一个地放入一堆字节,请不要尝试以任何方式解释它们?出于这个原因,我写入的文件以“>:raw”打开,但我想我需要一种方法来指定给定标量也是“原始”的吗?



结语:我找到了问题的原因。 $bigInputString 应该 完全由 UTF-8 编码数据组成。但它 did 包含具有大值的原始字节,因为 C 中的错误(结果是“char”(不是“unsigned char”)最好用位运算符测试,而不是“> 127”...咳咳)。因此,在 C API.

中,“大”字节未拆分为两个字节的 UTF-8 序列

这意味着从错误的 C 数据创建的 $bigInputString 没有预期的内容,Perl 理所当然地也不喜欢它。

在我纠正错误后,字符串正确编码为 UTF-8(对于我想保留为 UTF-8 的部分)或 LATIN-1(对于我想转换回来的“原始字节”),我没有进一步的问题。

抱歉浪费你们的时间,伙计们。但我还是学到了一些东西,所以我会把它放在这里。这个故事的寓意是,Devel::Peek 非常适合调试(感谢 ikegami),并且应该始终仔细检查,而不是假设。诚然,我周五很匆忙,但错还是在我。

所以,感谢所有帮助过或尝试过的人,特别感谢 ikegami(再次),他花了很多时间帮助我。

所以你有

my $in = "\x61\xC3\x8B\x00\xC3\xBF";

你想要

my $out = "\x61\xC3\x8B\x00\xFF";

这是仅对输入字符串的某些部分进行解码的结果,因此您需要如下内容:

sub decode_utf8 { my ($s) = @_; utf8::decode($s) or die("Invalid Input"); $s }

my $out = join "",
               substr($in, 0, 3),
   decode_utf8(substr($in, 3, 1)),
   decode_utf8(substr($in, 4, 2));

Tested.

或者,您可以解码整个内容并重新编码应该编码的部分。

sub encode_utf8 { my ($s) = @_; utf8::encode($s); $s }

utf8::decode($in) or die("Invalid Input");
my $out = join "",
   encode_utf8(substr($in, 0, 2)),
               substr($in, 2, 1),
               substr($in, 3, 1);

Tested.

您没有说明您如何知道哪些要解码哪些不应该解码,但您表示您有此信息。

假设您有一个 Unicode 字符串,您知道每个代码点应该存储为什么 - UTF-8 序列或单个字节,以及一种创建模板字符串的方法,其中每个字符代表对应的字符的 unicode 字符串应该使用(U 用于 UTF-8,C 用于单字节以保持简单),您可以使用 pack:

#!/usr/bin/env perl
use strict;
use warnings;

sub process {
    my ($str, $formats) = @_;
    my $template = "C0$formats";
    my @chars = map { ord } split(//, $str);
    pack $template, @chars;
}

my $str = "\x61\xC3\x8B\x00\xC3\xBF";
utf8::decode($str);
print process($str, "UUCC"); # Outputs 0x61 0xc3 0x8b 0x00 0xff