Learning Perl 学习笔记 Ch14 字符串与排序
- Perl提供一系列强大的字符串处理函数,来处理它90%情况下要处理的和文本相关的工作。其中用于在字符串内查找匹配字串位置的函数就是
index
。index
函数有三个参数,分别表示待匹配字符串,需要匹配的子串、匹配开始位置,其中第三个参数是可选的。
index ($match_string, $substring, $start_position);
index
函数查找子串在待匹配的字符串中第一次出现的位置,从左侧开始数,0表示第一个元素(也可以理解为字符串第一次出现子串前需要跳过的元素个数),如果找不到匹配的位置,则返回-1
demo14-1
:
#!/usr/bin/perl
$hello_string = "Hello world";
$pos = index ($hello_string, "l");
while($pos >= 0){
print "I found 'l' in '$hello_string' at '$pos'.\n";
$pos = index ($hello_string, "l", $pos + 1);
}
./demo14-1
I found 'l' in 'Hello world' at '2'.
I found 'l' in 'Hello world' at '3'.
I found 'l' in 'Hello world' at '9'.
rindex
函数的功能和index
功能相同,唯一的区别在于它从右往左(从字符串的结尾向前)寻找匹配字串的位置(但是匹配的顺序和代表匹配的位置的返回值还是从左往右的),第三个参数对应的也指向从右往左开始匹配的位置
demo14-2
:
#!/usr/bin/perl
$string = "abc def def ghi";
print $string."\nPlease type in the matching substring: ";
chomp($substring = <STDIN>);
printf "I found '%s' in '%s' at '%d' from left to right and '%d' from right to left.\n",
$substring,
$string,
index ($string, $substring),
rindex ($string, $substring);
./demo14-2
abc def def ghi
Please type in the matching substring: def
I found 'def' in 'abc def def ghi' at '4' from left to right and '8' from right to left.
./demo14-2
abc def def ghi
Please type in the matching substring: abc
I found 'abc' in 'abc def def ghi' at '0' from left to right and '0' from right to left.
-
substr
操作符的主要功能是按需截断字符串,但它实际上能做更多。substr
接受三个参数,第一个参数是字符串,第二个是截断子串开始的位置,第三个是子串的长度
$part = substr($string, $initial_position, $length);
第三个参数是可选的,如果没有第三个参数,子串会从开始位置一直截断到字符串结束。另外,如果第三个参数超过了子串的最大长度(从截断开始位置到字符串结束)那么子串就到字符串为止,不会补充字符以填满参数规定的长度。
demo14-3
:
#!/usr/bin/perl
$string = "Thomas Jefferson";
$substring = substr ($string, 6);
print $substring."\n";
$substring = substr($string, 5, 3);
print $substring."\n";
./demo14-3
Jefferson
s J
有一点不同的是,第二参数标记截断子串开始位置的整数也可以是负数,和在数组角标中的作用相同,负数表示从字符串结尾向前数的位置,-3
就表示从从字符串倒数第三个字符开始截断,-1
则表示只截取字符串最后一个字符(负数不会改变截断的方向)
substr
操作符可以和index
联用,实现截断指定子串的内容:
demo14-4
:
#!/usr/bin/perl
$name = "Thomas Jefferson";
$last_name = substr ($name, index($name, " ") + 1);
print $last_name."\n";
./demo14-4
Jefferson
另一个特殊的用法是用substr
实现替换功能,如果待截断的字符串是变量,那么可以用如下的代码将截断的子串进行替换
substr($string, index($string, "test")) = "success";
需要替换的字符串不必和原来的字符串长度相同
如果需要更复杂的功能还可以结合正则替换和绑定操作符
substr($string, index($string, "name:"), 10) = s/^name(.)*/Name:$1/;
示例demo14-5
:
#!/usr/bin/perl
$string = "Name: Max Gender:Male";
print "The original string: '$string'"."\n";
substr($string, 0, index($string, "Gender")) = "Name: Caroline ";
substr($string, index ($string, "Gender")) =~ s/Male/Female/;
print "Modified String: '".$string."'\n";
./demo14-5
The original string: 'Name: Max Gender:Male'
Modified String: 'Name: Caroline Gender:Female'
作为一种简化版的实现,substr
和index
结合起来通常要比正则表达式快一点。除了上面这种用法,substr
也提供传统的函数调用方式实现替换,新增一个参数传递替换后的字符串(这种情况,第三个参数不能省略):
demo14-6
:
#!/usr/bin/perl
$string = "Air China CA987 Beijing to Los Angeles";
$return_string = substr($string, 10, 5, "CA887");
print "The function substr return: '$return_string'\n";
print "The original string: '$string'\n";
./demo14-6
The function substr return: 'CA987'
The original string: 'Air China CA887 Beijing to Los Angeles'
-
sprintf
可以和printf
一样对字符串进行格式化处理,但会把格式化后的字符串返回而不是打印,所以可以和程序进行更好的结合
demo14-7
:
#!/usr/bin/perl
$year = 2019;
$month = 6;
$day = 22;
$hour = 5;
$minute = 25;
$second = 0;
$result = sprintf "%4d/%02d/%02d %2d:%2d:%2d", $year, $month, $day, $hour, $minute, $second;
print $result."\n";
注意到%02d
这种格式应用了前置零,格式化结果会在不足两位时,用0填补,可以对比下面展示的6月和5点,一个是有前置零的,另一个则没有
./demo14-7
2019/06/22 5:25: 0
下面这个例子将对财务数据进行格式化,格式化的要求有:
- 四舍五入到两位小数
- 每三位用逗号隔开
四舍五入可以用格式化字符串%.2f
实现精确到小数点后两位,每三位用逗号隔开则需要使用s///
进行替换,特别需要注意的是,需要从个位数向前数三个数,保证不足三个数的情况只发生在最高位。
demo14-8
:
#!/usr/bin/perl
sub big_money{
$number = sprintf "%.2f", shift @_; #对数据进行第一部处理:精确到小数点后两位
1 while $number =~ s/(-?\d+)(\d\d\d)/$1,$2/; #添加逗号
$number =~ s/^(-?)/$1\$/; #在开始位置添加美元$符号
$number; #返回
}
print "Please type in the number:";
chomp(my $input = <STDIN>);
my $result = &big_money($input);
print "The result is: $result\n";
./demo14-8
Please type in the number:-12569853200.369
The result is: -$12,569,853,200.37
这个程序的特殊之处在于1 while $number =~ s/(-?\d+)(\d\d\d)/$1,$2/;
这一行,循环体的内容是1
,所以没有任何意义,循环的作用体现在循环条件判断语句上:$number =~ s/(-?\d+)(\d\d\d)/$1,$2/
,利用正则表达式量词匹配的贪婪特性,每次总会在最右侧合适的位置加上逗号,直到没有更多合适的位置,返回false。因为负数符号是可选的,所以符号的量词是“0或1次”
4 Perl中的sort
函数可以实现对列表进行排序,但是默认只能按照ASCII码序进行排序,Perl支持扩展sort
函数,实现自定义排序规则,被称为高级排序。
所谓扩展,实际是可以自定义排序时比较两个值的子程序,排序本身的算法仍有Perl完成。要实现高级排序,首先要写一个子程序,子程序的返回值只有-1, 0, 1
,用来表示待比较的两个值的“小于、等于或大于”关系。然后在调用sort
函数时,把子程序的名字插在sort
关键字和参数列表之间。
demo14-9
:
#!/usr/bin/perl
sub by_number{
if($a < $b) {
1
} elsif($a > $b) {
-1
} else {
0
}
}
my @array = qw /1 3 5 7 9/;
my @result = sort by_number @array;
print "The result is [@result]\n";
./demo14-9
The result is [9 7 5 3 1]
注意到这里子程序使用了两个未声明的变量$a, $b
,Perl会把原始列表中需要比较的元素自动放在$a
和$b
,而且这里放的就是元素本身而非拷贝,所以不能对$a
和$b
进行修改。
sort
默认的规则是按数字升序和字符的ASCII码序,如果要使用这种默认规则,可以用简化的操作符<=>
和cmp
分别对应着数值比较操作和字符串比较操作,而且Perl会尝试把由数值组成的字符串按照数值而非字符串进行比较。
sub default_ASCII {$a cmp $b;}
sort default_ASCII @array;
和
sub default_ASCEND {$a <=> $b;}
sort default_ASCEND @array;
看起来并没有什么意义,因为sort
函数本来也是那样做的,但是我们可以稍微变通一下操作符两边的内容,
sub default_ASCII_ignore_cases { '\L$a' cmp '\L$b';}
可以实现忽略大小写的字符串比较,注意这里只改变了cmp
操作符两边的内容,但并没有改变$a
和$b
的值,那种操作是不被Perl允许的。
同样我们可以用
sub default_DESCEND {$b <=> $a;}
实现降序排序,<=>
和cmp
操作符都是短视的,它们并不能认出$a
和$b
,他们只是用左边的值去比较右边的值,然后返回1
,-1
或0
。
如果使用操作符,还可以把代码进一步简化,略过不必要的子程序定义
my @descend = sort {$b <=> $a} @array;
利用高级排序的特性,我们还可以对哈希进行排序,传统的方式只能对哈希的键进行排序,但我们可以自定义比较程序,来实现对哈希的值进行排序:
demo14-10
:
#!/usr/bin/perl
my %score = ("barney" => 195,
"fred" => 205,
"dino" => 30
);
sub by_score{
$score{$b} <=> $score{$a};
}
@result = sort by_score (keys %score);
print "The sorting result is\n";
foreach (@result){
print " $_ => $score{$_}\n";
}
这里再比较子程序中,比较了哈希表中的值,实现了按值排序,注意这里并没有修改$a
和$b
的内容。
对哈希表按值排序会引入一个问题,不同于哈希表键的唯一性,哈希表的值可能会有相同的项目。Perl支持多值排序,允许对多个条件进行比较
sub by_score_and_name{
$score{$b} <=> $score{$a} #按分数降序,如果同分则为0
or #如果前式为0,继续判断后式
$a cmp $b # 分数相同,再按ASCII码排序
}
这个规则可以无限扩展,实现多值比较排序:
@patron = sort {
&fines($b) <=> &fines($a) or
$items{$b} <=> $items{$a} or
$family_name{$a} cmp $family_name{$b} or
$personal_name{$a} cmp $personal_name{$b} or
$a cmp $b} @patron;