在 C 語言中,我們可以使用 sizeof() 來得知配置給變數的記憶體大小,有了變數的記憶體位址與大小,就可以直接存取變數內部的資料,這種直接存取記憶體的方式,在處理二進位資料時,是常見的手法。
在 Perl 中就沒辦法像 C 語言這樣直接存取記憶體,不過我們可以使用 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
文字處理
假設我們有一個文字檔案,其內容如下:如果想要使用 Perl 解析這類的資料,一般第一個會想到的就是 split() 函數,不過這裡的資料並沒有很明顯可以辨識的分隔字元,所以也沒辦法直接用 split() 來處理,大概只能用 substr():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
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
沒有留言:
張貼留言