1. 程式人生 > >Perl一行式:處理行號和單詞數

Perl一行式:處理行號和單詞數

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


所有行的行號

$ perl -pe '$_ = "$. $_"' file.log
$ perl -ne 'print "$. $n"' file.log

這裡涉及了一個特殊變數$.

這個特殊變數代表的是當前處理行的行號。對於Perl的一行式來說,通過<>隱式開啟的檔案控制代碼預設不會關閉,所以如果引數中有多個檔案,進入下一個檔案時行號不會重置。

例如:

$ cat a.txt
aaa
bbb
$ cat b.txt
ccc
ddd

# 行號不重置
$ perl -pe '$_ = "$. $_"' a.txt b.txt
1 aaa
2 bbb
3 ccc
4 ddd

如果想要每個檔案的行號都獨立計算。可以使用下面這種方式進行判斷:遇到檔案尾部,顯式關閉檔案。

$ perl -e '
    while(<>){
        print "$. $_"
    }continue{
        close ARGV if eof
    }' a.txt b.txt
1 aaa
2 bbb
1 ccc
2 ddd

非空行的行號

$.是Perl自帶的檔案控制代碼上的行號計數器,讀取的每一行都會計數。所以如果想要統計檔案中的某些行的行號,使用自帶的$.是不可行的,只能自己實現行號計數器。

如下:

$ perl -pe '$_ = ++$x." $_" if /\S/' file.log

這裡的邏輯是,只要行中有非空白字元,就自增一個變數的值。自增後的值和字串進行串聯,並賦值給$_被-p輸出。

非空行行號並刪除空白行

因為要刪除某些行不輸出,所以不能使用-p選項,它會將所有行都輸出,除非使用s///來刪除整行。可以考慮使用-n選項。

$ perl -ne 'print ++$x." $_" if /\S/' file.log

輸出匹配行的行號

例如輸出檔案中匹配"nologin"單詞的行號,其它行照常輸出。

$ perl -pe '$_ = "$. $_" if /nologin/' file.log
root   x 0     0 root   /root     /bin/bash
2 daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
3 bin    x 2     2 bin    /bin      /usr/sbin/nologin
4 sys    x 3     3 sys    /dev      /usr/sbin/nologin
sync   x 4 65534 sync   /bin      /bin/sync

如果想要單獨計數被匹配行的行號,可以自己寫計數器。

$ perl -pe '$_ = ++$num." $_" if /nologin/' file.log
root   x 0     0 root   /root     /bin/bash
1 daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
2 bin    x 2     2 bin    /bin      /usr/sbin/nologin
3 sys    x 3     3 sys    /dev      /usr/sbin/nologin
sync   x 4 65534 sync   /bin      /bin/sync

如果需要格式化輸出,使得沒有匹配的行也和帶有行號的行對齊。可以進行多分支的賦值:

$ perl -pe '
    $_ = do {
        if(/nologin/){
            ++$num." $_"
        }else{
            "  $_"
        }}' file.log

  root   x 0     0 root   /root     /bin/bash
1 daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
2 bin    x 2     2 bin    /bin      /usr/sbin/nologin
3 sys    x 3     3 sys    /dev      /usr/sbin/nologin
  sync   x 4 65534 sync   /bin      /bin/sync

或者3目邏輯運算:

$ perl -pe '$_ = /nologin/ ? ++$num." $_" : "  $_"' file.log

更規範的格式化可以使用printf來對齊,因為這裡我使用-p選項,使用使用sprintf格式化字串儲存到$_變數上。

$ perl -pe '
    $_ = do {
        if(/nologin/){
            sprintf("%-3s %s", ++$num, $_);
        }else{
            sprintf("%-3s %s","", $_);
        }}' file.log

    root   x 0     0 root   /root     /bin/bash
1   daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
2   bin    x 2     2 bin    /bin      /usr/sbin/nologin
3   sys    x 3     3 sys    /dev      /usr/sbin/nologin
    sync   x 4 65534 sync   /bin      /bin/sync

輸出匹配行及行號

例如輸出能匹配"nologin"的行以及它們的行號。

因為只輸出某些匹配行,而不是所有行,所以不使用-p選項。

