常用句型
程式範例:
- rencase: 一次更改許多檔案名稱, 大小寫對調。 (檔名從 stdin 讀)
- userinfo: 印出某使用者的公開個人資訊。
- datefmt: 把所有 "星期幾" 與 "幾月" 的英文全部改成月份。
- length: 數一數每個檔案的每個非空白列各有多少字元 (進階)。
三個使用 =~ 符號的運算子
$var =~ tr/.../.../;
把變數 $var 內的 ... 字元都逐一代換成 ... 字元 兩串 ... 通常長度一樣. 想成是查 "字元典" 翻譯。 (較不常用)$var =~ s/.../.../;
把變數 $var 內的第一個 ... 子字串整個代換成 ... 子字串。if ($var =~ m/.../) { ... }
詢問變數 $var 裡面有沒有 ... 這個子字串呢?
注意:
- 前兩項功能是破壞性的 (destructive), $var 的內容可能因而改變; 第三項是非破壞性的 (non-destructive)。
- 後兩項功能在現實生活中較常用到; 兩者都支援 regular expression。
- 其實可以用其他標點符號來取代斜線, 只要一句話內前後一致就好。
- 其實第三項功能的 m 可以省略掉
- 如果 $var 是 $_ 則可以用簡寫, 把 $var =~ 全部省略掉。
代換字串 ... =~ s/.../.../
時, 可以加上一些選項, 例如
i (ignore) 表示忽略大小寫 (比對成功的條件變得更寬鬆); g (global)
表示全面代換, 不只代換第一個比對成功的子字串。 例如 $x =
"...Sea...sea...SEA...sea..."
究竟其中那幾個
sea 會被代換掉呢?
...Sea... | ...sea... | ...SEA... | ...sea... | |
(都不加) | v | |||
i | v | |||
g | v | v | ||
ig | v | v | v | v |
神奇的內定參數 $_
在很多場合下, 參數可以省略不寫, 而此時 perl 自動以 $_ 作為內定參數. 例如:
- 用 <FH> 從檔案讀入的一列, 自動存在 $_ 中.
- 許多運算子的參數, 例如 tr, s, m.
- 許多函數的參數, 例如 print, chomp, split, ...
- foreach 的 dummy variable.
檔案讀寫
- 用 open 開啟, 用 close 關閉. Open 的第一個參數叫做 file handle, 是你自己命名的檔案代號. 從 open 以後, 都用這個 file handle 來傳給處理檔案函數當做參數. File handle 習慣上用大寫字串.
- 開啟時, 檔名前面加 < 表示要讀; 加 > 表示要寫入, 會毀掉原內容; 加 >> 也表示要寫, 但新資料接續在原有的資料之後;
- STDIN, STDOUT, STDERR 這三個特殊的 file handles 不必 open 與 close.
- 最簡單的「讀取檔案」句型:
while (<ABC>) { ... }
理解為: 「每次從 ABC 這個 file handle 讀取一列, 放入 $_ 這個變數裡面, 然後 ... (處理 $_), 直到檔案讀完, 沒有資料為止 (迴圈自動結束).」 注意: <ABC> 語法脫離 while 迴圈單獨使用的狀況比較複雜, 詳見 perlop(1) 的 I/O Operators 單元. 目前請固定將它放在 while 迴圈的 (...) 當中. - 寫到檔案去:
print ABC "good morning ", $name, "\n";
注意: 這裡要把整句話理解成三段: print, 檔案, 以及用逗點分開的一串資料. 檔案與資料之間不可以有逗點! 所以平常的 print 相當於 print STDOUT ...
另外, 因為有好幾次看到有些同學或朋友不約而同提出這個 (錯誤的) 寫法, 所以特別說明一下:
錯! open F, "..."; 錯!
錯! open G, "..."; 錯!
錯! while (<F>) { 錯!
錯! \.. / 錯!
錯! while/(<G>) { 錯!
錯! \ /.. 錯!
錯! X... 錯!
錯! / \.. 錯!
錯! }/ \ 錯!
錯! /.. \ 錯!
錯! } 錯!
錯! close G; 錯!
錯! close F; 錯!
可以看出程式作者希望同時處理兩個檔案, 每從 F 讀一列,
就要從頭到尾把 G 掃描一遍; 再從 F 讀一列, 又把 G 掃描一遍。
這個邏輯本身沒有問題, 就像兩個 while 迴圈或兩個 for
迴圈疊在一起一樣。 問題是: G 只 open 一次! 於是讀到 F 的第二列時, G
的檔案指標還在最尾巴, 沒有回頭。 所以從此以後, 內層迴圈每執行必 false
-- 都執行零次。 解決之道有二。 其一是將 G 的 open 與 close 移到 F
的迴圈內。 不過這樣重複讀檔, 效率可能比較低。 較佳的方式是:
看看兩個檔案通常是誰比較小? 把較小的檔案一口氣讀入一個陣列,
然後只對較大的檔案用 while
, 像這樣:
open G, "...";
@G_data = <G>;
close G;
open F, "...";
while (<F>) {
foreach (@G_data) {
...
}
}
close F;
不指定檔案, 讓 perl 替你傷腦筋
這句話 while (<>) { ... }
與 while
(<ABC>) 這類句型意義類似, 但後者只針對 ABC 這個單一的檔案處理;
而前者則不指定要處理那個檔案, 作用是:
- 若使用者未在命令列上給參數, 則你程式的效果相當於
while (<STDIN>) { ... }
也就是「癡癡地等, 每次從鍵盤上讀取一列 ...」 - 若命令列上有參數, 則把命令列上的每個參數當做一個檔案名稱, 從第一個檔案的第一列開始讀起, 每個檔案讀完後, 依序讀下個檔案, ... 彷彿所有檔案的內容串成一個檔案一樣, 迴圈一直執行到最後一個檔案的最後一列讀完為止. Perl 會自動幫你 open/close 每個檔案, 而 $ARGV 內則存有 "目前正在處理的檔案" 的名稱.
聽起來很複雜; 用起來很簡單: 這樣的安排可以讓我們寫的 perl 程式與許多系統工具一樣 (例如 sort, grep, ...), 既可處理一般檔案, 又可當做 filter 放在 pipe 當中, 而程式設計師 (我們) 卻不需要操心如何分開處理這兩種不同的狀況. 此外, 處理一般檔案時, 我們不必多費心, 自然就可以一次處理很多個檔案.
隱含迴圈
- 若在命令列上加上 -n 選項, 就彷彿在你的程式最外面包上一個
while (<>) { ... }
迴圈一樣. 換句話說, 你只需要寫迴圈裡面的部分, 專心思考「如何處理一列」就好了. - 若在命令列上加上 -p 選項, 就彷彿在你的程式最外面包上一個
while (<>) { ... print; }
迴圈一樣. 換句話說, 效果類似 -n, 但在迴圈最底部更把 $_ 的值印出來. 以上說明稍微簡化, 不完全正確; 詳請參閱手冊 perlrun(1). - 因此我們經常可以用
perl -ne ...
來取代 shell 底下的 grep 命令; 而用perl -pe ...
來取代 sed 命令. - 使用 -p 或 -n 時, 如果需要在進入迴圈之前/出了迴圈之後,
先/再多做一些事, 可以用
BEGIN { ... }
及END{ ... }
例如宣告變數, 可能就需要放在 BEGIN 之內; 列印最後統計的結果, 可能就需要放在 END 之內. 詳見 perlmod(1) - Q: 本篇最前面的範例程式 length, 如果這麼使用:
./length this is a book
會印出什麼? 下例中的迴圈版求和程式, 如果這麼使用:perl -ne '...' 23 45 99
會發生什麼事? 會印出幾個總和?
幾個範例: 左邊是 「完整版」, 右邊是 「隱含迴圈版」
#!/usr/bin/perl -w
while (<STDIN>) {
print length($_),"\n"; perl -ne 'print length($_), "\n"' < 檔案
}
----------------------------------------------------------------------
#!/usr/bin/perl -w
while (<STDIN>) {
$sum += $_; perl -ne '$sum+=$_;END{print"$sum\n";}' < 檔案
}
print "$sum\n";
----------------------------------------------------------------------
#!/usr/bin/perl -w #!/usr/bin/perl -wn
while (<STDIN>) {
chomp $_; chomp $_;
$oldfn = $_; $oldfn = $_;
$_ =~ tr/a-zA-Z/A-Za-z/; $_ =~ tr/a-zA-Z/A-Za-z/;
rename $oldfn, $_; rename $oldfn, $_;
}
Here Document
當你發現你的程式寫成一連串的 print "..." 時, 可以用 here document 來化簡, 直接寫出要印的東西就好, 省略掉重複的 print 敘述和一大堆引號.
- 在第一個 print 之後用 <<"name" (name 是你自己隨便取的一個名字) 從此以下都當成要印的資料 (而不是要執行的程式), 一直到 name 再度出現為止.
- << 與 name 之間不可以有空格.
- 標示結束的 name 必須單獨出現在一列, 前後不可以有空格.
- 這當中大部分東西都會原封不動地印出來, 但遇到 $... 及 @... 還是會造成變數代換. 如果當初是用 <<'name' 那麼就連變數代換都不做了.
- 詳見 perlfaq4(1) 的 "Why don't my <<HERE documents work?" 與 perldata(1) 的 "here-doc"
其他常識
- 不論是 array 或 hash, 設定初始值時都是用 小括弧!
- 現在知道 "Use of uninitialized value at line ... chunk ..." 這個錯誤訊息的意思了嗎? line 與 chunk 是指錯誤發生時執行到程式的第幾列, 正在處理資料的第幾列. 務必養成 從錯誤訊息當中學習 的習慣, 進步才快. 如果你用到一個未曾設定初始值的變數, 就會出現這個訊息. 但是 ++, --, +=, -=, ... .= 等等運算子容許未設定初始值的變數 (想想也蠻合理的). 要判斷一個變數是否有定義, 可以用 defined. 見 perlfunc(1).
-
如果你的程式只讀一個資料檔, 用
while (<>)
或許比「把程式開啟的檔案名稱寫死」要好, 因為- 使用者可以自由決定要選用 (處理) 那一個資料檔, 甚或不要用檔案當做輸入資料, 而是用來自 pipe 的資料.
- 我們不必自己開啟/關閉檔案, perl 會代勞
- 自動可以一次處理多個檔案
- 有 -n 與 -p 可以幫助我們隱藏迴圈
- split 真的非常好用. 不只是字元可以用來分隔欄位, 字串也可以. 也不只有固定的字串才能用, 甚至可以是 regular expression. 但要注意: 若寫 split / /, ... 則連續的兩個空格之間算做有一個空欄位; 若寫 split /\s+/, ... 則連續的空格算做一個分隔符號; 若寫 split " ", ... 意思與 split /\s+/ 相同, 而且字串開頭的地方如果有空格會被自動忽略. 總之, 比較常用的兩種寫法是: split /:/, ... (分隔符號不是空格時) 與 split " ", ... (分隔符號是空格時)
- 與 split 功能正好相反的是 join, 可以把一個陣列的所有元素串在一起, 變成一個字串.
作業
- 請試著用老實的方法模擬 while (<>) { ... } "如果命令列上沒有參數... 如果有 ..." 你就會了解 while (<>) { ... } 的語法幫你省了多少程式碼.
- 請解釋這句話的意義: perl -ne 'print if /abc/' data.txt 提示: 先把它改寫成完整的 perl 程式.
- 請解釋這句話的意義: perl -pe 's/abc/xy/g' data.txt 提示: 先把它改寫成完整的 perl 程式.
- 請解釋 上一篇 當中, 精簡版
get_field
的意義。 - 寫一個程式分析 last 命令的輸出, 統計 (月初以來) 每個使用者曾經上機多少次, 總共上機多少時間. 提示: 可能要用到 split(" ", ...) 與 substr
- 寫一個程式, 將文字檔當中所有的全形標點符號改成半形標點符號。
- 請分析
rpm -qa --qf '%12{SIZE} %-12{NAME} %{URL}\n'
根據套件來源網址的最高層網域 (網址尾巴, 例如 .edu .org .com ... 等等) 統計來自每個網域的套件個數, 與該網域所有套件大小總和。 提示: 要用到三次 split; 又, 因為 / 與 . 都有特殊意義, 所以拿它們當分隔符號時, 前面必須加上倒斜線, 像這樣:... split /\// ...
- 本頁最新版網址: https://frdm.cyut.edu.tw/~ckhung/b/pl/idiom.php; 您所看到的版本: February 14 2012 02:32:25.
- 作者: 朝陽科技大學 資訊管理系 洪朝貴
- 寶貝你我的地球, 請 減少列印, 多用背面, 丟棄時做垃圾分類。
- 本文件以 Creative Commons Attribution-ShareAlike License 或以 Free Document License 方式公開授權大眾自由複製/修改/散佈。