類Unix流編輯器sed線上極速入門 第三部分

helloxchen發表於2010-10-21
類Unix流編輯器sed線上極速入門 第三部分

強健的 sed

在 第二部分 sed 文章中,提供了一些示例來演示 sed 的工作原理,但是它們當中很少有示例能實際做特別 有用的事。在這篇 sed 系列的最後一部分文章中,我要改變那種方式,並使用 sed 來做實際的事。我將為您顯示幾個示例,它們不僅演示 sed 的能力,而且還做一些真正巧妙(和方便)的事。例如,在本文的後半部,將為您演示如何設計一個 sed 指令碼來將 .QIF 檔案從 Intuit 的 Quicken 金融程式轉換成具有良好格式的文字檔案。在那樣做之前,我們將看一下不怎麼複雜但卻很有用的 sed 指令碼。

文字轉換

第一個實際指令碼將 UNIX 風格的文字轉換成 DOS/Windows 格式。您可能知道,基於 DOS/Windows 的文字檔案在每一行末尾有一個 CR(回車)和 LF(換行),而 UNIX 文字只有一個換行。有時可能需要將某些 UNIX 文字移至 Windows 系統,該指令碼將為您執行必需的格式轉換。
  1. $ sed -e 's/$/r/' myunix.txt > mydos.txt
複製程式碼
在該指令碼中,'$' 規則表示式將與行的末尾匹配,而 'r' 告訴 sed 在其之前插入一個回車。在換行之前插入回車,立即,每一行就以 CR/LF 結束。請注意,僅當使用 GNU sed 3.02.80 或以後的版本時,才會用 CR 替換 'r'。如果還沒有安裝 GNU sed 3.02.80,請在第一部分中檢視如何這樣做的說明。

我已記不清有多少次在下載一些示例指令碼或 C 程式碼之後,卻發現它是 DOS/Windows 格式。雖然很多程式不在乎 DOS/Windows 格式的 CR/LF 文字檔案,但是有幾個程式卻在乎 -- 最著名的是 bash,只要一遇到回車,它就會出問題。以下 sed 呼叫將把 DOS/Windows 格式的文字轉換成可信賴的 UNIX 格式:
  1. $ sed -e 's/.$//' mydos.txt > myunix.txt
複製程式碼
該指令碼的工作原理很簡單:替代規則表示式與一行的最末字元匹配,而該字元恰好就是回車。我們用空字元替換它,從而將其從輸出中徹底刪除。如果使用該指令碼並注意到已經刪除了輸出中每行的最末字元,那麼,您就指定了已經是 UNIX 格式的文字檔案。也就沒必要那樣做了!

反轉行

下面是另一個方便的小指令碼。與大多數 Linux 發行版中包括的 "tac" 命令一樣,該指令碼將反轉檔案中行的次序。"tac" 這個名稱可能會給人以誤導,因為 "tac" 不反轉行中字元的位置(左和右),而是反轉檔案中行的位置(上和下)。用 "tac" 處理以下檔案:
foo bar oni


....將產生以下輸出:
oni bar foo


可以用以下 sed 指令碼達到相同目的:
  1. $ sed -e '1!G;h;$!d' forward.txt > backward.txt
複製程式碼
如果登入到恰巧沒有 "tac" 命令的 FreeBSD 系統,將發現該 sed 指令碼很有用。雖然方便,但最好還是知道該指令碼為什麼那樣做。讓我們對它進行討論。

反轉解釋

首先,該指令碼包含三個由分號隔開的單獨 sed 命令:'1!G'、'h' 和 '$!d'。現在,需要好好理解用於第一個和第三個命令的地址。如果第一個命令是 '1G',則 'G' 命令將只應用第一行。然而,還有一個 '!' 字元 -- 該 '!' 字元 忽略該地址,即,'G' 命令將應用到除第一行之外的 所有行。'$!d' 命令與之類似。如果命令是 '$d',則將只把 'd' 命令應用到檔案中的最後一行('$' 地址是指定最後一行的簡單方式)。然而,有了 '!' 之後,'$!d' 將把 'd' 命令應用到除最後一行之外的 所有行。現在,我們所要理解的是這些命令本身做什麼。

