perl一行式程式系列文章Perl一行式


獲取每行最後一個欄位

$ perl -alne 'print $F[$#F]' file.log

這裡涉及到了選項"-a"、陣列@F。這裡同時還會解釋-F選項,它和-a常一起使用。

選項"-a"和awk的自動欄位分割一樣,會自動將每行資料劃分為幾個欄位。劃分欄位的分隔符由-F選項指定。如果沒有指定-F,則預設以空白符號進行分割(連續空格被認為是單空格)。

分割後的元素全都收集到一個數組@F中,所以第一個欄位的內容是$F[0],最後一個欄位是$F[-1]$F[$#F]

如果想取多個欄位,可以對陣列@F進行切片,例如第3個欄位和第第5個欄位@F[2,4],第3個欄位到倒數第二個欄位是@F[2..$#F-1]@F[1..~~@F-2]

獲取範圍欄位

正如上面所解釋的,如果想要獲取第二個欄位到倒數第二個欄位:

$ perl -lane 'print "@F[1..~~@F-2]"' file.log
$ perl -lane 'print "@F[1..$#F-1]"' file.log

指定欄位分隔符

之所以單獨拿出來解釋,是因為"-F"指定分隔符時,空白符號的特殊之處。

對於普通字元,-F自然沒有什麼問題:

$ perl -F: -alne 'print $F[1]' /etc/passwd

但是想指定空白字元作為欄位的分隔符時,"-F"選項將出現故障:

$ perl -F" " -alne 'print $F[1]' file.log
o
a
i
y
y

發現空格分隔符根本沒起作用,而是按照NUL作為分隔符對每個字元都分割了。

這個問題在-F選項的官方手冊中已經註明了:

You can't use literal whitespace or NUL characters in the pattern

如果想要指定空白符號作為欄位分隔符,可以考慮其它方式。例如使用\s的正則模式,或者直接不使用-F,而是直接在-e表示式中使用split函式進行行的分割。

$ perl -F'\s+' -anle 'print $F[1]' file.log
$ perl -alne 'split / /;print $F[1]' file.log

計算所有行數值的總和

假如檔案內容為:

1 3 5 9
2 3 1
10 2 3 6

想要總計所有這些數值之和,可以使用如下方式:

$ perl -M'List::Util=sum' -alne '$num += sum @F;END{print $num}' file.log

或者,將所有行讀取到陣列中,最後對陣列加總:

$ perl -M'List::Util=sum' -alne 'push @S,@F;END{print sum @S}' file.log

這種方式對於大檔案肯定是不如前一種方式友好的,因為它會將所有行內容都儲存起來,而前一種方式為所有行都只儲存一個結果$num,佔用的記憶體要低的多的多。

打亂欄位的順序

$ perl -M'List::Util=shuffle' -alne 'print "@{[shuffle @F]}"' file.log

List::Util模組中有一個函式shuffle,它會按照隨機的順序打亂一個列表(瞭解即可,這不是本文的重點)。例如:

$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
8 1 3 7 10 5 2 4 6 9
$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
1 2 7 10 8 4 3 5 6 9
$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
2 5 7 1 4 6 3 8 9 10

這裡我想介紹的重點是"@{[ shuffle @F ]}",它是讓一個操作可以插入到雙引號中的方法,這個在Perl一行式必知語法基礎解釋過。

雖然目的是插入到雙引號中,但它的最終目標是為了讓陣列元素輸出時以空格分隔。所以,這種技巧不是唯一的方法,見下一小節。

指定欄位輸出分隔符(無引號包圍)

上一小節通過@{[shuffle @F]}的方式可以將打亂陣列的操作插入到雙引號中。下面是其它方法。

1.指定陣列的欄位輸出分隔符$,

$ perl -M'List::Util=shuffle' -alne 'BEGIN{$,=" "}print shuffle @F' file.log

預設情況下,print/say輸出列表的時候,如果陣列/列表不插入到雙引號中,各元素之間緊連在一起被輸出:

$ perl -le '@arr=qw(Shell Perl Python);print @arr'
ShellPerlPython

特殊變數$,指定的就是print/say輸出陣列且不插入雙引號時的元素分隔符,其預設值為undef。

例如指定為空格:

$ perl -le '
@arr=qw(Shell Perl Python);
$,=" ";
print @arr'
Shell Perl Python

2.將列表join成字串,join的連線符指定為空格即可

$ perl -M'List::Util=shuffle' -anle 'print join " ",shuffle @F' file.log

指定欄位分隔符(引號包圍)

變數$,控制無雙引號包圍的陣列/列表在print/say輸出時的元素分隔符。其實雙引號包圍的陣列/列表被print/say輸出時也可以指定元素分隔符。

控制輸出雙引號包圍的列表的元素分隔符的特殊變數是$",預設值為空格。

# 預設空格分隔雙引號中的元素
$ perl -le '
@arr = qw(Shell Perl PHP Python);
print "@arr"'
Shell Perl PHP Python # $"改變雙引號中的元素分隔符
$ perl -le '
@arr = qw(Shell Perl PHP Python);
$" = ":";
print "@arr"'
Shell:Perl:PHP:Python

所有行中最小數值

假如檔案內容為:

1 3 0 9
2 -3 1
10 -2 -3 6

所有行的最小值為-3,如何取得?

最簡單的方式是將所有行都讀入並儲存到陣列中,然後使用List::Util模組的min函式取得。

$ perl -M'List::Util=min' -anle 'push @nums,@F;END{print min @nums}' num.log

但這對於大檔案來說記憶體佔用率會很高。比較好的方式是從每行中取出最小值,保留到陣列中,最後從這個陣列中取出最小值(稍後繼續解釋更好的方式)。

$ perl -M'List::Util=min' -anle '
push @nums,min @F;
END{print min @nums}' num.log

如果檔案的行數量非常大,這也會在記憶體中保留很多數值,也不是最佳方式。

更好的方式是從每行中取出最小值儲存下來,然後和後面的行結合在一起取最小值。這樣的方式使得整個處理過程都只佔用一行記憶體空間。

$ perl -M'List::Util=min' -anle '
$min = min ($min // (),@F);
END{print $min};' num.log

這裡的關鍵是min $min//(),@F

首先,$min//()表示如果$num未定義,則返回空列表(),否則返回$min。如果這裡不進行$min是否已定義的判斷,那麼第一次使用$min的時候,它被當作空。所以如果檔案中沒有負數,下面的操作將會因此而返回空。

$ perl -M'List::Util=min' -anle '
$min = min ($min,@F);
END{print $min};' num.log

再者,上面$min//()$min未定義的時候返回的是空列表(),不能編寫為返回0或空,否則就多出了一個要比較的值。

最後,min函式操作的是一個列表,而Perl會將多個列表壓扁形成一個大列表,所以$min//(),@F被壓成了一個被min函式操作的列表,而()表示空列表,這使得第一次使用$min的時候不會影響要比較的值。

將所有數值取絕對值

假如檔案內容為:

1 3 0 9
2 -3 1
10 -2 -3 6

要返回所有數值的絕對值,可以借用預設函式abs來操作。

簡單的邏輯可以遍歷@F,並應用abs函式,最後追加回列表被輸出:

$ perl -lane '
@line=();
for(@F){push @line,abs};
print "@line";
' num.log

在Perl中,對於列表中每個元素都要檢查、操作的情形,可以使用map函式。map函式太強大了,堪稱逆天級,map的詳細用法參見Perl map用法詳解。這裡根據此處需求給出map的使用示例:

$ perl -lane 'print "@{[map {abs} @F]}"' num.log

其中map {op} LIST表示對LIST中的每個元素都執行op操作,操作後的值構成一個新列表。@{[ ]}的格式在前面已經出現多次了,不再解釋。

統計所有匹配欄位數量

$ perl -lane '/pattern/ && ++$num for @F;END{print $num || 0}' file.log

這種統計方式是安全的。先劃分欄位,然後匹配每個欄位,只要匹配到就將計數器變數加1。最後輸出計數器的值。但可能匹配不到任何東西,所以必須給計數器變數設定一個預設值,也就是$num || 0

另一種改寫方式是:

$ perl -anle '$t += /pattern/ for @F;END{print $t}' file.log

這裡採用的賦值方式$t += /pattern/,因為/pattern/返回的是匹配成功的數量,不匹配成功則會返回0,所以無需像前面一樣設定計數器變數的預設值。

如果使用grep來改寫,則一行式命令如下:

$ perl -alne '$num += grep /pattern/,@F;END{print $num}' file.log

grep返回一個新的表示能匹配的元素列表,但是在標量上下文中列表返回的是元素個數,所以可直接加總計算。

生成10個5-15之間的隨機數

$ perl -le 'print "@{[ map int(rand(10)+5),1..10 ]}"'
10 9 9 11 13 10 14 12 13 6

rand(10)表示生成0-9之間的隨機浮點數,int(rand(10))表示生成0-9之間的隨機整數,加上5表示5-14的隨機整數,整個過程執行10次,所以生成了10個隨機整數。

生成字母表

$ perl -le 'print a..z'
abcdefghijklmnopqrstuvwxyz

更準確的寫法是加上雙引號:

$ perl -le 'print "a".."z"'
abcdefghijklmnopqrstuvwxyz

逗號分隔字母表:

$ perl -le 'print join ",","a".."z"'
a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z

輸出"a"到"zz":

$ perl -le '"a".."zz"'

..符號會按照字元序列進行遞增,"z"遞增後得到"aa",再遞增得到"ab","az"遞增得到"ba",依次類推。所以這裡會返回大量字元(共702個字元):

a..z
aa..az
ba..bz
...
xa..xz
ya..yz
za..zz

生成8字元隨機密碼

$ perl -le 'print map { ("a".."z")[rand 26] } 1..8'

這裡("a".."z")[rand 26]取26個字母中的一個隨機字母,1..8表示生成8個字元。

如果想要包含大小寫字母、數字,可以:

$ perl -le 'print map { ("a".."z","A".."Z",0..9)[rand 62] } 1..8'

生成8字元隨機密碼,包含特殊字元

如果想要讓生成的隨機密碼中包含大小寫字母、數字、各種標點符號,可以通過ascii碼錶來指定範圍,然後使用chr函式來轉換ascii碼為字元。

根據ascii,從33開始到126結束是大小寫字母、數字、各種標點符號部分。所以:

$ perl -le 'print map { chr int(rand(94)) + 33 } 1..8'

int(rand(94) + 33)表示生成33-126(包括126)之間的隨機整數,chr函式可以將數值轉換為對應的字元。

$ for i in `seq 1 10`;do
perl -le 'print map {chr int(rand(94))+33} 1..8'
done >t]>W69#
>]e<Hub6
D}R1l)._
*(HKFZ6Q
x++\"=1O
@K)%.N@s
sSji5&FX
o+.#?@/x
^6[l~%-k
.bkTKA[%