在 Perl 中如果要處理二進位(Binary)的資料結構,通常都會使用 pack
與 unpack
來處理,這裡介紹這兩個函數的使用方法。
在 C 語言中,我們可以使用 sizeof()
來得知配置給變數的記憶體大小,有了變數的記憶體位址與大小,就可以直接存取變數內部的資料,這種直接存取記憶體的方式,在處理二進位資料時,是常見的手法。
pack()
與 unpack()
這兩個函數來將資料進行轉換,達到相同的功能。pack()
可以將變數中儲存的資料依照指定的格式樣板(template)轉換為一連串的位元組序列(byte sequence),而 unpack()
的功能則剛好相反,它是將位元組序列轉換回 Perl 的變數。
pack()
轉換過的資料都可以直接被 unpack()
轉換回來,有些時候需要一些技巧。您可能會問為什麼我們在 Perl 中會需要用到記憶體中二進位的資料?最常見的狀況就是當我們需要處理一些二進位檔案、設備(device)或是網路傳輸時,這類的 I/O 資料通常都會需要以二進位的方式表示;另外一的狀況就是使用 Perl 中沒有的系統呼叫(system call)時,有時也會需要將資料以 C 語言中儲存的方式傳入;甚至也可以將這樣的二進位資料處理方式應用在文字的處理上,以簡化處理的流程。
首先介紹 unpack()
的使用方式,假設我們有一串二進位的資料,想要轉換為十六進位的方式傾印(dump)出來,可以這樣寫:
#!/usr/bin/perl # 二進位的資料 $bin = "abcd"; # 轉換為十六進位的字串 $hex = unpack('H*', $bin); print "$hex\n";
輸出為
61626364
其中 unpack()
的第一個參數是指定資料格式的樣板(template),這個例子的 H*
則是代表任意個十六進位數字的字串,詳細的樣板說明可以參考 pack 函數的說明。
這裡輸出的數字就是 abcd
四個字母的 ASCII 碼,以小寫的 a
來說,其 ASCII 碼為 0x61
,所以輸出的前兩個數字就是 61
,其他以此類推。
如果要將十六進位的字串轉換為二進位的資料,可以使用 pack()
函數:
#!/usr/bin/perl # 十六進位的字串 $hex = "61626364"; # 轉換為二進位的資料 $bin = pack('H*', $hex); print "$bin\n";
輸出為
abcd
假設我們有一個文字檔案,其內容如下:
Date |Description | Income|Expenditure
01/24/2001 Zed’s Camel Emporium 1147.99
01/28/2001 Flea spray 24.99
01/29/2001 Camel rides to tourists 235.00
如果想要使用 Perl 解析這類的資料,一般第一個會想到的就是 split()
函數,不過這裡的資料並沒有很明顯可以辨識的分隔字元,所以也沒辦法直接用 split()
來處理,大概只能用 substr()
:
while (<>) { my $date = substr($_, 0, 11); my $desc = substr($_, 12, 27); my $income = substr($_, 40, 7); my $expend = substr($_, 52, 7); # ... }
雖然這樣的方式可以正常解析出這樣的資料,不過有點冗長。另一種常見的作法是改用常規表示法(regular expression)來匹配:
while (<>) { my($date, $desc, $income, $expend) = m|(\d\d/\d\d/\d{4}) (.{27}) (.{7})(.*)|; # ... }
不過這樣的程式碼可能也不是很好被閱讀或維護。
這種狀況我們可以使用 unpack()
函數來處理:
while (<>) { my($date, $desc, $income, $expend) = unpack("A10xA27xA7A*", $_); # ... }
這樣的程式碼會比較簡潔,而且容易閱讀,以下我們來解釋這裡的格式樣板 "A10xA27xA7A*"
所代表的意義。
首先我們要解析第一個欄位,也就是前 10 個字元,在 pack 的樣板表示法中,字元是以 A
來表示的,而 10 個字元則寫成 A10
,所以如果我們只是要解析前 10 個字元,就可以寫成
$date = unpack("A10", $_);
接著第一欄與第二欄之間有一個沒有用的空白字元,這個字元我們使用 x
來跳過(x
代表跳過一個位元組)。
而之後的第二欄與第三欄分別是 27 與 7 個字元,加上去之後就會變成
my($date, $description, $income) = unpack("A10xA27xA7", $_);
而最後一個欄位因為是選擇性的,有些行根本沒有,所以我們就用 A*
表示任意長度的字串,這樣只要是在這個之後的任何字元,都會被納入這一欄,如此一來就得到最後的結果:
my ($date, $description, $income, $expend) = unpack("A10xA27xA7xA*", $_);
假設我們想要計算收入與支出的總和並以同樣的格式輸出,可以這樣寫:
while (<>) { my ($date, $desc, $income, $expend) = unpack("A10xA27xA7xA*", $_); $tot_income += $income; $tot_expend += $expend; } $tot_income = sprintf("%.2f", $tot_income); $tot_expend = sprintf("%12.2f", $tot_expend); $date = POSIX::strftime("%m/%d/%Y", localtime); print pack("A11 A28 A8 A*", $date, "Totals", $tot_income, $tot_expend);
這樣其輸出就會跟原本的格式一致,而附加在原本的內容之後,就會變成這樣:
01/28/2001 Flea spray 24.99
01/29/2001 Camel rides to tourists 1235.00
03/23/2001 Totals 1235.00 1172.98
pack()
與 unpack()
中所使用的樣板有些差異,因為小寫的 x
在 pack()
的樣板中代表 null 字元,而我們排版需要的是一個空白字元,所以在 pack()
中要將 x
改為空白字元,這樣才能維持正確的排版。參考資料:perldoc、Perl Cookbook
繼續閱讀:Perl 程式設計教學