當對上面的文字檔案執行反轉指令碼時,首先執行的命令是 'h'。該命令告訴 sed 將模式空間(儲存正在處理的當前行的緩衝區)的內容複製到保留空間(臨時緩衝區)。然後,執行 'd' 命令,該命令從模式空間中刪除 "foo",以便在對這一行執行完所有命令之後不列印它。

現在,第二行。在將 "bar" 讀入模式空間之後,執行 'G' 命令,該命令將保留空間的內容 ("foon") 附加到模式空間 ("barn"),使模式空間的內容為 "barnfoon"。'h' 命令將該內容放回保留空間保護起來,然後,'d' 從模式空間刪除該行,以便不列印它。

對於最後的 "oni" 行,除了不刪除模式空間的內容(由於 'd' 之前的 '$!')以及將模式空間的內容(三行)列印到標準輸出之外,重複同樣的步驟。

現在,要用 sed 執行一些強大的資料轉換。

sed QIF 魔法

過去幾個星期,我一直想買一份 Quicken來結算我的銀行帳戶。Quicken 是一個非常好的金融程式,當然會成功地完成這項工作。但是,經過考慮之後,我覺得自己可以輕易編寫某個軟體來結算我的支票簿。我想,畢竟,我是個軟體開發人員!

我開發了一個很好的小型支票簿結算程式(使用 awk),它透過分析包含我的所有交易的文字檔案的語法來計算餘額。略微調整之後,我將其改進,以便可以象 Quicken 那樣跟蹤不同的貸款和借款類別。但是,我還要新增一個特性。最近,我將帳戶轉移到一家有聯機 Web 帳戶介面的銀行。有一天,我注意到,這家銀行的 Web 站點允許以 Quicken 的 .QIF 格式下載我的帳戶資訊。我馬上覺得,如果可以將該資訊轉換成文字格式,那就太棒了。

兩種格式的故事

在檢視 QIF 格式之前,先看一下我的 checkbook.txt 格式:
28 Aug 2000 food - - Y Supermarket 30.94 25 Aug 2000 watr - 103 Y Check 103 52.86


在我的檔案中,所有欄位都由一個或多個製表符分開,每個交易佔據一行。日期之後的下一個欄位列出支出型別(如果是收入項,則為 "-")。第三個欄位列出收入型別(如果是支出項,則為 "-")。然後,是一個支票號欄位(如果為空,則還是 "-"),一個交易完成欄位("Y" 或 "N"),一個註釋和一個美元金額欄位。現在,讓我們看一下 QIF 格式。當用文字檢視器檢視下載的 QIF 檔案時,它看起來如下:
!Type:Bank D08/28/2000 T-8.15 N PCHECKCARD SUPERMARKET ^ D08/28/2000 T-8.25 N PCHECKCARD PUNJAB RESTAURANT ^ D08/28/2000 T-17.17 N PCHECKCARD SUPERMARKET


瀏覽過檔案之後,不難猜出其格式 -- 忽略第一行,其餘的格式如下:

D
T
N
P
^ (這是欄位分隔符)


開始處理

在處理象這樣重要的 sed 專案時,不要氣餒 -- sed 允許您將資料逐漸修改成最終形式。在進行當中,可以繼續細化 sed 指令碼,直到輸出與預期的完全一樣為止。無需在試第一次時就保證其完全正確。

要開始,首先建立一個名為 "qiftrans.sed" 的檔案,然後開始修改資料:
  1. 1d /^^/d s/[[:cntrl:]]//g
