关于 unpack() 和 printf() 中的 v 标志的 Perl 问题

Posted

技术标签:

【中文标题】关于 unpack() 和 printf() 中的 v 标志的 Perl 问题【英文标题】:Perl questions regarding unpack() and the v flag in printf() 【发布时间】:2016-04-19 21:03:25 【问题描述】:

我正在努力完成以下工作:

对于任意 Perl 字符串(无论它是否在内部以 UTF-8 编码,以及是否设置了 UTF-8 标志),从左到右扫描字符串,并且对于每个字符,打印该字符的十六进制格式的 Unicode 代码点。让自己绝对清楚:我不想打印 UTF-8 字节序列或其他东西;我只想为字符串中的每个字符打印 Unicode 代码点

起初,我想出了以下解决方案:

#!/usr/bin/perl -w

use warnings;
use utf8;
use feature 'unicode_strings';

binmode(STDOUT, ':encoding(UTF-8)');
binmode(STDIN, ':encoding(UTF-8)');
binmode(STDERR, ':encoding(UTF-8)');

$Text = "\x3B1\x3C9";
print $Text."\n";
printf "%vX\n", $Text;

# Prints the following to the console (the console is UTF8):
# αω
# 3B1.3C9

然后我看到了一些例子,但没有合理的解释,这让我怀疑我的解决方案是否正确,现在我对自己的解决方案以及示例有疑问。

1) Perl 关于 (...)printf 中的 v 标志的文档说:

"该标志告诉 Perl 将提供的字符串解释为整数向量,字符串中的每个字符对应一个。[...]"

不过,它并没有说明“整数向量”的确切含义。在查看我的示例的输出时,似乎这些整数是 Unicode 代码点,但我希望有确定的人确认这一点。

因此问题:

1) 我们能否确定以这种方式从字符串中提取的每个整数都是相应字符的 Unicode 代码点(而不是其他一些字节序列)?

其次,关于我找到的一个示例(稍作修改;我不记得我从哪里得到它,可能来自 Perl 文档):

#!/usr/bin/perl -w

use warnings;
use utf8;
use feature 'unicode_strings';

binmode(STDOUT, ':encoding(UTF-8)');
binmode(STDIN, ':encoding(UTF-8)');
binmode(STDERR, ':encoding(UTF-8)');

$Text = "\x3B1\x3C9";
print $Text."\n";
printf "%vX\n", $Text for unpack('C0A*', $Text);

# Prints the following to the console (the console is UTF8):
# αω
# 3B1.3C9

作为一名 C 和汇编人员,我只是不明白为什么有人会像示例中所示那样编写 printf 语句。根据我的理解,相应的行在语法上等同于:

for $_ (unpack('C0A*', $Text)) 
  printf "%vX\n", $Text;  

据我了解,unpack() 接受$Text,将其解包(无论具体是什么意思)并返回一个列表,在这种情况下,该列表包含一个元素,即解包后的字符串。然后 $_ 用一个元素遍历该列表(没有在任何地方使用),因此块(即printf())执行一次。综上所述,上述sn-p所做的唯一动作就是执行printf "%vX\n", $Text;一次。

因此问题:

2) 像示例中所示将其包装到 for 循环中的原因可能是什么?

最后的问题:

3) 如果问题 1) 的答案是“是”,为什么我看到的大多数示例毕竟使用unpack()

4) 在上面的三行 sn-p 中,unpack() 周围的括号是必需的(离开它们会导致语法错误)。相比之下,在示例中,unpack() 不需要括在括号中(但如果添加它们也无妨)。谁能解释一下原因?

编辑/更新以回复以下 ikegami 的回答:

当然,我知道字符串是整数序列。但是

a) 这些整数有许多不同的编码,并且在某个字符串的内存区域中的字节取决于编码,即如果我有两个包含完全相同的字符串 字符序列,但是我使用不同的编码将它们存储在内存中,字符串内存位置的字节序列是不同的。

b) 我强烈认为(除了 Unicode)还有许多其他系统/标准将字符映射到整数/代码点。例如,Unicode 代码点 0x3B1 是希腊字母 α,但在其他一些系统中,它可能是德语字母 Ö。

在这种情况下,恕我直言,这个问题完全有道理,但我可能应该更准确并改写它:

如果我有一个字符串$Text,它只包含Unicode 代码点的字符,然后如果我执行printf "%vX\n", $Text;,它会为每个字符打印Unicode 十六进制代码点在所有情况下,特别是(但不限于):