$ perl -ne 'print "$. $_" if /nologin/' file.log
2 daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
3 bin    x 2     2 bin    /bin      /usr/sbin/nologin
4 sys    x 3     3 sys    /dev      /usr/sbin/nologin

如果匹配行的行號要獨立計數,則不使用$.,自己寫個自增的計數器即可:

$ perl -ne 'print ++$num." $_" if /nologin/' file.log
1 daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
2 bin    x 2     2 bin    /bin      /usr/sbin/nologin
3 sys    x 3     3 sys    /dev      /usr/sbin/nologin

統計行數

$ perl -lne 'END{print $.}' file.log
5

這裡使用END語句塊,表示執行完主邏輯程式碼後程序退出前執行的,因為這個示例中沒有主邏輯程式碼,所以讀取完所有行後就會執行END語句塊。另外,這裡的-l選項主要用來為print追加換行符。

上面的語句僅會輸出行號,不會輸出檔名,而且多個檔案的時候只會輸出總行數,而不是每個檔案單獨統計。

還有其它實現方式,介紹兩個:

# (1)
perl -le 'print scalar(@tmp = <>)' file.log
perl -le 'print [email protected] = <>' file.log

# (2)
perl -ne '}{print $.' file.log

上面的方式(1)沒有使用-p和-n,所以自己在-e表示式中寫<>。而@tmp = <>是讓<>以列表的方式一次性讀取所有行,然後scalar強制轉換其為標量上下文,於是得到行數量。它等價於scalar( () = <> ),還等價於$num = () =<>

scalar()可以替換成~~符號,它是兩個位元位取反操作,等價於什麼都不做,但它工作在標量上下文,所以可以用來轉換上下文。