複製程式碼
第一個 '1d' 命令刪除第一行,第二個命令從輸出除去那些討厭的 '^' 字元。最後一行除去檔案中可能存在的任何控制字元。既然在處理外來檔案格式,我想消除在中途遇到任何控制字元的風險。到目前為止,一切順利。現在,要向該基本指令碼中新增一些處理功能:
  1. 1d /^^/d s/[[:cntrl:]]//g /^D/ {
  2. s/^D(.*)/1tOUTYtINNYt/
  3. s/^01/Jan/ s/^02/Feb/
  4. s/^03/Mar/ s/^04/Apr/
  5. s/^05/May/ s/^06/Jun/
  6. s/^07/Jul/ s/^08/Aug/
  7. s/^09/Sep/ s/^10/Oct/
  8. s/^11/Nov/ s/^12/Dec/
  9. s:^(.*)/(.*)/(.*):2 1 3: }
複製程式碼
首先,新增一個 '/^D/' 地址,以便 sed 只在遇到 QIF 資料欄位的第一個字元 'D' 時才開始處理。當 sed 將這樣一行讀入其模式空間時,將按順序執行花括號中的所有命令。

花括號中的第一個命令將把如下行:
D08/28/2000


變換成:
08/28/2000 OUTY INNY


當然,現在的格式還不完美,但沒關係。我們將在進行過程中逐漸細化模式空間的內容。後面 12 行的最後效果是將資料變換成三個字母的格式,最後一行從資料中除去三個斜槓。最後得到這一行:
Aug 28 2000 OUTY INNY



OUTY 和 INNY 欄位是佔位符,以後將被替換。現在還不能確定它們,因為如果美元金額為負,將把 OUTY 和 INNY 設定成 "misc" 和 "-",但是,如果美元金額為正,將分別把它們更改成 "-" 和 "inco"。既然還沒有讀入美元金額,所以,需要暫時使用佔位符。

細化

現在進一步細化:
  1. 1d /^^/d s/[[:cntrl:]]//g /^D/ {
  2. s/^D(.*)/1tOUTYtINNYt/
  3. s/^01/Jan/ s/^02/Feb/
  4. s/^03/Mar/ s/^04/Apr/
  5. s/^05/May/ s/^06/Jun/
  6. s/^07/Jul/ s/^08/Aug/
  7. s/^09/Sep/ s/^10/Oct/
  8. s/^11/Nov/ s/^12/Dec/
  9. s:^(.*)/(.*)/(.*):2 1 3:
  10. N N N
  11. s/nT(.*)nN(.*)nP(.*)/NUM2NUMttYtt3tAMT1AMT/
  12. s/NUMNUM/-/ s/NUM([0-9]*)NUM/1/
  13. s/([0-9]),/1/ }
複製程式碼
後七行有些複雜,所以將詳細討論它們。首先,連續使用三個 'N' 命令。'N' 命令告訴 sed 將 下一行讀入輸入中,然後將其附加到當前模式空間。這三個 'N' 命令導致將下三行附加到當前模式空間緩衝區,現在這一行看起來如下:
28 Aug 2000 OUTY INNY nT-8.15nNnPCHECKCARD SUPERMARKET


sed 的模式空間變得很難看 -- 需要除去額外的新行,並執行某些附加的格式化。要這樣做,將使用替代命令。要匹配的模式為:
  1. 'nT.*nN.*nP.*'