不考虑 Perl 对字符串的实际内部编码 不管字符串的 UTF-8 标志是什么 use 'unicode_strings' 是否处于活动状态

如果答案是肯定的,那么所有使用unpack() 的示例有什么意义,尤其是上面的示例?顺便说一句,我现在记得我从哪里得到的:原始形式在 Perl 的 pack() 文档中,在关于 C0 和 U0 模式的部分中。既然他们使用的是unpack(),那么这样做肯定有充分的理由。

编辑/更新 2 号

我做了进一步的研究。下面证明UTF8标志起到了重要作用:

use Encode;
use Devel::Peek;

$Text = "\x3B1\x3C9";
Dump $Text;
printf("\nSPRINTF: %vX\n", $Text);
print("UTF8 flag: ".((Encode::is_utf8($Text)) ? "TRUE" : "FALSE")."\n\n");

Encode::_utf8_off($Text);
Dump $Text;
printf "\nSPRINTF: %vX\n", $Text;
print("UTF8 flag: ".((Encode::is_utf8($Text)) ? "TRUE" : "FALSE")."\n\n");

# This prints the following lines:
#
# SV = PV(0x1750c20) at 0x1770530
#   REFCNT = 1
#   FLAGS = (POK,pPOK,UTF8)
#   PV = 0x17696b0 "\316\261\317\211"\0 [UTF8 "\x3b1\x3c9"]
#   CUR = 4
#   LEN = 16
#
# SPRINTF: 3B1.3C9
# UTF8 flag: TRUE
#
# SV = PV(0x1750c20) at 0x1770530
#   REFCNT = 1
#   FLAGS = (POK,pPOK)
#   PV = 0x17696b0 "\316\261\317\211"\0
#   CUR = 4
#   LEN = 16
#
# SPRINTF: CE.B1.CF.89
# UTF8 flag: FALSE

我们可以看到_utf_off 确实删除了 UTF8 标志,但保持字符串的字节不变。 sprintf() 带有 v 标志输出不同的结果,仅取决于字符串的 UTF8 标志,即使字符串的字节保持不变。

【问题讨论】:

对于问题 3,printf "%vX\n", $Text for unpack('C0A*', $Text); 中 for 的上下文是语句修饰符语法,其语法为 for LIST。其中第 3 行中的 sn-p 是一个控制块。语法不同。 LABEL for VAR (LIST) BLOCK 您可以在 perl 文档的 perlsyn 部分中的复合语句和语句修饰符下阅读这些内容 您对等效的for 循环几乎是正确的,但是unpack 返回一个值的列表,因此循环内将是printf "%vX\n", $_。但是unpack 的模板非常奇怪。 C 规则获取下一个字符的代码点并将其作为整数返回,但是如果重复计数为 0,它将完全无效。然后A 规则返回一个字符串,并且重复计数为* 它只返回对象字符串的所有其余部分。换句话说,unpack 'C0A*', $Text 就是$Text。这是损坏的或故意混淆的代码 对于您的问题 #1,我在“Programming Perl”第 3 版一书中发现了对“v”标志的引用...“v”标志对于显示字符的序数值很有用任意字符串: printf "version is v%vd\n", $^V; # Perl 的版本 printf "address is %*vX\n", ":", $addr; # IPv6 地址 printf "bits are %*vb\n", " ", $bits; # 随机位串 您能否更明确地说一下从unpack 调用中删除括号的意思?我不希望它在这里有任何区别,我的测试证实了这一点 @Chris Doyle:感谢您的回答。在阅读 perldoc perlsyn 时我没有注意到这一点。您完全正确,并且在文档中明确提及。 【参考方案1】:

sprintf '%vX' 不知道代码点或 UTF-8。它只返回字符串字符的字符串表示形式。换句话说,

sprintf('%vX', $s)

等价于

