古代某些電腦無法處理 8-bit 字元, 只能處理 7-bit 的 ASCII 碼 -- 基本上就是英文、 數字、 鍵盤上的標點符號等等。 當時如果兩部中文電腦要交換資料 (例如收發 e-mail), 就必須先把 8-bit 的中文轉碼變成 7-bit 的 ASCII 字元 (當然, 就會使用更多 bytes), 以免傳遞路徑當中的英文電腦 把多出來的一個 bit 砍掉, 遺失資料。 Quoted-Printable 編碼就是其中一種很簡單的轉碼方式。 本文介紹如何善用 perl 與 od 這兩個簡單工具, 不必寫程式就能將 qp 碼與 8-bit 碼互轉。
(沒有力氣讀完全文的讀者, 請至少要看一下 結論, 欣賞一下 「三句神功」 的成果有多麼精簡。)
首先下: echo '一中街' | od -t x1
輸出應該長這樣:
0000000 e4 b8 80 e4 b8 ad e8 a1 97 0a 0000012
意思是: "一" 的 utf8 碼是 e4 b8 80, "中" 的 utf8 碼是 e4 b8 ad; "街" 的 utf8 碼是 e8 a1 97, 而 0a 則是換列字元的 utf8 碼。 od 指令可以將輸入資料 (任何檔案, 包含各種無法閱讀的圖檔、 影片檔) 的每一個 byte 以十六進位方式印出來。
上述字串的 qp 編碼很簡單:
在每個 byte 的十六進位之前加上一個等號 "=", 並刪除空格,
變成這樣: =e4=b8=80=e4=b8=ad=e8=a1=97=0a
。
如果太長, 可以將它拆成好幾列
(但是不得將代表一個 byte 的三個字元拆開),
並且在拆開的地方補上一個等號, 表示下一列還有資料。
例如下面一段文字, 請剪貼到一個文字檔 asimov.txt 裡面:
歷史不斷地教育人類: 拉開時空距離, 往往更能夠清楚地觀察事理。 不過, 人類似乎從來不曾永久地學會這個課程。 -- 艾薩克 艾西莫夫 (科幻小說家)
然後下指令: od -t x1 < zz | perl -pe 's/^.{7}//; s/ /=/g;
s/$/=/' > asimov-qp.txt
產生如下的輸出:
=e6=ad=b7=e5=8f=b2=e4=b8=8d=e6=96=b7=e5=9c=b0=e6= =95=99=e8=82=b2=e4=ba=ba=e9=a1=9e=3a=20=e6=8b=89= ... =ab=20=28=e7=a7=91=e5=b9=bb=e5=b0=8f=e8=aa=aa=e5= =ae=b6=29=0a= =
最後, 用任何文字編輯器進入 asimov-qp.txt, 把最後兩列尾巴的等號拿掉 (檔案最後面只剩下 "=ae=b6=29=0a=" 和空列), 這就是一個 quoted-printable 的字串。
反過來, 如何將 qp 字串解碼呢?
第一步, 先將所有 「以等號結尾」 的每一列, 跟下一列串起來。
用 perl 的 regexp 來寫, 就是
perl -pe 's/=\n$//'
("把一列最尾巴的等號跟換列字元一起刪掉")。
為了週到與保險起見, (1)
不過 "\n" 是 linux 的換列字元;
若是 dos/windows 檔, 則是 "\r\n"。
此外, 為避免誤串, 要加強檢查條件, 只有當
「最後的等號之前, 出現一組三個長得像是
qp 編碼的字元 "等號-十六進位數字-十六進位數字"」
的時候, 才做代換。 當然, 要小心別誤刪這組數字。」
所以完整的寫法是:
perl -pe 's/(=[a-f\d][a-f\d])=\r?\n$/$1/i' < asimov-qp.txt > a1.txt
第二步, 把每一組的等號刪掉, 用後面兩個數字產生一個 8-bit 字元。
Perl 裡面有一個函數 hex() 可以將 "ff" 之類的十六進位字串轉成數字。
另有一個函數 chr(), 接受數字輸入, 然後輸出它所代表的一個 8-bit 字元。
例如 chr(0x41) 傳回 "A", chr (0x5a) 傳回 "Z"。
但是我們正在代換字串, 能呼叫 perl 的函數嗎?
用 s/abc/xyz/ 代換字串時, 在最後加上一個 "e" 選項,
可以叫 perl 別把 xyz 當成字串, 而要把它當成是程式碼來看待,
把它執行完的結果貼上去當成新的字串。
所以這句話的初版是:
perl -pe 's/=([a-f\d][a-f\d])/chr(hex($1))/ieg'
不過, 萬一正文裡面有一些字串看起來很像是 qp, 但其實並不是, 就遭糕了。
例如: "ENCODING=BASE64" 裡面的 "=BA" 會被當做是 qp (實際上並不是)。
用一個很簡單/不完整的測試稍微強化一下條件:
只有當這一列連續出現至少二組字串 (共六個字元) 的時候,
才進行轉換。 最終完整的第二句話變成這樣:
perl -pe 'next unless /(=[a-f\d][a-f\d]){2}/i; s/=([a-f\d][a-f\d])/chr(hex($1))/ieg' < a1.txt > a2.txt
進入第三步之前, 要先解釋一下為什麼寫這篇。 之前為了將手機通訊錄 vcf 檔和試算表認得的 csv 檔互轉, 所以寫了一支小小的 perl 程式 vcf2csv。 Android 手機所匯出的 vcf 檔裡面, 若含中文, 會出現一段 "...ENCODING=QUOTED-PRINTABLE..." 然後就是一串 qp 字串。 先前的版本直接對 qp 字串解碼, 然後把這一串宣告刪掉。 最近重新改寫, 覺得還是保留這一串比較保險。 但也不能直接保留, 而是必須告訴接手的軟體: 現在傳送的已經是未編碼的 8BIT 字串了。 所以第三步的動作是: 「凡是遇到中文列 (內含 "CHARSET=UTF-8") 就改宣告: 編碼已改為 qp」:
perl -pe 's/CHARSET=UTF-8.*?:/CHARSET=UTF-8;ENCODING=8BIT:/;' < a2.txt > a3.txt
把這三個動作合在一起, 就變成了 qp28bit 這個小小的 shell script。 這是新版的 vcf2csv 當中的第一個小程式。 請試試看用你所熟悉的任何一種語言來寫, 看看需要多少列。 然後, 你就會知道:
- 為什麼 perl 語言的發明人 Larry Wall 是我心中的神;
- 為什麼很多人喜歡 用 perl 寫詩;
- 為什麼學會用 perl 寫程式之後, 你幾乎可以不必寫程式 (這就是 「不戰而屈人之兵」 吧);
- 為什麼 我的 regular expressions 講義 要用 perl 做範例;
:-) 就算沒空學完整的 perl 語言, 光是學 regexp, 就已經很厲害了。 Regexp 是一種報酬率很高的學習投資, 請認真考慮吧!
http://people.ofset.org/~ckhung/p/vcf2csv/index.zh_TW.php
回覆刪除的連結是壞的
改好了, 謝謝 Shelandy!
刪除