上面的方式(2)使用的是超乎想象的}{,這不是Perl中的什麼特殊符號,僅僅只是結合-n選項時的一個技巧。-n選項的程式碼邏輯如下:

while(<>){
    ... -e expression here
}

所以,-e中指定}{print $.表示破壞原始的-n邏輯,使之變成下面的邏輯:

while(<>){
    }{print $.
}

這個格式化一下就是:

while(<>){}
{
    print $.
}

也就是說,while迴圈體內不做任何操作,直到<>讀取完成後,while結束,然後執行一次性語句塊{print $.}。所以,-ne }{xxx等價於在END語句塊中執行xxx操作。

模仿 wc -l

wc -l會單獨輸出每個檔案的行數,並總計所有檔案的行數。例如:

$ wc -l file.log
5 file.log

$ wc -l file.log paragraph.log
  5 file.log
 18 paragraph.log
 23 total

所以,這也可以使用perl一行式程式來實現。因為這個邏輯中需要單獨統計每個檔案,所以必須顯式區分每個檔案。使用eof判斷每個檔案的尾部即可。

$ perl -M'List::Util qw(sum)' -lne '
        # 將@ARGV儲存起來,以便後續能夠按先後順序獲取所有檔名
        BEGIN{
            @files = @ARGV;
        }
        # 每個檔案處理完時,儲存行和檔案資訊,並關閉檔案以便重置行號計數器
        if(eof){
            # 將每個檔案的行數註冊到一個hash結構中
            # hash的key為當前處理的檔名
            $line_filename{$ARGV} = $.;
            close ARGV;
        }
        END{
            # 獲取總行數以及總行數的字元長度,以便格式化對齊
            $total_lines = sum values %line_filename;
            $longest = length $total_lines;

            # 輸出每個檔案對應的行數及檔名,且按照@ARGV的順序輸出
            foreach (@files){
                printf "%${longest}d %s\n",$line_filename{$_},$_;
            }
            # 輸出總行數
            print "$total_lines total";
        }
    ' file.log paragraph.log

這是遇到的第一個比較大的程式,這樣的邏輯應該寫成Perl指令碼而不是一行式程式。不過這裡的幾個知識點很適合引入Perl一行式。

先分析下這段程式的邏輯:要統計總行數,且要輸出每個檔案對應的行數,輸出時還要進行格式化對齊,所以先將每個檔案對應的行數儲存到一個hash結構中,最後在END語句塊中計算總行數,並計算總行數有多少個字元以便確定格式化對齊時的字元數量。

上面使用了-M選項,它表示匯入一個模組。此處所匯入的模組是List::Util模組,它是額外的列表(陣列)工具模組,該模組中有不少處理列表的工具。例如這裡使用qw(sum)表示匯入這個模組中的sum函式,用於對列表元素進行加總。如果不寫qw(sum),那麼在使用sum函式的時候,需要寫完整的名稱List::Util::sum @arr

"-l"選項的目的是給print函式追加換行符。

另外這裡使用了BEGIN語句塊來儲存@ARGV陣列,雖然%line_filename中也能取得所有的檔名,但hash結構中的元素是無序的,要保證檔名的順序,只能使用陣列(列表)來儲存。再者,因為@ARGV中的引數檔案會隨著<>的讀取而被剔除出@ARGV,所以應該在BEGIN中對@ARGV進行儲存。

統計非空白行的行數

用計數器實現非常簡單:

$ perl -lne '++$num if /\S/;END{print $num+0;}' paragraph.log

這裡的邏輯非常簡單。唯一需要注意的是$num+0,因為檔案可能是空的,使得END語句塊中的$num變數仍處於未定義狀態。加上一個+0,可以保證它會輸出數值格式,未定義時則輸出0。

為了保證得到數值,可以使用int函式進行轉換:

$ perl -lne '++$num if /\S/;END{print int $num;}' paragraph.log

我準備在這裡引入grep函式的簡單用法。

Shell中有個grep命令可以用來匹配內容,在Perl中也有一個grep函式,它的簡單工作方式可以類同於shell的grep命令,用於篩選列表中符合條件的元素,並將這些元素構成一個新的列表。比如能正則匹配的元素、操作後布林真的元素。

所以,可以使用grep函式來匹配非空白行:

$ perl -e '@lines = grep /\S/,<>;print "@lines"' paragraph.log

grep期待的是列表上下文,使得<>一次性讀完所有行形成一個列表,然後grep對這個列表的每個元素進行篩選,只要是非空白行都放入一個新的列表。

那麼要統計非空白行數就非常簡單了,直接將grep的結果轉換成標量上下文就可以。

$ perl -le 'print ~~grep /\S/,<>' paragraph.log

前面說過,~~可以用來轉換標量上下文。

其實Perl grep函式要強大的多,它支援完整的流程控制邏輯。如有需要,參考Perl grep函式

計數每個單詞

為檔案中每行中的單詞進行計數。

$ perl -pe 's/(\w+)/"<".++$num.">.$1"/ge' file.log

<1>.first <2>.paragraph:
        <3>.first <4>.line <5>.in <6>.1st <7>.paragraph
        <8>.second <9>.line <10>.in <11>.1st <12>.paragraph
        <13>.third <14>.line <15>.in <16>.1st <17>.paragraph

這裡使用s///命令的e修飾符。該修飾符可以評估s/reg/replacement/的replacement部分,將其作為Perl的程式碼被perl執行,然後進行s替換操作。

正如上面的示例,每一行匹配後評估replacement部分是"<".++$num.">.$1",被perl執行的話,這裡的++$num就會在perl環境下執行自增操作,$1也會被替換成已匹配的分組,最後完成s的替換。

下面的示例可能會更容易理解一些:

$ perl -pe 's/(\w+)/++$num/ge' file.log

1 2:
        3 4 5 6 7
        8 9 10 11 12
        13 14 15 16 17

如果想讓它們頂格輸出,可以繼續刪除行首空白:

$ perl -pe 's/(\w+)/++$num/ge;s/^\s+//' file.log
1 2:
3 4 5 6 7
8 9 10 11 12
13 14 15 16 17

再比如,每行的單詞單獨計數,只需在每次讀入行的開頭進行計數器重置即可:

$ perl -pe '$num=0;s/(\w+)/"<".++$num.">.$1"/ge' file.log

<1>.first <2>.paragraph:
        <1>.first <2>.line <3>.in <4>.1st <5>.paragraph
        <1>.second <2>.line <3>.in <4>.1st <5>.paragraph
        <1>.third <2>.line <3>.in <4>.1st <5>.paragraph