join('.', map  sprintf('%X', ord($_))  split(//, $s))

这意味着它以十六进制输出s[0]s[1]s[2]、...、s[length(s)-1],以点分隔。

无论UTF8 标志的状态如何,它都会返回字符串的字符(整数)。这意味着字符串的存储方式(例如,UTF8 标志是否设置)对输出没有影响。

use Encopde;

$Text1 = "\xC9ric";
utf8::downgrade($Text2);

printf("Text1 is a string of %1\$d characters (a vector of %1\$d integers)\n",
   length($Text1));
print("UTF8 flag: ".((Encode::is_utf8($Text2)) ? "TRUE" : "FALSE")."\n");
printf("SPRINTF: %vX\n\n", $Text1);

$Text2 = $Text1;
utf8::upgrade($Text2);
print($Text1 eq $Text2
    ? "Text2 is identical to Text1\n\n"
    : "Text2 differs from Text1\n\n");

printf("Text2 is a string of %1\$d characters (a vector of %1\$d integers)\n",
   length($Text2));
print("UTF8 flag: ".((Encode::is_utf8($Text2)) ? "TRUE" : "FALSE")."\n");
printf "SPRINTF: %vX\n\n", $Text2;

输出:

Text1 is a string of 4 characters (a vector of 4 integers)
UTF8 flag: FALSE
SPRINTF: C9.72.69.63

Text2 is identical to Text1

Text2 is a string of 4 characters (a vector of 4 integers)
UTF8 flag: TRUE
SPRINTF: C9.72.69.63

让我们更改您问题中的代码以显示相关信息:

use Encode;

$Text1 = "\x3B1\x3C9";

printf("Text1 is a string of %1\$d characters (a vector of %1\$d integers)\n",
   length($Text1));
printf("SPRINTF: %vX\n\n", $Text1);

$Text2 = $Text1;
Encode::_utf8_off($Text2);
print($Text1 eq $Text2
    ? "Text2 is identical to Text1\n\n"
    : "Text2 differs from Text1\n\n");

printf("Text2 is a string of %1\$d characters (a vector of %1\$d integers)\n",
   length($Text2));
printf "SPRINTF: %vX\n\n", $Text2;

输出:

Text1 is a string of 2 characters (a vector of 2 integers)
SPRINTF: 3B1.3C9

Text2 differs from Text1

Text2 is a string of 4 characters (a vector of 4 integers)
SPRINTF: CE.B1.CF.89

这表明sprintf '%vX'对于不同的字符串会有不同的输出,这并不奇怪,因为sprintf '%vX'只是输出字符串的字符。你可以很容易地使用uc 而不是_utf8_off


    如果对于两个相同的字符串,sprintf '%vX' 根据 UTF8 标志更改其输出,则将被认为存在 Unicode 错误。大多数情况都已修复(尽管 sprintf 从未遇到过此错误)。

【讨论】:

请查看我更新的问题。关于括号,我没有注意到 Perl 的文档明确解释了何时使用它们,何时不使用它们。 我认为我的更新确实添加了一些新内容。想象一下像我这样担心这个问题并通过谷歌提出这个问题的人。我认为他们会感谢您的澄清,并再次明确代码点和编码之间的区别。此外,我已经提到我在哪里找到了使用 unpack() 和奇怪的 for ... 构造的示例,并且由于该示例来自一个非常严肃的来源(即 Perl 文档),我想知道它们为什么会这样这样做很奇怪。尽管如此,接受你的回答...... 经过进一步研究,您的答案似乎并不完全正确。我明确询问过是否设置了 UTF8 标志是否有效,你说是的。这是不正确的。从$Text 中删除UTF8 标志后,printf "%vX\n", $Text; 打印出$TextUTF-8 字节序列(而不是Unicode 代码点)。 稍后我将彻底研究您更新的示例。但是(虽然还没有研究过这个例子)我仍然认为我并没有完全弄错。我做了以下事情:perl -e 'use Encode; $Text = "\x3B1\x3C9"; printf("%vX\n", $Text); Encode::_utf8_off($Text); printf("%vX\n", $Text);',这会在第一行打印 Unicode 代码点,在第二行打印 UTF8 字节。我知道不应该像我那样关闭 UTF8 标志(事实上,我从未在生产代码中这样做过),但它似乎是作为实验关闭它的最简单方法。 1) 是的,我从不在生产中使用 _utf8_off;我只是好奇会发生什么。 2) 你是说Encode::_utf8_off($s) 也有 implicit encode_utf8($s) 吗?我怀疑......相反,Encode 的文档似乎暗示内存中表示字符串的字节 not 被执行 Encode::_utf8_off 触及(事实上,这似乎是避免的主要原因那)。好吧,似乎是时候听从您的建议并使用 Devel::Peek 了。

以上是关于关于 unpack() 和 printf() 中的 v 标志的 Perl 问题的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 struct.unpack 并将其转换为 Objective-c 中的值

Javascript Mismatch 中的 PHP Pack/Unpack 实现

perl中的pack与unpack

小白学Lua之Lua变长参数和unpack函数

Qt4 中的 PHP unpack() 模拟函数

如何以十六进制格式从 unpack 中获取校验和?