内容

名称

perlfaq4 - 数据处理

版本

版本 5.20210520

说明

本部分常见问题解答回答了与处理数字、日期、字符串、数组、哈希和各种数据问题相关的问题。

数据:数字

为什么我得到长小数(例如 19.9499999999999)而不是我应该得到的数字(例如 19.95)?

有关详细说明,请参阅大卫·戈德伯格的“每个计算机科学家都应该了解的浮点运算”(http://web.cse.msu.edu/~cse320/Documents/FloatingPoint.pdf)。

在内部,您的计算机以二进制形式表示浮点数。数字(如 2 的幂)计算机无法准确存储所有数字。某些实数在此过程中会丢失精度。这是计算机存储数字的方式存在的问题,它会影响所有计算机语言,而不仅仅是 Perl。

perlnumber 展示了数字表示和转换的可怕细节。

要限制数字的小数位数,可以使用 printfsprintf 函数。有关更多详细信息,请参阅 "perlop 中的浮点算术"

printf "%.2f", 10/3;

my $number = sprintf "%.2f", 10/3;

为什么 int() 会损坏?

您的 int() 很可能工作正常。问题在于数字与您想象的不太一样。

首先,请参阅“为什么我得到长小数(例如 19.9499999999999)而不是我应该得到的数字(例如 19.95)?”的答案。

例如,此

print int(0.6/0.2-2), "\n";

将在大多数计算机上打印 0,而不是 1,因为即使是 0.6 和 0.2 等简单数字也无法通过浮点数准确表示。您在上面认为的“三个”实际上更像是 2.9999999999999995559。

为什么我的八进制数据无法正确解释?

(由 brian d foy 贡献)

您可能正在尝试将字符串转换为数字,而 Perl 仅将其转换为十进制数字。当 Perl 将字符串转换为数字时,它会忽略前导空格和零,然后假定其余数字为基数 10

my $string = '0644';

print $string + 0;  # prints 644

print $string + 44; # prints 688, certainly not octal!

此问题通常涉及 Perl 内置函数,该函数与 Unix 命令同名,该命令在命令行中使用八进制数字作为参数。在此示例中,命令行上的 chmod 知道其第一个参数是八进制数,因为这是它的作用

%prompt> chmod 644 file

如果您想在 Perl 中使用相同的字面数字 (644),则必须通过在数字前加上 0 或使用 oct 告诉 Perl 将它们视为八进制数字

chmod(     0644, $filename );  # right, has leading zero
chmod( oct(644), $filename );  # also correct

当您从 Perl 认为是字符串的内容(例如 @ARGV 中的命令行参数)获取数字时,就会出现问题

chmod( $ARGV[0],      $filename );  # wrong, even if "0644"

chmod( oct($ARGV[0]), $filename );  # correct, treat string as octal

您可以始终通过以八进制表示法打印值来检查您正在使用的值,以确保它与您认为应该匹配的值匹配。以八进制和十进制格式打印它

printf "0%o %d", $number, $number;

Perl 是否有 round() 函数?ceil() 和 floor() 呢?三角函数呢?

记住,int() 仅仅是向 0 截断。对于舍入到特定数量的数字,sprintf()printf() 通常是最简单的途径。

printf("%.3f", 3.1415926535);   # prints 3.142

POSIX 模块(标准 Perl 发行版的一部分)实现了 ceil()floor() 以及许多其他数学和三角函数。

use POSIX;
my $ceil   = ceil(3.5);   # 4
my $floor  = floor(3.5);  # 3

在 5.000 至 5.003 的 Perl 中,三角函数是在 Math::Complex 模块中完成的。在 5.004 中,Math::Trig 模块(标准 Perl 发行版的一部分)实现了三角函数。在内部,它使用 Math::Complex 模块,并且一些函数可以从实轴分解到复平面,例如 2 的反正弦。

在财务应用中进行舍入可能会产生严重的影响,并且应准确指定所使用的舍入方法。在这些情况下,可能不要相信 Perl 使用的任何舍入系统,而是自己实现所需的舍入函数。

要了解原因,请注意,在半路点交替时,您仍然会遇到问题

for (my $i = -5; $i <= 5; $i += 0.5) { printf "%.0f ",$i }

-5 -4 -4 -4 -3 -2 -2 -2 -1 -0 0 0 1 2 2 2 3 4 4 4 5

不要责怪 Perl。它与 C 中的相同。IEEE 规定我们必须这样做。绝对值在 2**31(在 32 位计算机上)以下的 Perl 数字将非常像数学整数。其他数字无法保证。

如何转换数字表示形式/进制/基数?

与 Perl 一样,总有多种方法可以做到这一点。以下是进行数字表示形式之间常见转换的一些方法示例。这旨在具有代表性,而不是详尽无遗。

perlfaq4 中后面的一些示例使用了 CPAN 中的 Bit::Vector 模块。您可能选择 Bit::Vector 而非 Perl 内置函数的原因在于,它适用于任何大小的数字,它针对某些操作进行了速度优化,并且对于至少一些程序员来说,符号可能很熟悉。

如何将十六进制转换为十进制

使用 perl 内置的 0x 符号转换

my $dec = 0xDEADBEEF;

使用 hex 函数

my $dec = hex("DEADBEEF");

使用 pack

my $dec = unpack("N", pack("H8", substr("0" x 8 . "DEADBEEF", -8)));

使用 CPAN 模块 Bit::Vector

use Bit::Vector;
my $vec = Bit::Vector->new_Hex(32, "DEADBEEF");
my $dec = $vec->to_Dec();
如何从十进制转换为十六进制

使用 sprintf

my $hex = sprintf("%X", 3735928559); # upper case A-F
my $hex = sprintf("%x", 3735928559); # lower case a-f

使用 unpack

my $hex = unpack("H*", pack("N", 3735928559));

使用 Bit::Vector

use Bit::Vector;
my $vec = Bit::Vector->new_Dec(32, -559038737);
my $hex = $vec->to_Hex();

并且 Bit::Vector 支持奇数位计数

use Bit::Vector;
my $vec = Bit::Vector->new_Dec(33, 3735928559);
$vec->Resize(32); # suppress leading 0 if unwanted
my $hex = $vec->to_Hex();
如何从八进制转换为十进制

使用 Perl 内置的带有前导零的数字转换

my $dec = 033653337357; # note the leading 0!

使用 oct 函数

my $dec = oct("33653337357");

使用 Bit::Vector

use Bit::Vector;
my $vec = Bit::Vector->new(32);
$vec->Chunk_List_Store(3, split(//, reverse "33653337357"));
my $dec = $vec->to_Dec();
如何从十进制转换为八进制

使用 sprintf

my $oct = sprintf("%o", 3735928559);

使用 Bit::Vector

use Bit::Vector;
my $vec = Bit::Vector->new_Dec(32, -559038737);
my $oct = reverse join('', $vec->Chunk_List_Read(3));
如何从二进制转换为十进制

Perl 5.6 允许你使用 0b 符号直接编写二进制数字

my $number = 0b10110110;

使用 oct

my $input = "10110110";
my $decimal = oct( "0b$input" );

对较长的字符串使用 packord

my $decimal = ord(pack('B8', '10110110'));

对较长的字符串使用 packunpack

my $int = unpack("N", pack("B32",
substr("0" x 32 . "11110101011011011111011101111", -32)));
my $dec = sprintf("%d", $int);

# substr() is used to left-pad a 32-character string with zeros.

使用 Bit::Vector

my $vec = Bit::Vector->new_Bin(32, "11011110101011011011111011101111");
my $dec = $vec->to_Dec();
如何从十进制转换为二进制

使用 sprintf(perl 5.6+)

my $bin = sprintf("%b", 3735928559);

使用 unpack

my $bin = unpack("B*", pack("N", 3735928559));

使用 Bit::Vector

use Bit::Vector;
my $vec = Bit::Vector->new_Dec(32, -559038737);
my $bin = $vec->to_Bin();

剩余的转换(例如,hex -> oct、bin -> hex 等)留给有兴趣的读者练习。

为什么 & 不能按照我想要的方式工作?

二进制算术运算符的行为取决于它们是用于数字还是字符串。运算符将字符串视为一系列位并使用该字符串(字符串 "3" 是位模式 00110011)。运算符使用数字的二进制形式(数字 3 被视为位模式 00000011)。

因此,说 11 & 3 对数字执行“与”操作(产生 3)。说 "11" & "3" 对字符串执行“与”操作(产生 "1")。

&| 的大多数问题都出现,因为程序员认为他们有一个数字,但实际上它是一个字符串,反之亦然。为避免这种情况,请显式地将参数字符串化(使用 ""qq())或显式地将它们转换为数字(使用 0+$arg)。其余的出现是因为程序员说

if ("\020\020" & "\101\101") {
    # ...
}

但由两个空字节组成的字符串("\020\020" & "\101\101" 的结果)在 Perl 中不是一个假值。你需要

if ( ("\020\020" & "\101\101") !~ /[^\000]/) {
    # ...
}

如何相乘矩阵?

使用 Math::MatrixMath::MatrixReal 模块(可从 CPAN 获得)或 PDL 扩展(也可从 CPAN 获得)。

如何在一系列整数上执行操作?

要对数组中的每个元素调用函数并收集结果,请使用

my @results = map { my_func($_) } @array;

例如

my @triple = map { 3 * $_ } @single;

在数组的每个元素上调用一个函数,但忽略结果

foreach my $iterator (@array) {
    some_func($iterator);
}

在(小)范围内对每个整数调用一个函数,可以使用

my @results = map { some_func($_) } (5 .. 25);

但你应该知道,在这种形式下,.. 运算符会创建一个范围内所有整数的列表,对于大范围来说会占用大量内存。然而,在 for 循环中使用 .. 时不会出现此问题,因为在这种情况下,范围运算符被优化为在不创建整个列表的情况下迭代范围。所以

my @results = ();
for my $i (5 .. 500_005) {
    push(@results, some_func($i));
}

或者甚至

push(@results, some_func($_)) for 5 .. 500_005;

不会创建 500,000 个整数的中间列表。

如何输出罗马数字?

获取 http://www.cpan.org/modules/by-module/Roman 模块。

为什么我的随机数不是随机的?

如果你使用的是 5.004 之前的 Perl 版本,你必须在程序开始时调用 srand 一次,以播种随机数生成器。

BEGIN { srand() if $] < 5.004 }

5.004 及更高版本会在开始时自动调用 srand。不要多次调用 srand——你会让你的数字变得不那么随机,而不是更随机。

计算机擅长可预测,不擅长随机(尽管你的程序中的错误会造成相反的假象 :-)。http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz 中“远远超出你想要了解的”集合中的随机文章,由 Tom Phoenix 提供,对此进行了更多讨论。约翰·冯·诺依曼说,“任何试图通过确定性手段生成随机数的人,当然处于一种罪恶状态。”

Perl 依赖于底层系统来实现 randsrand;在某些系统上,生成的数字不够随机(尤其是在 Windows 上:参见 http://www.perlmonks.org/?node_id=803632)。Math 命名空间中的几个 CPAN 模块实现了更好的伪随机生成器;例如,参见 Math::Random::MT(“梅森旋转器”,速度快)或 Math::TrulyRandom(利用系统计时器的不完善来生成随机数,速度相当慢)。更多生成随机数的算法在 http://www.nr.com/ 上的“C 语言中的数值食谱”中进行了描述

如何获取 X 和 Y 之间的随机数?

要获取两个值之间的随机数,可以使用 rand() 内置函数来获取 0 和 1 之间的随机数。在此基础上,将其移至所需的范围内。

rand($x) 返回一个数字,使得 0 <= rand($x) < $x。因此,您希望 perl 计算出的是 XY 之间的差值范围内的随机数。

也就是说,要得到 10 到 15 之间的数字(包括 10 和 15),您需要一个 0 到 5 之间的随机数,然后可以将该随机数加到 10。

my $number = 10 + int rand( 15-10+1 ); # ( 10,11,12,13,14, or 15 )

因此,您导出以下简单函数来抽象它。它在给定的两个整数之间(包括这两个整数)选择一个随机整数。例如:random_int_between(50,120)

sub random_int_between {
    my($min, $max) = @_;
    # Assumes that the two arguments are integers themselves!
    return $min if $min == $max;
    ($min, $max) = ($max, $min)  if  $min > $max;
    return $min + int rand(1 + $max - $min);
}

数据:日期

如何查找一年中的某一天或某一周?

一年中的某一天在 localtime 函数返回的列表中。如果没有参数,localtime 使用当前时间。

my $day_of_year = (localtime)[7];

POSIX 模块还可以将日期格式化为一年中的某一天或某一周。

use POSIX qw/strftime/;
my $day_of_year  = strftime "%j", localtime;
my $week_of_year = strftime "%W", localtime;

要获取任何日期的一年中的某一天,请使用 POSIXmktime 来获取 localtime 参数的纪元秒。

use POSIX qw/mktime strftime/;
my $week_of_year = strftime "%W",
    localtime( mktime( 0, 0, 0, 18, 11, 87 ) );

您还可以使用 Time::Piece,它附带 Perl,并提供返回对象的 localtime

use Time::Piece;
my $day_of_year  = localtime->yday;
my $week_of_year = localtime->week;

Date::Calc 模块也提供了两个函数来计算这些

use Date::Calc;
my $day_of_year  = Day_of_Year(  1987, 12, 18 );
my $week_of_year = Week_of_Year( 1987, 12, 18 );

如何查找当前的世纪或千年?

使用以下简单函数

sub get_century    {
    return int((((localtime(shift || time))[5] + 1999))/100);
}

sub get_millennium {
    return 1+int((((localtime(shift || time))[5] + 1899))/1000);
}

在某些系统上,POSIX 模块的 strftime() 函数已以非标准方式扩展为使用 %C 格式,他们有时声称这是“世纪”。事实并非如此,因为在大多数此类系统上,这只是四位数年份的前两位数字,因此无法可靠地确定当前的世纪或千年。

如何比较两个日期并找出差异?

(由 brian d foy 贡献)

你可以将所有日期存储为数字,然后进行减法。但生活并不总是那么简单。

Perl 附带的 Time::Piece 模块用一个返回对象的版本替换了 localtime。它还重载了比较运算符,以便你可以直接比较它们

use Time::Piece;
my $date1 = localtime( $some_time );
my $date2 = localtime( $some_other_time );

if( $date1 < $date2 ) {
    print "The date was in the past\n";
}

你还可以通过减法获得差异,它返回一个 Time::Seconds 对象

my $date_diff = $date1 - $date2;
print "The difference is ", $date_diff->days, " days\n";

如果你想使用格式化的日期,Date::ManipDate::CalcDateTime 模块可以帮助你。

如何将字符串转换为纪元秒?

如果它是一个格式始终相同的常规字符串,你可以将其拆分并将部分传递给标准 Time::Local 模块中的 timelocal。否则,你应该查看 CPAN 中的 Date::CalcDate::ParseDate::Manip 模块。

如何查找儒略日?

(由 brian d foy 和 Dave Cross 贡献)

你可以使用 Time::Piece 模块,它是标准库的一部分,它可以将日期/时间转换为儒略日

$ perl -MTime::Piece -le 'print localtime->julian_day'
2455607.7959375

或修正儒略日

$ perl -MTime::Piece -le 'print localtime->mjd'
55607.2961226851

甚至可以转换为一年中的某天(有些人认为这就是儒略日)

$ perl -MTime::Piece -le 'print localtime->yday'
45

你还可以使用 DateTime 模块执行相同操作

$ perl -MDateTime -le'print DateTime->today->jd'
2453401.5
$ perl -MDateTime -le'print DateTime->today->mjd'
53401
$ perl -MDateTime -le'print DateTime->today->doy'
31

你可以使用 CPAN 上提供的 Time::JulianDay 模块。不过,请确保你真的想查找儒略日,因为许多人对儒略日有不同的看法(例如,请参见 http://www.hermetic.ch/cal_stud/jdn.htm

$  perl -MTime::JulianDay -le 'print local_julian_day( time )'
55608

如何查找昨天的日期?

(由 brian d foy 贡献)

要正确执行此操作,你可以使用其中一个 Date 模块,因为它们使用日历而不是时间。即使夏令时发生变化,DateTime 模块也能简化操作,并为你提供相同的时间,只是前一天

use DateTime;

my $yesterday = DateTime->now->subtract( days => 1 );

print "Yesterday was $yesterday\n";

你还可以使用 Date::Calc 模块及其 Today_and_Now 函数。

use Date::Calc qw( Today_and_Now Add_Delta_DHMS );

my @date_time = Add_Delta_DHMS( Today_and_Now(), -1, 0, 0, 0 );

print "@date_time\n";

大多数人尝试使用时间而不是日历来确定日期,但这假设每一天都是 24 小时。对于大多数人来说,一年中有两天不是这样:切换到夏令时和从夏令时切换回来会打乱这一点。例如,其他建议有时会出错

从 Perl 5.10 开始,Time::PieceTime::Seconds 是标准发行版的一部分,所以你可能会认为可以执行类似这样的操作

use Time::Piece;
use Time::Seconds;

my $yesterday = localtime() - ONE_DAY; # WRONG
print "Yesterday was $yesterday\n";

Time::Piece 模块导出一个新的 localtime,它返回一个对象,Time::Seconds 导出 ONE_DAY 常量,它是一个设置好的秒数。这意味着它总是给出 24 小时前的时间,但这并不总是昨天。当夏令时结束时,会出现一天 25 小时的情况,这可能会造成问题。

对于 Time::Local,您遇到了同样的问题,对于这些特殊情况,它会给出错误的答案

# contributed by Gunnar Hjalmarsson
 use Time::Local;
 my $today = timelocal 0, 0, 12, ( localtime )[3..5];
 my ($d, $m, $y) = ( localtime $today-86400 )[3..5]; # WRONG
 printf "Yesterday: %d-%02d-%02d\n", $y+1900, $m+1, $d;

Perl 是否具有 2000 年或 2038 年问题?Perl 是否符合 Y2K 标准?

(由 brian d foy 贡献)

Perl 本身从未出现过 Y2K 问题,尽管这从未阻止人们自行制造 Y2K 问题。请参阅 localtime 的文档以了解其正确用法。

从 Perl 5.12 开始,localtimegmtime 可以处理 2038 年 1 月 19 日 03:14:08 之后的时间,那时基于 32 位的时间将溢出。您仍然可能会在 32 位 perl 上收到警告

% perl5.12 -E 'say scalar localtime( 0x9FFF_FFFFFFFF )'
Integer overflow in hexadecimal number at -e line 1.
Wed Nov  1 19:42:39 5576711

在 64 位 perl 上,对于那些真正长期运行的项目,您可以获得更大的日期

% perl5.12 -E 'say scalar gmtime( 0x9FFF_FFFFFFFF )'
Thu Nov  2 00:42:39 5576711

不过,如果您需要跟踪衰变质子,您仍然会很倒霉。

数据:字符串

如何验证输入?

(由 brian d foy 贡献)

有许多方法可以确保值符合您的预期或您希望接受的值。除了我们在 perlfaq 中介绍的具体示例之外,您还可以查看名称中包含“Assert”和“Validate”的模块,以及其他模块,例如 Regexp::Common

某些模块对特定类型的输入进行验证,例如 Business::ISBNBusiness::CreditCardEmail::ValidData::Validate::IP

如何取消转义字符串?

这取决于您所说的“转义”的含义。URL 转义在 perlfaq9 中进行了处理。使用反斜杠 (\) 字符进行的 Shell 转义可通过以下方式删除

s/\\(.)/$1/g;

这不会展开 "\n""\t" 或任何其他特殊转义。

如何删除连续成对的字符?

(由 brian d foy 贡献)

您可以使用替换运算符查找成对的字符(或连续的字符)并用单个实例替换它们。在此替换中,我们在 (.) 中找到一个字符。内存括号将匹配的字符存储在反向引用 \g1 中,我们使用它来要求相同的内容紧跟其后。我们用 $1 中的字符替换字符串的那一部分。

s/(.)\g1/$1/g;

我们还可以使用转写运算符 tr///。在此示例中,tr/// 的搜索列表一侧没有任何内容,但 c 选项对其进行了补充,因此它包含所有内容。替换列表也不包含任何内容,因此转写几乎是无操作的,因为它不会执行任何替换(或更准确地说,用字符本身替换字符)。但是,s 选项会压缩字符串中重复和连续的字符,因此字符不会出现在其自身旁边

my $str = 'Haarlem';   # in the Netherlands
$str =~ tr///cs;       # Now Harlem, like in New York

如何在字符串中扩展函数调用?

(由 brian d foy 贡献)

这在 perlref 中有记录,虽然它不是最容易阅读的东西,但它确实有效。在这些示例中的每一个中,我们都在用于取消引用引用的花括号内调用函数。如果我们有多个返回值,我们可以构造并取消引用一个匿名数组。在这种情况下,我们在列表上下文中调用函数。

print "The time values are @{ [localtime] }.\n";

如果我们想在标量上下文中调用函数,我们必须做更多的工作。我们真的可以在花括号内放入我们喜欢的任何代码,所以我们只需要以标量引用结尾,尽管如何做到这一点取决于你,你可以在花括号内使用代码。请注意,使用括号会创建一个列表上下文,所以我们需要 scalar 来强制对函数使用标量上下文

print "The time is ${\(scalar localtime)}.\n"

print "The time is ${ my $x = localtime; \$x }.\n";

如果你的函数已经返回了一个引用,你就不需要自己创建引用。

sub timestamp { my $t = localtime; \$t }

print "The time is ${ timestamp() }.\n";

Interpolation 模块也可以为你做很多神奇的事情。你可以指定一个变量名,在本例中为 E,以设置一个为你执行插值的关联哈希。它还有其他几种方法来做到这一点。

use Interpolation E => 'eval';
print "The time values are $E{localtime()}.\n";

在大多数情况下,可能更容易简单地使用字符串连接,它也会强制使用标量上下文。

print "The time is " . localtime() . ".\n";

如何查找匹配/嵌套任何内容?

要查找两个单字符之间的内容,/x([^x]*)x/ 这样的模式将在 $1 中获取中间位。对于多个,则需要类似于 /alpha(.*?)omega/ 的内容。对于嵌套模式和/或平衡表达式,请参阅所谓的 (?PARNO) 构造(自 perl 5.10 起可用)。CPAN 模块 Regexp::Common 可以帮助构建此类正则表达式(特别是 Regexp::Common::balancedRegexp::Common::delimited)。

更复杂的情况需要编写一个解析器,可能使用 CPAN 中的解析模块,如 Regexp::GrammarsParse::RecDescentParse::YappText::BalancedMarpa::R2

如何反转字符串?

在标量上下文中使用 reverse(),如 "reverse" in perlfunc 中所述。

my $reversed = reverse $string;

如何在字符串中扩展制表符?

你可以自己做

1 while $string =~ s/\t+/' ' x (length($&) * 8 - length($`) % 8)/e;

或者你只需使用 Text::Tabs 模块(标准 Perl 发行版的一部分)。

use Text::Tabs;
my @expanded_lines = expand(@lines_with_tabs);

如何重新格式化一个段落?

使用 Text::Wrap(标准 Perl 分发的一部分)

use Text::Wrap;
print wrap("\t", '  ', @paragraphs);

您提供给 Text::Wrap 的段落不应包含嵌入的换行符。 Text::Wrap 不会对齐行(右对齐)。

或使用 CPAN 模块 Text::Autoformat。可以通过创建 shell 别名轻松地格式化文件,如下所示

alias fmt="perl -i -MText::Autoformat -n0777 \
    -e 'print autoformat $_, {all=>1}' $*"

请参阅 Text::Autoformat 的文档,以了解其众多功能。

如何访问或更改字符串的 N 个字符?

您可以使用 substr() 访问字符串的第一个字符。例如,要获取第一个字符,请从位置 0 开始并获取长度为 1 的字符串。

my $string = "Just another Perl Hacker";
my $first_char = substr( $string, 0, 1 );  #  'J'

要更改字符串的一部分,可以使用可选的第四个参数,该参数是替换字符串。

substr( $string, 13, 4, "Perl 5.8.0" );

您还可以将 substr() 用作左值。

substr( $string, 13, 4 ) =  "Perl 5.8.0";

如何更改某项的第 N 次出现?

您必须自己跟踪 N。例如,假设您想将“whoever”或“whomever”的第五次出现更改为“whosoever”或“whomsoever”,不区分大小写。这些都假定 $_ 包含要更改的字符串。

$count = 0;
s{((whom?)ever)}{
++$count == 5       # is it the 5th?
    ? "${2}soever"  # yes, swap
    : $1            # renege and leave it there
    }ige;

在更一般的情况下,您可以在 while 循环中使用 /g 修饰符,并记录匹配次数。

$WANT = 3;
$count = 0;
$_ = "One fish two fish red fish blue fish";
while (/(\w+)\s+fish\b/gi) {
    if (++$count == $WANT) {
        print "The third fish is a $1 one.\n";
    }
}

它打印出:“第三条鱼是红色的。”您还可以使用重复计数和重复模式,如下所示

/(?:\w+\s+fish\s+){2}(\w+)\s+fish/i;

如何在字符串中计算子字符串出现的次数?

有多种方法,效率各不相同。如果您想计算字符串中某个单个字符 (X) 的计数,可以使用 tr/// 函数,如下所示

my $string = "ThisXlineXhasXsomeXx'sXinXit";
my $count = ($string =~ tr/X//);
print "There are $count X characters in the string";

如果您只是在寻找单个字符,这很好。但是,如果您尝试在较长的字符串中计算多个字符子字符串,则 tr/// 将不起作用。您可以执行的操作是围绕全局模式匹配包装一个 while() 循环。例如,让我们计算负整数

my $string = "-9 55 48 -2 23 -76 4 14 -44";
my $count = 0;
while ($string =~ /-\d+/g) { $count++ }
print "There are $count negative numbers in the string";

另一个版本在列表上下文中使用全局匹配,然后将结果分配给标量,生成匹配次数的计数。

my $count = () = $string =~ /-\d+/g;

如何在单行上将所有单词大写?

(由 brian d foy 贡献)

Damian Conway 的 Text::Autoformat 为您处理所有思考。

use Text::Autoformat;
my $x = "Dr. Strangelove or: How I Learned to Stop ".
  "Worrying and Love the Bomb";

print $x, "\n";
for my $style (qw( sentence title highlight )) {
    print autoformat($x, { case => $style }), "\n";
}

您想如何将这些单词大写?

FRED AND BARNEY'S LODGE        # all uppercase
Fred And Barney's Lodge        # title case
Fred and Barney's Lodge        # highlight case

这不是看起来那么容易的问题。您认为那里有多少个单词?等等...等等....如果你回答 5,你就对了。Perl 单词是 \w+ 的组,但这不是您想要大写的。Perl 如何知道不要在大写撇号后的 s?您可以尝试正则表达式

$string =~ s/ (
             (^\w)    #at the beginning of the line
               |      # or
             (\s\w)   #preceded by whitespace
               )
            /\U$1/xg;

$string =~ s/([\w']+)/\u\L$1/g;

现在,如果你不想将“and”大写呢?只需使用 Text::Autoformat,然后继续解决下一个问题即可。 :)

如何在 [character] 内部除外的情况下,分割一个以 [character] 分隔的字符串?

有几个模块可以处理这种类型的解析,包括 Text::BalancedText::CSVText::CSV_XSText::ParseWords

以尝试将一个逗号分隔的字符串分割成不同字段的示例案例为例。你不能使用 split(/,/),因为逗号在引号内时不应分割。例如,取如下数据行

SAR001,"","Cimetrix, Inc","Bob Smith","CAM",N,8,1,0,7,"Error, Core Dumped"

由于引号的限制,这是一个相当复杂的问题。谢天谢地,我们有《精通正则表达式》的作者 Jeffrey Friedl 为我们处理这些问题。他建议(假设你的字符串包含在 $text 中)

my @new = ();
push(@new, $+) while $text =~ m{
    "([^\"\\]*(?:\\.[^\"\\]*)*)",? # groups the phrase inside the quotes
   | ([^,]+),?
   | ,
}gx;
push(@new, undef) if substr($text,-1,1) eq ',';

如果你想表示引号分隔字段内的引号,请使用反斜杠对其进行转义(例如,"like \"this\""

或者,Text::ParseWords 模块(标准 Perl 发行版的一部分)允许你这样说

use Text::ParseWords;
@new = quotewords(",", 0, $text);

但是,对于解析或生成 CSV,强烈建议使用 Text::CSV 而不是自己实现它;通过使用多年来在生产中已经过试用和测试的代码,你可以避免以后出现奇怪的错误。

如何从字符串的开头/结尾删除空白符?

(由 brian d foy 贡献)

替换可以为你完成此操作。对于单行,你希望用空值替换所有前导或尾随空格。你可以使用一对替换来实现

s/^\s+//;
s/\s+$//;

你也可以将其写为单个替换,尽管事实证明组合语句比单独语句慢。不过,这对你来说可能并不重要

s/^\s+|\s+$//g;

在此正则表达式中,交替匹配字符串的开头或结尾,因为锚的优先级低于交替。使用 /g 标志,替换会进行所有可能的匹配,因此它会同时获得两者。请记住,尾随换行符与 \s+ 匹配,并且 $ 锚可以与字符串的绝对结尾匹配,因此换行符也会消失。只需将换行符添加到输出中,这具有保留“空白”(完全由空格组成)行的额外好处,而 ^\s+ 本身会删除所有这些行

while( <> ) {
    s/^\s+|\s+$//g;
    print "$_\n";
}

对于多行字符串,可以通过添加 /m 标志(表示“多行”)将正则表达式应用于字符串中的每行逻辑行。使用 /m 标志时,$ 匹配嵌入换行符之前的内容,因此不会将其移除。此模式仍会移除字符串末尾的换行符

$string =~ s/^\s+|\s+$//gm;

请记住,由空白字符组成的行将消失,因为交替的第一部分可以匹配整个字符串并用空内容替换它。如果您需要保留嵌入的空行,则必须多做一些工作。不要匹配任何空白字符(因为它包括换行符),只需匹配其他空白字符

$string =~ s/^[\t\f ]+|[\t\f ]+$//mg;

如何用空白字符填充字符串或用零填充数字?

在以下示例中,$pad_len 是您希望填充字符串的长度,$text$num 包含要填充的字符串,$pad_char 包含填充字符。如果您事先知道 $pad_char 变量,则可以使用单个字符字符串常量代替它。同样,如果您事先知道填充长度,则可以使用整数代替 $pad_len

最简单的方法是使用 sprintf 函数。它可以在左侧或右侧使用空白字符填充,在左侧使用零填充,并且不会截断结果。pack 函数只能在右侧使用空白字符填充字符串,并且会将结果截断为 $pad_len 的最大长度。

# Left padding a string with blanks (no truncation):
my $padded = sprintf("%${pad_len}s", $text);
my $padded = sprintf("%*s", $pad_len, $text);  # same thing

# Right padding a string with blanks (no truncation):
my $padded = sprintf("%-${pad_len}s", $text);
my $padded = sprintf("%-*s", $pad_len, $text); # same thing

# Left padding a number with 0 (no truncation):
my $padded = sprintf("%0${pad_len}d", $num);
my $padded = sprintf("%0*d", $pad_len, $num); # same thing

# Right padding a string with blanks using pack (will truncate):
my $padded = pack("A$pad_len",$text);

如果您需要使用空白字符或零以外的字符填充,则可以使用以下方法之一。它们都使用 x 运算符生成填充字符串,并将其与 $text 结合使用。这些方法不会截断 $text

使用任何字符在左侧和右侧填充,创建一个新字符串

my $padded = $pad_char x ( $pad_len - length( $text ) ) . $text;
my $padded = $text . $pad_char x ( $pad_len - length( $text ) );

使用任何字符在左侧和右侧填充,直接修改 $text

substr( $text, 0, 0 ) = $pad_char x ( $pad_len - length( $text ) );
$text .= $pad_char x ( $pad_len - length( $text ) );

如何从字符串中提取选定的列?

(由 brian d foy 贡献)

如果您知道包含数据的列,则可以使用 substr 提取单个列。

my $column = substr( $line, $start_column, $length );

如果列由空白字符或其他分隔符分隔,则可以使用 split,只要空白字符或分隔符不会作为数据的一部分出现即可。

my $line    = ' fred barney   betty   ';
my @columns = split /\s+/, $line;
    # ( '', 'fred', 'barney', 'betty' );

my $line    = 'fred||barney||betty';
my @columns = split /\|/, $line;
    # ( 'fred', '', 'barney', '', 'betty' );

如果您想处理逗号分隔值,请不要这样做,因为该格式有点复杂。使用处理该格式的模块之一,例如 Text::CSVText::CSV_XSText::CSV_PP

如果您想拆分一整行固定列,可以使用 A(ASCII)格式的 unpack。通过在格式说明符后使用一个数字,您可以表示列宽。有关更多详细信息,请参阅 perlfunc 中的 packunpack 条目。

my @fields = unpack( $line, "A8 A8 A8 A16 A4" );

请注意,unpack 格式参数中的空格并不表示实际空格。如果您有空格分隔的数据,您可能需要使用 split

如何查找字符串的音标值?

(由 brian d foy 贡献)

您可以使用 Text::Soundex 模块。如果您想进行模糊或近似匹配,您还可以尝试 String::ApproxText::MetaphoneText::DoubleMetaphone 模块。

如何在文本字符串中展开变量?

(由 brian d foy 贡献)

如果您能避免,就不要这样做,或者如果您能使用模板系统,例如 Text::TemplateTemplate Toolkit,那就这样做。您甚至可以使用 sprintfprintf 完成这项工作

my $string = sprintf 'Say hello to %s and %s', $foo, $bar;

但是,对于我不想提取完整模板系统的简单情况,我将使用一个包含两个 Perl 标量变量的字符串。在此示例中,我想将 $foo$bar 展开为其变量值

my $foo = 'Fred';
my $bar = 'Barney';
$string = 'Say hello to $foo and $bar';

我可以执行此操作的一种方法涉及替换运算符和双重 /e 标志。第一个 /e 在替换方评估 $1 并将其转换为 $foo。第二个 /e 以 $foo 开头,并将其替换为其值。然后,$foo 变成了“Fred”,最后这就是字符串中剩下的内容

$string =~ s/(\$\w+)/$1/eeg; # 'Say hello to Fred and Barney'

/e 还将静默忽略严格违规,用空字符串替换未定义的变量名。由于我正在使用 /e 标志(甚至两次!),因此我遇到了与字符串形式的 eval 相同的所有安全问题。如果 $foo 中有什么奇怪的东西,比如 @{[ system "rm -rf /" ]},那么我可能会遇到麻烦。

为了解决安全问题,我还可以从哈希中提取值,而不是评估变量名。使用单个 /e,我可以检查哈希以确保值存在,如果不存在,我可以将缺失值替换为标记,在本例中为 ???,以表示我错过了某些内容

my $string = 'This has $foo and $bar';

my %Replacements = (
    foo  => 'Fred',
    );

# $string =~ s/\$(\w+)/$Replacements{$1}/g;
$string =~ s/\$(\w+)/
    exists $Replacements{$1} ? $Replacements{$1} : '???'
    /eg;

print $string;

Perl 是否具有类似于 Ruby 的 #{} 或 Python 的 f 字符串?

与其他语言不同,Perl 允许你在双引号字符串中裸嵌入变量,例如 "variable $variable"。当变量名称后面没有空格或其他非单词字符时,你可以添加大括号(例如 "foo ${foo}bar")以确保正确解析。

数组也可以直接嵌入字符串中,并且默认情况下会在元素之间用空格展开。默认的 LIST_SEPARATOR 可以通过将不同的字符串分配给特殊变量 $" 来更改,例如 local $" = ', ';

Perl 还支持字符串中的引用,提供与其他两种语言中的特性等效的功能。

嵌入在字符串中的 ${\ ... } 将适用于大多数简单语句,例如对象->方法调用。更复杂的代码可以包装在 do 块 ${\ do{...} } 中。

当你想让列表按 $" 展开时,请使用 @{[ ... ]}

use Time::Piece;
use Time::Seconds;
my $scalar = 'STRING';
my @array = ( 'zorro', 'a', 1, 'B', 3 );

# Print the current date and time and then Tommorrow
my $t = Time::Piece->new;
say "Now is: ${\ $t->cdate() }";
say "Tomorrow: ${\ do{ my $T=Time::Piece->new + ONE_DAY ; $T->fullday }}";

# some variables in strings
say "This is some scalar I have $scalar, this is an array @array.";
say "You can also write it like this ${scalar} @{array}.";

# Change the $LIST_SEPARATOR
local $" = ':';
say "Set \$\" to delimit with ':' and sort the Array @{[ sort @array ]}";

你可能还想查看模块 Quote::Code,以及模板工具,例如 Template::ToolkitMojo::Template

另请参阅:"如何在文本字符串中展开变量?""如何在字符串中展开函数调用?" 中的此常见问题解答。

始终引用 "$vars" 有什么问题?

问题在于那些双引号强制字符串化——将数字和引用强制转换为字符串——即使你不想将它们转换为字符串。可以这样理解:双引号展开用于生成新字符串。如果你已经有一个字符串,为什么还需要更多?

如果你习惯于编写像这样的奇怪内容

print "$var";       # BAD
my $new = "$old";       # BAD
somefunc("$var");    # BAD

你将遇到麻烦。在 99.8% 的情况下,它们应该是更简单、更直接的

print $var;
my $new = $old;
somefunc($var);

否则,除了减慢你的速度之外,当标量中的内容实际上既不是字符串也不是数字,而是一个引用时,你将破坏代码

func(\@array);
sub func {
    my $aref = shift;
    my $oref = "$aref";  # WRONG
}

你还可以遇到 Perl 中少数几个实际关心字符串和数字之间差异的操作的微妙问题,例如神奇的 ++ 自动增量运算符或 syscall() 函数。

字符串化还会破坏数组。

my @lines = `command`;
print "@lines";     # WRONG - extra blanks
print @lines;       # right

为什么我的 <<HERE 文档不起作用?

可以在 perlop 中找到 Here 文档。检查以下三件事

<< 部分后面不能有空格。
(可能)应在起始令牌的末尾添加分号
(轻松)无法在标记前添加任何空格。
结束令牌后至少需要一个换行符。

如果您想缩进此处文档中的文本,可以执行以下操作

# all in one
(my $VAR = <<HERE_TARGET) =~ s/^\s+//gm;
    your text
    goes here
HERE_TARGET

但是 HERE_TARGET 仍然必须与边距齐平。如果您也想缩进该边距,则必须在缩进中引用。

(my $quote = <<'    FINIS') =~ s/^\s+//gm;
        ...we will have peace, when you and all your works have
        perished--and the works of your dark master to whom you
        would deliver us. You are a liar, Saruman, and a corrupter
        of men's hearts. --Theoden in /usr/src/perl/taint.c
    FINIS
$quote =~ s/\s+--/\n--/;

下面是一个适用于缩进此处文档的通用修复程序函数。它希望使用此处文档作为其参数进行调用。它会查看每行是否以一个通用子字符串开头,如果是,则会去掉该子字符串。否则,它会获取第一行上找到的前导空格量,并从每后续行中删除该空格量。

sub fix {
    local $_ = shift;
    my ($white, $leader);  # common whitespace and common leading string
    if (/^\s*(?:([^\w\s]+)(\s*).*\n)(?:\s*\g1\g2?.*\n)+$/) {
        ($white, $leader) = ($2, quotemeta($1));
    } else {
        ($white, $leader) = (/^(\s+)/, '');
    }
    s/^\s*?$leader(?:$white)?//gm;
    return $_;
}

它适用于动态确定的前导特殊字符串

my $remember_the_main = fix<<'    MAIN_INTERPRETER_LOOP';
@@@ int
@@@ runops() {
@@@     SAVEI32(runlevel);
@@@     runlevel++;
@@@     while ( op = (*op->op_ppaddr)() );
@@@     TAINT_NOT;
@@@     return 0;
@@@ }
MAIN_INTERPRETER_LOOP

或者适用于固定数量的前导空格,同时正确保留剩余缩进

my $poem = fix<<EVER_ON_AND_ON;
   Now far ahead the Road has gone,
  And I must follow, if I can,
   Pursuing it with eager feet,
  Until it joins some larger way
   Where many paths and errands meet.
  And whither then? I cannot say.
    --Bilbo in /usr/src/perl/pp_ctl.c
EVER_ON_AND_ON

从 Perl 版本 5.26 开始,该语言中添加了一种编写缩进此处文档的更简单、更简洁的方法:波浪号 (~) 修饰符。有关详细信息,请参阅perlop 中的“缩进此处文档”

数据:数组

列表和数组之间有什么区别?

(由 brian d foy 贡献)

列表是标量的一个固定集合。数组是一个变量,它包含标量的可变集合。数组可以为列表操作提供其集合,因此列表操作也适用于数组

# slices
( 'dog', 'cat', 'bird' )[2,3];
@animals[2,3];

# iteration
foreach ( qw( dog cat bird ) ) { ... }
foreach ( @animals ) { ... }

my @three = grep { length == 3 } qw( dog cat bird );
my @three = grep { length == 3 } @animals;

# supply an argument list
wash_animals( qw( dog cat bird ) );
wash_animals( @animals );

数组操作(更改标量、重新排列标量或添加或减去一些标量)仅适用于数组。这些操作无法用于列表,因为列表是固定的。数组操作包括 shiftunshiftpushpopsplice

数组还可以更改其长度

$#animals = 1;  # truncate to two elements
$#animals = 10000; # pre-extend to 10,001 elements

您可以更改数组元素,但不能更改列表元素

$animals[0] = 'Rottweiler';
qw( dog cat bird )[0] = 'Rottweiler'; # syntax error!

foreach ( @animals ) {
    s/^d/fr/;  # works fine
}

foreach ( qw( dog cat bird ) ) {
    s/^d/fr/;  # Error! Modification of read only value!
}

但是,如果列表元素本身是一个变量,则表明您可以更改列表元素。但是,列表元素是变量,而不是数据。您不是在更改列表元素,而是在更改列表元素引用的内容。列表元素本身不会更改:它仍然是同一个变量。

您还必须注意上下文。您可以将数组分配给标量以获取数组中的元素数量。不过,这仅适用于数组

my $count = @animals;  # only works with arrays

如果您尝试对您认为是列表的内容执行相同操作,则会得到完全不同的结果。虽然在右侧看起来您有一个列表,但 Perl 实际上看到的是一堆用逗号分隔的标量

my $scalar = ( 'dog', 'cat', 'bird' );  # $scalar gets bird

由于您正在分配给标量,因此右侧处于标量上下文中。标量上下文中的逗号运算符(是的,它是一个运算符!)求值其左侧,丢弃结果,求值其右侧并返回结果。实际上,该类列表将最右侧的值分配给$scalar。许多人会搞砸,因为他们选择最后一个元素也是他们期望计数的类列表

my $scalar = ( 1, 2, 3 );  # $scalar gets 3, accidentally

$array[1]和@array[1]有什么区别?

(由 brian d foy 贡献)

区别在于符号,数组名称前面的特殊字符。$符号表示“恰好一个项目”,而@符号表示“零个或多个项目”。$获得单个标量,而@获得列表。

困惑的原因是人们错误地认为符号表示变量类型。

$array[1]是对数组的单元素访问。它将返回索引 1 中的项目(如果没有项目,则返回未定义)。如果您打算从数组中获取恰好一个元素,则应使用此表单。

@array[1]是数组切片,尽管它只有一个索引。您可以通过指定其他索引作为列表(如@array[1,4,3,0])同时提取多个元素。

在赋值的左侧使用切片会向右侧提供列表上下文。这可能导致意外结果。例如,如果您想从文件句柄中读取一行,则分配给标量值就可以了

$array[1] = <STDIN>;

但是,在列表上下文中,行输入运算符将所有行作为列表返回。第一行进入@array[1],其余行神秘地消失了

@array[1] = <STDIN>;  # most likely not what you want

use warnings pragma 或-w标志会在您使用具有单个索引的数组切片时发出警告。

如何从列表或数组中删除重复元素?

(由 brian d foy 贡献)

使用哈希。当您想到“唯一”或“重复”一词时,请想到“哈希键”。

如果您不关心元素的顺序,则可以创建哈希,然后提取键。创建该哈希的方式并不重要:只需使用keys获取唯一元素即可。

my %hash   = map { $_, 1 } @array;
# or a hash slice: @hash{ @array } = ();
# or a foreach: $hash{$_} = 1 foreach ( @array );

my @unique = keys %hash;

如果您想使用模块,请尝试使用 List::MoreUtils 中的uniq 函数。在列表上下文中,它返回唯一元素,保留它们在列表中的顺序。在标量上下文中,它返回唯一元素的数量。

use List::MoreUtils qw(uniq);

my @unique = uniq( 1, 2, 3, 4, 4, 5, 6, 5, 7 ); # 1,2,3,4,5,6,7
my $unique = uniq( 1, 2, 3, 4, 4, 5, 6, 5, 7 ); # 7

您还可以遍历每个元素并跳过之前已看到过的元素。使用哈希来进行跟踪。循环第一次看到一个元素时,该元素在 %Seen 中没有键。next 语句创建键并立即使用其值,该值为 undef,因此循环继续到 push 并增加该键的值。循环下次看到该元素时,其键存在于哈希中该键的值为真(因为它不是 0 或 undef),因此 next 跳过该迭代,循环转到下一个元素。

my @unique = ();
my %seen   = ();

foreach my $elem ( @array ) {
    next if $seen{ $elem }++;
    push @unique, $elem;
}

您可以使用 grep 更简洁地编写此内容,它执行相同操作。

my %seen = ();
my @unique = grep { ! $seen{ $_ }++ } @array;

如何判断某个元素是否包含在列表或数组中?

(本答案的部分内容由 Anno Siegel 和 brian d foy 提供)

听到“in”一词表明您可能应该使用哈希而不是列表或数组来存储数据。哈希旨在快速有效地回答此问题。数组则不然。

话虽如此,有几种方法可以解决这个问题。如果您要多次对任意字符串值进行此查询,最快的办法可能是反转原始数组并维护一个哈希,其键是第一个数组的值

my @blues = qw/azure cerulean teal turquoise lapis-lazuli/;
my %is_blue = ();
for (@blues) { $is_blue{$_} = 1 }

现在,您可以检查 $is_blue{$some_color}。一开始就将蓝色全部保存在哈希中可能是个好主意。

如果值都是小整数,则可以使用简单的索引数组。这种数组将占用更少的空间

my @primes = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31);
my @is_tiny_prime = ();
for (@primes) { $is_tiny_prime[$_] = 1 }
# or simply  @istiny_prime[@primes] = (1) x @primes;

现在,您检查 $is_tiny_prime[$some_number]。

如果相关值是整数而不是字符串,则可以使用位字符串来节省大量空间

my @articles = ( 1..10, 150..2000, 2017 );
undef $read;
for (@articles) { vec($read,$_,1) = 1 }

现在检查 vec($read,$n,1) 是否对某些 $n 为真。

这些方法保证了快速的个体测试,但需要重新组织原始列表或数组。只有当您必须针对同一数组测试多个值时,它们才有效。

如果您只测试一次,标准模块 List::Util 会为此目的导出函数 any。它通过在找到元素后停止来工作。它用 C 编写以提高速度,其 Perl 等效项看起来像此子例程

sub any (&@) {
    my $code = shift;
    foreach (@_) {
        return 1 if $code->();
    }
    return 0;
}

如果速度无关紧要,则常用惯用法在标量上下文中使用 grep(它返回通过其条件的项目数)来遍历整个列表。不过,这样做的好处是可以告诉您它找到了多少个匹配项。

my $is_there = grep $_ eq $whatever, @array;

如果您想实际提取匹配元素,只需在列表上下文中使用 grep。

my @matches = grep $_ eq $whatever, @array;

如何计算两个数组的差值?如何计算两个数组的交集?

使用哈希。以下代码可以执行这两项操作以及更多操作。它假定每个元素在给定数组中都是唯一的

my (@union, @intersection, @difference);
my %count = ();
foreach my $element (@array1, @array2) { $count{$element}++ }
foreach my $element (keys %count) {
    push @union, $element;
    push @{ $count{$element} > 1 ? \@intersection : \@difference }, $element;
}

请注意,这是对称差,也就是说,在 A 或 B 中但不在两者中同时存在的元素。把它想象成异或运算。

如何测试两个数组或哈希是否相等?

以下代码适用于单级数组。它使用字符串比较,不区分已定义和未定义的空字符串。如果您有其他需求,请修改。

$are_equal = compare_arrays(\@frogs, \@toads);

sub compare_arrays {
    my ($first, $second) = @_;
    no warnings;  # silence spurious -w undef complaints
    return 0 unless @$first == @$second;
    for (my $i = 0; $i < @$first; $i++) {
        return 0 if $first->[$i] ne $second->[$i];
    }
    return 1;
}

对于多级结构,您可能希望使用更类似于此方法的方法。它使用 CPAN 模块 FreezeThaw

use FreezeThaw qw(cmpStr);
my @a = my @b = ( "this", "that", [ "more", "stuff" ] );

printf "a and b contain %s arrays\n",
    cmpStr(\@a, \@b) == 0
    ? "the same"
    : "different";

此方法也适用于比较哈希。这里我们将演示两个不同的答案

use FreezeThaw qw(cmpStr cmpStrHard);

my %a = my %b = ( "this" => "that", "extra" => [ "more", "stuff" ] );
$a{EXTRA} = \%b;
$b{EXTRA} = \%a;

printf "a and b contain %s hashes\n",
cmpStr(\%a, \%b) == 0 ? "the same" : "different";

printf "a and b contain %s hashes\n",
cmpStrHard(\%a, \%b) == 0 ? "the same" : "different";

第一个报告说这两个哈希都包含相同的数据,而第二个报告说它们不包含。您更喜欢哪一个留给读者练习。

如何查找第一个满足条件的数组元素?

要查找满足条件的第一个数组元素,可以使用 List::Util 模块中的 first() 函数,该模块随 Perl 5.8 一起提供。此示例查找包含“Perl”的第一个元素。

use List::Util qw(first);

my $element = first { /Perl/ } @array;

如果您不能使用 List::Util,您可以自己创建一个循环来执行相同操作。一旦找到该元素,您就可以使用 last 停止循环。

my $found;
foreach ( @array ) {
    if( /Perl/ ) { $found = $_; last }
}

如果您想要数组索引,请使用 List::MoreUtils 中的 firstidx() 函数

use List::MoreUtils qw(firstidx);
my $index = firstidx { /Perl/ } @array;

或者自己编写,遍历索引并检查每个索引处的数组元素,直到找到满足条件的元素

my( $found, $index ) = ( undef, -1 );
for( $i = 0; $i < @array; $i++ ) {
    if( $array[$i] =~ /Perl/ ) {
        $found = $array[$i];
        $index = $i;
        last;
    }
}

如何处理链表?

(由 brian d foy 贡献)

Perl 的数组没有固定大小,因此如果您只想添加或删除项目,则不需要链表。您可以使用数组操作(例如 pushpopshiftunshiftsplice)来执行此操作。

然而,有时链表在您希望“分片”数组的情况下很有用,这样您就有许多小数组而不是一个大数组。您可以保留比 Perl 最大数组索引更长的数组,分别在多线程程序中锁定较小的数组,重新分配较少的内存,或快速插入链中间的元素。

Steve Lembark 在他的 YAPC::NA 2009 演讲“Perly Linked Lists”中详细介绍了这些细节( http://www.slideshare.net/lembark/perly-linked-lists ),尽管您只需使用他的 LinkedList::Single 模块即可。

如何处理循环列表?

(由 brian d foy 贡献)

如果您希望循环遍历一个数组,则可以将索引递增,模数组中的元素数

my @array = qw( a b c );
my $i = 0;

while( 1 ) {
    print $array[ $i++ % @array ], "\n";
    last if $i > 20;
}

您还可以使用 Tie::Cycle 来使用标量,该标量始终具有循环数组的下一个元素

use Tie::Cycle;

tie my $cycle, 'Tie::Cycle', [ qw( FFFFFF 000000 FFFF00 ) ];

print $cycle; # FFFFFF
print $cycle; # 000000
print $cycle; # FFFF00

Array::Iterator::Circular 为循环数组创建迭代器对象

use Array::Iterator::Circular;

my $color_iterator = Array::Iterator::Circular->new(
    qw(red green blue orange)
    );

foreach ( 1 .. 20 ) {
    print $color_iterator->next, "\n";
}

如何随机排列一个数组?

如果您已安装 Perl 5.8.0 或更高版本,或者已安装 Scalar-List-Utils 1.03 或更高版本,则可以这么说

use List::Util 'shuffle';

@shuffled = shuffle(@list);

如果没有,则可以使用 Fisher-Yates 随机置换。

sub fisher_yates_shuffle {
    my $deck = shift;  # $deck is a reference to an array
    return unless @$deck; # must not be empty!

    my $i = @$deck;
    while (--$i) {
        my $j = int rand ($i+1);
        @$deck[$i,$j] = @$deck[$j,$i];
    }
}

# shuffle my mpeg collection
#
my @mpeg = <audio/*/*.mp3>;
fisher_yates_shuffle( \@mpeg );    # randomize @mpeg in place
print @mpeg;

请注意,与 List::Util::shuffle()(它采用一个列表并返回一个新的已随机置换的列表)不同,上述实现会就地随机置换一个数组。

您可能已经看到使用 splice 来工作的随机置换算法,该算法会随机选择另一个元素来与当前元素交换

srand;
@new = ();
@old = 1 .. 10;  # just a demo
while (@old) {
    push(@new, splice(@old, rand @old, 1));
}

这是不好的,因为 splice 已经是 O(N),并且由于您执行了 N 次,所以您刚刚发明了一个二次算法;即 O(N**2)。这不会扩展,尽管 Perl 非常高效,但您可能在拥有相当大的数组之前不会注意到这一点。

如何处理/修改数组的每个元素?

使用 for/foreach

for (@lines) {
    s/foo/bar/;    # change that word
    tr/XZ/ZX/;    # swap those letters
}

这是另一个;让我们计算球体积

my @volumes = @radii;
for (@volumes) {   # @volumes has changed parts
    $_ **= 3;
    $_ *= (4/3) * 3.14159;  # this will be constant folded
}

也可以使用 map() 完成,它用于将一个列表转换为另一个列表

my @volumes = map {$_ ** 3 * (4/3) * 3.14159} @radii;

如果您想对哈希值进行相同的修改,则可以使用 values 函数。从 Perl 5.6 开始,不会复制值,因此如果您修改 $orbit(在这种情况下),则会修改该值。

for my $orbit ( values %orbits ) {
    ($orbit **= 3) *= (4/3) * 3.14159;
}

在 perl 5.6 之前,values 会返回值的副本,因此较旧的 perl 代码通常包含诸如 @orbits{keys %orbits} 而不是 values %orbits(其中哈希将被修改)之类的结构。

如何从数组中选择一个随机元素?

使用 rand() 函数(请参阅 "rand" in perlfunc

my $index   = rand @array;
my $element = $array[$index];

或者,简单地

my $element = $array[ rand @array ];

如何排列列表的 N 个元素?

在 CPAN 上使用 List::Permutor 模块。如果列表实际上是一个数组,请尝试 Algorithm::Permute 模块(也在 CPAN 上)。它是用 XS 代码编写的,非常高效

use Algorithm::Permute;

my @array = 'a'..'d';
my $p_iterator = Algorithm::Permute->new ( \@array );

while (my @perm = $p_iterator->next) {
   print "next permutation: (@perm)\n";
}

为了执行得更快,您可以执行

use Algorithm::Permute;

my @array = 'a'..'d';

Algorithm::Permute::permute {
    print "next permutation: (@array)\n";
} @array;

这是一个小程序,它生成输入的每一行中所有单词的所有排列。permute() 函数中体现的算法在 Knuth 的《计算机编程的艺术》第 4 卷(尚未出版)中进行了讨论,并且适用于任何列表

#!/usr/bin/perl -n
# Fischer-Krause ordered permutation generator

sub permute (&@) {
    my $code = shift;
    my @idx = 0..$#_;
    while ( $code->(@_[@idx]) ) {
        my $p = $#idx;
        --$p while $idx[$p-1] > $idx[$p];
        my $q = $p or return;
        push @idx, reverse splice @idx, $p;
        ++$q while $idx[$p-1] > $idx[$q];
        @idx[$p-1,$q]=@idx[$q,$p-1];
    }
}

permute { print "@_\n" } split;

Algorithm::Loops 模块还提供了 NextPermuteNextPermuteNum 函数,这些函数可以高效地找到数组的所有唯一排列,即使它包含重复值,也会就地修改它:如果其元素按逆序排列,则数组将被反转,使其有序,并返回 false;否则将返回下一个排列。

NextPermute 使用字符串顺序和 NextPermuteNum 数字顺序,因此你可以像这样枚举 0..9 的所有排列

use Algorithm::Loops qw(NextPermuteNum);

my @list= 0..9;
do { print "@list\n" } while NextPermuteNum @list;

如何按(任何内容)对数组进行排序?

提供一个比较函数来排序(在 perlfunc 中的“sort” 中描述)

@list = sort { $a <=> $b } @list;

默认的排序函数是 cmp,字符串比较,它会将 (1, 2, 10) 排序为 (1, 10, 2)。上面使用的 <=> 是数字比较运算符。

如果你有一个复杂函数需要提取你想要排序的部分,那么不要在排序函数中执行此操作。首先提取它,因为排序 BLOCK 可以针对同一个元素调用多次。以下是如何提取每个项目第一个数字之后的第一个单词,然后对这些单词进行不区分大小写的排序的示例。

my @idx;
for (@data) {
    my $item;
    ($item) = /\d+\s*(\S+)/;
    push @idx, uc($item);
}
my @sorted = @data[ sort { $idx[$a] cmp $idx[$b] } 0 .. $#idx ];

也可以使用一种被称为 Schwartzian Transform 的技巧来编写此内容

my @sorted = map  { $_->[0] }
    sort { $a->[1] cmp $b->[1] }
    map  { [ $_, uc( (/\d+\s*(\S+)/)[0]) ] } @data;

如果你需要根据多个字段进行排序,以下范例很有用。

my @sorted = sort {
    field1($a) <=> field1($b) ||
    field2($a) cmp field2($b) ||
    field3($a) cmp field3($b)
} @data;

这可以方便地与上面给出的键的预计算结合使用。

请参阅 http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz 中“Far More Than You Ever Wanted To Know”集合中的 sort 文章,以了解有关此方法的更多信息。

另请参阅 perlfaq4 中关于排序哈希的后续问题。

如何操作位数组?

使用 pack()unpack(),或者使用 vec() 和按位运算。

例如,你不必在数组中存储单个位(这意味着你会浪费大量空间)。要将位数组转换为字符串,请使用 vec() 设置正确的位。这会将 $vec 设置为仅在设置 $ints[N] 时才设置位 N

my @ints = (...); # array of bits, e.g. ( 1, 0, 0, 1, 1, 0 ... )
my $vec = '';
foreach( 0 .. $#ints ) {
    vec($vec,$_,1) = 1 if $ints[$_];
}

字符串 $vec 仅占用所需的位数。例如,如果你在 @ints 中有 16 个条目,$vec 只需要两个字节来存储它们(不包括标量变量开销)。

以下是给定 $vec 中的向量,你可以将这些位放入 @ints 数组中的方法

sub bitvec_to_list {
    my $vec = shift;
    my @ints;
    # Find null-byte density then select best algorithm
    if ($vec =~ tr/\0// / length $vec > 0.95) {
        use integer;
        my $i;

        # This method is faster with mostly null-bytes
        while($vec =~ /[^\0]/g ) {
            $i = -9 + 8 * pos $vec;
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
            push @ints, $i if vec($vec, ++$i, 1);
        }
    }
    else {
        # This method is a fast general algorithm
        use integer;
        my $bits = unpack "b*", $vec;
        push @ints, 0 if $bits =~ s/^(\d)// && $1;
        push @ints, pos $bits while($bits =~ /1/g);
    }

    return \@ints;
}

这种方法在位向量越稀疏时速度越快。(由 Tim Bunce 和 Winfried Koenig 提供。)

你可以使用 Benjamin Goldberg 的建议,让 while 循环更短

while($vec =~ /[^\0]+/g ) {
    push @ints, grep vec($vec, $_, 1), $-[0] * 8 .. $+[0] * 8;
}

或者使用 CPAN 模块 Bit::Vector

my $vector = Bit::Vector->new($num_of_bits);
$vector->Index_List_Store(@ints);
my @ints = $vector->Index_List_Read();

Bit::Vector 为位向量、小整数集和“大整数”数学提供了高效的方法。

下面是使用 vec() 的更详细的示例

# vec demo
my $vector = "\xff\x0f\xef\xfe";
print "Ilya's string \\xff\\x0f\\xef\\xfe represents the number ",
unpack("N", $vector), "\n";
my $is_set = vec($vector, 23, 1);
print "Its 23rd bit is ", $is_set ? "set" : "clear", ".\n";
pvec($vector);

set_vec(1,1,1);
set_vec(3,1,1);
set_vec(23,1,1);

set_vec(3,1,3);
set_vec(3,2,3);
set_vec(3,4,3);
set_vec(3,4,7);
set_vec(3,8,3);
set_vec(3,8,7);

set_vec(0,32,17);
set_vec(1,32,17);

sub set_vec {
    my ($offset, $width, $value) = @_;
    my $vector = '';
    vec($vector, $offset, $width) = $value;
    print "offset=$offset width=$width value=$value\n";
    pvec($vector);
}

sub pvec {
    my $vector = shift;
    my $bits = unpack("b*", $vector);
    my $i = 0;
    my $BASE = 8;

    print "vector length in bytes: ", length($vector), "\n";
    @bytes = unpack("A8" x length($vector), $bits);
    print "bits are: @bytes\n\n";
}

为什么 defined() 在空数组和哈希上返回 true?

简而言之,你可能应该只对标量或函数使用 defined,而不是对聚合(数组和哈希)使用。有关更多详细信息,请参阅 Perl 5.004 及更高版本中的 "defined" in perlfunc

数据:哈希(关联数组)

如何处理整个哈希?

(由 brian d foy 贡献)

你可以使用两种方法来处理整个哈希。你可以获取键列表,然后遍历每个键,或者一次获取一个键值对。

要遍历所有键,请使用 keys 函数。这会提取哈希的所有键,并以列表的形式返回给你。然后,你可以通过正在处理的特定键获取值

foreach my $key ( keys %hash ) {
    my $value = $hash{$key}
    ...
}

获取键列表后,可以在处理哈希元素之前处理该列表。例如,你可以对键进行排序,以便按词法顺序处理它们

foreach my $key ( sort keys %hash ) {
    my $value = $hash{$key}
    ...
}

或者,你可能只想处理其中一些项。如果你只想处理以 text: 开头的键,则可以使用 grep 仅选择这些键

foreach my $key ( grep /^text:/, keys %hash ) {
    my $value = $hash{$key}
    ...
}

如果哈希非常大,你可能不想创建很长的键列表。为了节省一些内存,你可以使用 each() 一次获取一个键值对,它会返回你尚未看到的键值对

while( my( $key, $value ) = each( %hash ) ) {
    ...
}

each 运算符以看似随机的顺序返回键值对,因此如果顺序对你很重要,则必须坚持使用 keys 方法。

不过,each() 运算符可能有点棘手。在使用它时,不能添加或删除哈希的键,否则可能会在 Perl 内部重新哈希所有元素后跳过或重新处理某些对。此外,哈希只有一个迭代器,因此如果在同一个哈希上混合使用 keysvalueseach,则可能会重置迭代器并搞乱处理过程。有关更多详细信息,请参阅 perlfunc 中的 each 条目。

如何合并两个哈希?

(由 brian d foy 贡献)

在决定合并两个哈希之前,必须决定如果两个哈希都包含相同的键,该怎么做,以及是否要保留原始哈希的原样。

如果要保留原始哈希,请将一个哈希 (%hash1) 复制到一个新哈希 (%new_hash),然后将另一个哈希 (%hash2) 的键添加到新哈希中。检查键是否已存在于 %new_hash 中,可以决定如何处理重复项

my %new_hash = %hash1; # make a copy; leave %hash1 alone

foreach my $key2 ( keys %hash2 ) {
    if( exists $new_hash{$key2} ) {
        warn "Key [$key2] is in both hashes!";
        # handle the duplicate (perhaps only warning)
        ...
        next;
    }
    else {
        $new_hash{$key2} = $hash2{$key2};
    }
}

如果不想创建新哈希,仍然可以使用此循环技术;只需将 %new_hash 更改为 %hash1

foreach my $key2 ( keys %hash2 ) {
    if( exists $hash1{$key2} ) {
        warn "Key [$key2] is in both hashes!";
        # handle the duplicate (perhaps only warning)
        ...
        next;
    }
    else {
        $hash1{$key2} = $hash2{$key2};
    }
  }

如果不关心一个哈希覆盖另一个哈希的键和值,则可以直接使用哈希切片将一个哈希添加到另一个哈希中。在这种情况下,当 %hash2%hash1 具有公共键时,%hash2 中的值将替换 %hash1 中的值

@hash1{ keys %hash2 } = values %hash2;

如果在迭代哈希时添加或删除哈希的键,会发生什么?

(由 brian d foy 贡献)

简单的答案是“不要这样做!”

如果使用 each() 迭代哈希,则可以删除最近返回的键,而不用担心它。如果删除或添加其他键,则迭代器可能会跳过或重复它们,因为 perl 可能会重新排列哈希表。请参阅 perlfunceach() 的条目。

如何按值查找哈希元素?

创建反向哈希

my %by_value = reverse %by_key;
my $key = $by_value{$value};

这并不是特别有效。使用以下方法在空间上会更有效

while (my ($key, $value) = each %by_key) {
    $by_value{$value} = $key;
}

如果你的哈希可能具有重复值,则上述方法将只找到一个关联的键。这可能会让你担心,也可能不会。如果你确实担心,你可以始终将哈希反转为数组的哈希

while (my ($key, $value) = each %by_key) {
     push @{$key_list_by_value{$value}}, $key;
}

如何知道哈希中有多少个条目?

(由 brian d foy 贡献)

这与“如何处理整个哈希?”非常相似,也出现在 perlfaq4 中,但在常见情况下要简单一些。

你可以在标量上下文中使用内置函数 keys() 来找出哈希中有多少个条目

my $key_count = keys %hash; # must be scalar context!

如果你想找出有多少个条目具有已定义的值,那就有点不同了。你必须检查每个值。grep 很方便

my $defined_value_count = grep { defined } values %hash;

你可以使用相同的结构以任何你想要的方式计算条目。如果你想要包含元音的键的计数,你只需对此进行测试即可

my $vowel_count = grep { /[aeiou]/ } keys %hash;

标量上下文中的 grep 返回计数。如果你想要匹配项的列表,只需在列表上下文中使用它即可

my @defined_values = grep { defined } values %hash;

keys() 函数还会重置迭代器,这意味着如果你在使用其他哈希运算符(如 each())之间使用此函数,可能会看到奇怪的结果。

如何对哈希进行排序(可以选择按值而不是按键排序)?

(由 brian d foy 贡献)

要对哈希进行排序,请从键开始。在此示例中,我们将键列表提供给排序函数,然后按 ASCII 码顺序对它们进行比较(这可能会受到你的区域设置的影响)。输出列表中的键按 ASCII 码顺序排列。一旦有了键,我们就可以遍历它们来创建一个按 ASCII 码顺序列出键的报告。

my @keys = sort { $a cmp $b } keys %hash;

foreach my $key ( @keys ) {
    printf "%-20s %6d\n", $key, $hash{$key};
}

不过,我们可以在 sort() 块中变得更花哨。我们可以计算一个值并使用该值作为比较,而不是比较键。

例如,为了使我们的报告顺序不区分大小写,我们在比较键之前使用 lc 将键转换为小写

my @keys = sort { lc $a cmp lc $b } keys %hash;

注意:如果计算成本很高或哈希有许多元素,你可能需要查看 Schwartzian Transform 来缓存计算结果。

如果我们想按哈希值排序,我们使用哈希键来查找它。我们仍然得到一个键列表,但这次它们按其值排序。

my @keys = sort { $hash{$a} <=> $hash{$b} } keys %hash;

从那里我们可以变得更复杂。如果哈希值相同,我们可以在哈希键上提供辅助排序。

my @keys = sort {
    $hash{$a} <=> $hash{$b}
        or
    "\L$a" cmp "\L$b"
} keys %hash;

如何始终保持我的哈希排序?

你可以考虑使用 DB_File 模块和 tie(),并使用 $DB_BTREE 哈希绑定,如 DB_File 中的“内存数据库” 中所述。CPAN 中的 Tie::IxHash 模块也可能具有指导意义。尽管这确实让你的哈希保持排序,但你可能不喜欢从 tie 接口遭受的减速。你确定需要这样做吗? :)

“delete”和“undef”在哈希中有什么区别?

哈希包含标量对:第一个是键,第二个是值。键将被强制转换为字符串,尽管值可以是任何类型的标量:字符串、数字或引用。如果 %hash 中存在键 $key,则 exists($hash{$key}) 将返回 true。给定键的值可以是 undef,在这种情况下 $hash{$key} 将是 undef,而 exists $hash{$key} 将返回 true。这对应于 ($key, undef) 在哈希中。

图片有帮助... 以下是 %hash

  keys  values
+------+------+
|  a   |  3   |
|  x   |  7   |
|  d   |  0   |
|  e   |  2   |
+------+------+

并且这些条件成立

$hash{'a'}                       is true
$hash{'d'}                       is false
defined $hash{'d'}               is true
defined $hash{'a'}               is true
exists $hash{'a'}                is true (Perl 5 only)
grep ($_ eq 'a', keys %hash)     is true

如果你现在说

undef $hash{'a'}

你的表现在读取

  keys  values
+------+------+
|  a   | undef|
|  x   |  7   |
|  d   |  0   |
|  e   |  2   |
+------+------+

并且这些条件现在成立;大写字母的更改

$hash{'a'}                       is FALSE
$hash{'d'}                       is false
defined $hash{'d'}               is true
defined $hash{'a'}               is FALSE
exists $hash{'a'}                is true (Perl 5 only)
grep ($_ eq 'a', keys %hash)     is true

注意最后两个:你有一个 undef 值,但有一个已定义的键!

现在,考虑这个

delete $hash{'a'}

你的表现在读取

  keys  values
+------+------+
|  x   |  7   |
|  d   |  0   |
|  e   |  2   |
+------+------+

并且这些条件现在成立;大写字母的更改

$hash{'a'}                       is false
$hash{'d'}                       is false
defined $hash{'d'}               is true
defined $hash{'a'}               is false
exists $hash{'a'}                is FALSE (Perl 5 only)
grep ($_ eq 'a', keys %hash)     is FALSE

你看,整个条目都消失了!

为什么我的绑定哈希不区分已定义/存在?

这取决于绑定哈希的 EXISTS() 实现。例如,绑定到 DBM* 文件的哈希没有未定义的概念。这也意味着 exists() 和 defined() 对 DBM* 文件执行相同操作,而它们最终执行的操作与对普通哈希执行的操作不同。

如何在进行到一半时重置 each() 操作?

(由 brian d foy 贡献)

你可以使用 keysvalues 函数来重置 each。若要仅重置 each 使用的迭代器而不执行任何其他操作,请在 void 上下文中使用其中一个

keys %hash; # resets iterator, nothing else.
values %hash; # resets iterator, nothing else.

请参阅 perlfunceach 的文档。

如何从两个哈希中获取唯一键?

首先,将哈希中的键提取到列表中,然后解决上面描述的“删除重复项”问题。例如

my %seen = ();
for my $element (keys(%foo), keys(%bar)) {
    $seen{$element}++;
}
my @uniq = keys %seen;

或者更简洁地

my @uniq = keys %{{%foo,%bar}};

或者如果你真的想节省空间

my %seen = ();
while (defined ($key = each %foo)) {
    $seen{$key}++;
}
while (defined ($key = each %bar)) {
    $seen{$key}++;
}
my @uniq = keys %seen;

如何将多维数组存储在 DBM 文件中?

自己对结构进行字符串化(没意思),或者从 CPAN 获取 MLDBM(使用 Data::Dumper)模块,并将其分层放置在 DB_File 或 GDBM_File 之上。你也可以尝试 DBM::Deep,但它可能会有点慢。

如何让我的哈希记住我将元素放入其中的顺序?

从 CPAN 使用 Tie::IxHash

use Tie::IxHash;

tie my %myhash, 'Tie::IxHash';

for (my $i=0; $i<20; $i++) {
    $myhash{$i} = 2*$i;
}

my @keys = keys %myhash;
# @keys = (0,1,2,3,...)

为什么向子例程传递哈希中未定义的元素会创建它?

(由 brian d foy 贡献)

你使用的是非常旧版本的 Perl 吗?

通常,访问不存在的键的哈希键值不会创建该键。

my %hash  = ();
my $value = $hash{ 'foo' };
print "This won't print\n" if exists $hash{ 'foo' };

然而,向子例程传递 $hash{ 'foo' } 曾经是一个特例。由于你可以直接赋值给 $_[0],因此 Perl 必须准备好进行该赋值,所以它会提前创建哈希键

my_sub( $hash{ 'foo' } );
print "This will print before 5.004\n" if exists $hash{ 'foo' };

sub my_sub {
    # $_[0] = 'bar'; # create hash key in case you do this
    1;
}

然而,自 Perl 5.004 起,这种情况是一个特例,并且只有在你进行赋值时,Perl 才会创建哈希键

my_sub( $hash{ 'foo' } );
print "This will print, even after 5.004\n" if exists $hash{ 'foo' };

sub my_sub {
    $_[0] = 'bar';
}

但是,如果你想要旧的行为(并且仔细考虑这一点,因为它是一个奇怪的副作用),你可以传递一个哈希切片。Perl 5.004 没有将此作为特例

my_sub( @hash{ qw/foo/ } );

如何制作 Perl 等效于 C 结构/C++ 类/哈希或哈希或数组的数组?

通常是哈希引用,可能如下所示

$record = {
    NAME   => "Jason",
    EMPNO  => 132,
    TITLE  => "deputy peon",
    AGE    => 23,
    SALARY => 37_000,
    PALS   => [ "Norbert", "Rhys", "Phineas"],
};

引用在 perlrefperlreftut 中进行了说明。复杂数据结构的示例在 perldscperllol 中给出。结构和面向对象类的示例在 perlootut 中。

如何将引用用作哈希键?

(由 brian d foy 和 Ben Morrow 贡献)

哈希键是字符串,因此你无法真正将引用用作键。当你尝试这样做时,perl 会将引用转换为其字符串化形式(例如,HASH(0xDEADBEEF))。从那里你无法从字符串化形式中获取引用,至少在不自己做一些额外工作的情况下无法获取。

请记住,即使引用的变量超出范围,哈希中的条目仍会存在,并且 Perl 完全有可能随后在同一地址分配一个不同的变量。这意味着一个新变量可能会意外地与旧变量的值关联。

如果您有 Perl 5.10 或更高版本,并且只想针对引用存储一个值以供以后查找,则可以使用核心 Hash::Util::Fieldhash 模块。如果您使用多个线程(这会导致所有变量在新的地址重新分配,从而更改其字符串化),它还将处理重命名键,并在引用的变量超出范围时对条目进行垃圾回收。

如果您实际上需要能够从每个哈希条目中获取一个真正的引用,则可以使用 Tie::RefHash 模块,它为您完成所需的工作。

如何检查键是否存在于多级哈希中?

(由 brian d foy 贡献)

解决此问题的诀窍是避免意外自动生成。如果您想检查三个键的深度,您可能天真地尝试这样做

my %hash;
if( exists $hash{key1}{key2}{key3} ) {
    ...;
}

即使您从一个完全空的哈希开始,在调用 exists 之后,您已经创建了检查 key3 所需的结构

%hash = (
          'key1' => {
                      'key2' => {}
                    }
        );

那是自动生成。您可以通过几种方式解决这个问题。最简单的方法是关闭它。CPAN 上提供了词法 autovivification 实用程序。现在您不会添加到哈希中

{
    no autovivification;
    my %hash;
    if( exists $hash{key1}{key2}{key3} ) {
        ...;
    }
}

CPAN 上的 Data::Diver 模块也可以为您完成此操作。它的 Dive 子例程不仅可以告诉您键是否存在,还可以获取值

use Data::Diver qw(Dive);

my @exists = Dive( \%hash, qw(key1 key2 key3) );
if(  ! @exists  ) {
    ...; # keys do not exist
}
elsif(  ! defined $exists[0]  ) {
    ...; # keys exist but value is undef
}

您也可以轻松地自己执行此操作,方法是在移到下一级之前检查哈希的每一级。这基本上是 Data::Diver 为您完成的操作

if( check_hash( \%hash, qw(key1 key2 key3) ) ) {
    ...;
}

sub check_hash {
   my( $hash, @keys ) = @_;

   return unless @keys;

   foreach my $key ( @keys ) {
       return unless eval { exists $hash->{$key} };
       $hash = $hash->{$key};
    }

   return 1;
}

如何防止向哈希中添加不需要的键?

从 5.8.0 版本开始,哈希可以限制为固定数量的给定键。创建和处理受限哈希的方法由 Hash::Util 模块导出。

数据:其他

如何正确处理二进制数据?

Perl 是二进制干净的,因此它可以很好地处理二进制数据。然而,在 Windows 或 DOS 上,你必须对二进制文件使用 binmode 以避免换行符的转换。一般来说,任何时候你想要使用二进制数据时,都应该使用 binmode

另请参阅 perlfunc 中的“binmode”perlopentut

如果你担心 8 位文本数据,那么请参阅 perllocale。然而,如果你想要处理多字节字符,那么有一些陷阱。请参阅有关正则表达式的部分。

如何确定标量是数字/整数/浮点数?

假设你不在乎“NaN”或“Infinity”之类的 IEEE 表示法,你可能只想使用正则表达式(另请参阅 perlretutperlre)。

use 5.010;

if ( /\D/ )
    { say "\thas nondigits"; }
if ( /^\d+\z/ )
    { say "\tis a whole number"; }
if ( /^-?\d+\z/ )
    { say "\tis an integer"; }
if ( /^[+-]?\d+\z/ )
    { say "\tis a +/- integer"; }
if ( /^-?(?:\d+\.?|\.\d)\d*\z/ )
    { say "\tis a real number"; }
if ( /^[+-]?(?=\.?\d)\d*\.?\d*(?:e[+-]?\d+)?\z/i )
    { say "\tis a C float" }

还有一些常用的模块可以用于此任务。 Scalar::Util(随 5.8 发行)提供对 Perl 内部函数 looks_like_number 的访问,用于确定变量是否看起来像数字。 Data::Types 导出使用上述正则表达式和其他正则表达式验证数据类型的函数。第三,有 Regexp::Common,它有正则表达式来匹配各种类型的数字。这三个模块可从 CPAN 获得。

如果你在 POSIX 系统上,Perl 支持 POSIX::strtod 函数,用于将字符串转换为双精度(以及 POSIX::strtol 用于长整型)。它的语义有点繁琐,因此这里有一个 getnum 包装函数,以便更方便地访问。此函数接受一个字符串并返回它找到的数字,或者对于不是 C 浮点数的输入返回 undef。如果你只想说“这是浮点数吗?”,那么 is_numeric 函数是 getnum 的前端。

sub getnum {
    use POSIX qw(strtod);
    my $str = shift;
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;
    $! = 0;
    my($num, $unparsed) = strtod($str);
    if (($str eq '') || ($unparsed != 0) || $!) {
            return undef;
    }
    else {
        return $num;
    }
}

sub is_numeric { defined getnum($_[0]) }

或者你也可以查看 CPAN 上的 String::Scanf 模块。

如何在程序调用之间保持持久数据?

对于一些特定应用程序,你可以使用其中一个 DBM 模块。请参阅 AnyDBM_File。更一般地说,你应该查阅 CPAN 中的 FreezeThawStorable 模块。从 Perl 5.8 开始,Storable 是标准发行版的一部分。这里有一个使用 Storablestoreretrieve 函数的示例

use Storable;
store(\%hash, "filename");

# later on...
$href = retrieve("filename");        # by ref
%hash = %{ retrieve("filename") };   # direct to hash

如何打印或复制递归数据结构?

CPAN 上的 Data::Dumper 模块(或 Perl 的 5.005 版本)非常适合打印数据结构。CPAN 上的 Storable 模块(或 Perl 的 5.8 版本)提供了一个名为 dclone 的函数,该函数会递归复制其参数。

use Storable qw(dclone);
$r2 = dclone($r1);

其中 $r1 可以是对任何类型数据结构的引用。它将被深度复制。由于 dclone 接受并返回引用,因此如果你要复制一个数组哈希,则必须添加额外的标点符号。

%newhash = %{ dclone(\%oldhash) };

如何为每个类/对象定义方法?

(由 Ben Morrow 贡献)

你可以使用 UNIVERSAL 类(参见 UNIVERSAL)。但是,请务必仔细考虑这样做的后果:为每个对象添加方法很可能会产生意想不到的后果。如果可能的话,最好让所有对象从某个公共基类继承,或使用支持角色的 Moose 等对象系统。

如何验证信用卡校验和?

从 CPAN 获取 Business::CreditCard 模块。

如何为 XS 代码打包双精度或浮点数组?

CPAN 上 PGPLOT 模块中的 arrays.h/arrays.c 代码就是这样做的。如果你正在进行大量浮点或双精度处理,请考虑改用 CPAN 上的 PDL 模块——它可以轻松进行数字运算。

有关代码,请参见 https://metacpan.org/release/PGPLOT

作者和版权

版权所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者(如上所述)。保留所有权利。

本文件中的所有代码示例均在此免费提供。你可以根据与 Perl 本身相同的条款重新分发和/或修改它。

无论其分发情况如何,本文件中的所有代码示例在此特此置于公共领域。你可以根据自己的意愿在自己的程序中使用此代码以娱乐或盈利为目的。在代码中添加一个简单的注释来表示感谢将是一种礼貌,但不是必需的。