複製程式碼
這將與後面依次跟有 'T'、零或多個字元、新行、'N'、任何數量的字元、新行、'P'、以及任何數量字元的新行匹配。呀!這個規則表示式將與剛剛附加到模式空間的三行的全部內容匹配。但我們要重新格式化該區域,而不是整個替換它。美元金額、支票號(如果有的話)和描述需要出現在替換字串中。要這樣做,我們用帶有反斜槓的圓括號括起那些“感興趣部分”,以便可以在替換字串中引用它們(使用 '1'、'2 和 '3' 來告訴 sed 將它們插入到何處)。以下是最後的命令:
  1. s/nT(.*)nN(.*)nP(.*)/NUM2NUMttYtt3tAMT1AMT/
複製程式碼
該命令將我們的行變換成:
  1. 28 Aug 2000 OUTY INNY NUMNUM Y CHECKCARD SUPERMARKET AMT-8.15AMT
複製程式碼
雖然該行正變得好一些,但是,有幾件事一看就有點...啊...有趣。首先是那個愚蠢的 "NUMNUM" 字串 -- 其目的何在?如果檢視 sed 指令碼的後兩行,就會發現其目的,後兩行將把 "NUMNUM" 替換成 "-",而把 "NUM""NUM" 替換成 。如您所見,用愚蠢的標記括起支票號允許我們在該欄位為空時方便地插入一個 "-"。

結束嘗試

最後一行除去數字後的逗號。它把如 "3,231.00" 這樣的美元金額轉換成我使用的格式 "3231.00"。現在,讓我們看一下最終指令碼:

最終的“QIF 到文字”指令碼
  1. 1d /^^/d s/[[:cntrl:]]//g /^D/ { s/^D(.*)/1tOUTYtINNYt/
  2. s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ s/^05/May/
  3. s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/
  4. s/^11/Nov/ s/^12/Dec/ s:^(.*)/(.*)/(.*):2 1 3:
  5. N N N s/nT(.*)nN(.*)nP(.*)/NUM2NUMttYtt3tAMT1AMT/
  6. s/NUMNUM/-/ s/NUM([0-9]*)NUM/1/ s/([0-9]),/1/
  7. /AMT-[0-9]*.[0-9]*AMT/b fixnegs
  8. s/AMT(.*)AMT/1/ s/OUTY/-/ s/INNY/inco/
  9. b done :fixnegs s/AMT-(.*)AMT/1/ s/OUTY/misc/
  10. s/INNY/-/ :done }
複製程式碼
附加的十一行使用替代和一些分支功能來美化輸出。首先看一下這行:
  1. /AMT-[0-9]*.[0-9]*AMT/b fixnegs
複製程式碼
該行包含一個格式為 "/regexp/b label" 的分支命令。如果模式空間與規則表示式匹配,sed 將分支到 fixnegs 標號。您應該可以輕易找到該標號,它在程式碼中為 ":fixnegs"。如果規則表示式不匹配,則以常規方式繼續處理下一個命令。

既然您理解該命令本身的工作原理,讓我們看一下分支。如果看一下分支規則表示式,將看到它與後面依次跟有 '-'、任意數量的數字、一個 '.'、任意數量的數字和 'AMT' 的字串 'AMT' 匹配。就象我確信您已猜到一樣,該規則表示式專門處理負的美元金額。在這之前,用 'ATM' 括起美元金額,以便以後可以輕易找到它。因為規則表示式只與以 '-' 開始的美元金額匹配,所以,該分支只在恰巧處理借款時才發生。如果正處理貸款,應該將 OUTY 設定成 'misc',將 INNY 設定成 '-',並且應該除去貸款數量前面的負號。如果跟蹤程式碼的流程,將看到實際情況正是這樣。如果不執行分支,則用 '-' 替換 OUTY,用 'inco' 替換 INNY。完成了!現在輸出行是完美的:
28 Aug 2000 misc - - Y CHECKCARD SUPERMARKET -8.15


別犯糊塗

如您所見,只要循序漸進地解決問題,使用 sed 轉換資料就沒有那麼難。不要試圖使用一個 sed 命令或一下子解決所有問題。相反,要朝著目標逐步進行,並不斷改進 sed 指令碼,直到其輸出正如您希望那樣為止。sed 有許多功能,希望您已非常熟悉其內部工作原理並繼續努力以進一步掌握它!
[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040130/,如需轉載,請註明出處,否則將追究法律責任。

相關文章