正規表示式的入門與應用(二)

    張耀仁
    E-mail: changyj@rtfiber.com.tw
    http://www.rtfiber.com.tw/~changyj
    刊登於 Linuxer 第四期 pp.152-161
    作者同時譯有 Linux 核心研究篇
    著作權所有﹐如欲轉載請與筆者聯絡
    *筆者整理了一些與 sed 相關的資源放在網頁上
    *謝謝馬兒(marr@xlinux.com) 先生的潤稿

    在上期的內容中﹐ 我們介紹了正規表示式(regular expression﹐簡稱 RE)的基本觀念﹐ 並利用 grep 與 sed 示範一些 RE 的用途。 本期的重點在於介紹 sed 的編輯命令﹐包含了
    • 刪除某些字串
    • 取用符合 RE 字串的某些部分
    • 定址-限制 sed 只對某些資料行執行編輯命令
    • 位址與正規表示式的結合
    • 刪除資料行
    • 不對某些資料行執行編輯命令
    • 字的邊界
    • 限制符合正規表示式的次數
    • 對一資料行執行多個編輯命令

    (註:上一期經修正後的版本請到 Linuxers 或筆者的網頁中閱讀)

    首先筆者提出一個具有實用性的問題﹐一方面讓讀者複習上期的內容﹐ 另一方面則介紹如何『刪除』某些字串 (為讓讀者易於解讀﹐在某些地方我們會以 來表示空格):

    [問題 8]

      請利用 sed 完成以下的工作:
      1. 在每一行的最前端(以下簡稱為『行首』)加上 3 個空格。
      2. 把行首所有的空格﹐不論多少個﹐一律刪除。
      3. 把每一行中﹐所有兩個(含)以上的空格以一個空格代替。
    [說明]
    1. 這項工作可用 sed 的取代命令 s 來處理﹐其中利用了 ^ 這個定位字元﹐它代表行首﹐也就是第一個字元的前方。 編輯命令

      s/^//

      會在該位置加入三個空格而達到所要求的效果:

      由於管線符號(|)之後必須有 shell 命令﹐ 因此上例中在 | 後直接按 ENTER 時﹐shell 認為尚未結束而顯示 > 這個提示符號要求我們繼續輸入;當然這兩行也可以合併成一行。

    2. 雖然要把字串『刪除』﹐我們仍然可以利用 s 命令來完成: 只須把要刪除的字串用不含任何字元的『空字串』來『取代』即可﹐格式為

      s/正規表示式//g

      在『用來取代的字串』這個部分﹐我們以不輸入任何字元的方式來代表空字串。 例如要把每一行中﹐所有的 abc 刪除掉﹐利用 s/abc//g 這個命令即可:

      在知道如何『刪除』字串後﹐ 請讀者想想只讓 一行前端一或多個空格 能符合的 RE 該怎麼寫?分成兩個部分:

      • 一行前端:利用定位字元 ^﹐代表字串不僅必須符合 ^ 之後的 RE﹐ 還得在一行的前端才行。
      • 一或多個空格: 請參考下表

        條件對應的 RE
        零或多個空格 *
        一個(含)以上的空格 *
        兩個(含)以上的空格 *

      把這兩個部分結合得到

      ^*

      這個 RE。 請注意﹐如果不加上 ^ 這個定位字元﹐而又加了 g 這個選項時﹐ 一行中所有的空格都會被刪掉﹐這是讀者應當留意的。實驗看看:

    3. 從前一題的表格中得知﹐只讓 兩個(含)以上的空格 符合的 RE 是 * 因此

      s/*//g

      就是我們所要的編輯命令:

      (註:用 s/*//g 也可以達到相同的效果﹐ 但意義不同﹐請讀者想想看)

    取用符合 RE 字串的某些部分-\( 與 \) 的運用

    在 sed 的取代命令 s 內『用來取代的字串』的這個部分﹐ 我們可以利用特殊字元 & 來取得符合 RE 的字串; 如果我們要的只是該字串的某些部分時﹐ 可以先在 RE 中把這些部分對應的位置『標記』起來﹐ 然後在『用來取代的字串』內加以引用。 用一個例子來說明:

    [問題 9]

      我們常用 mm-dd-yy (月-日-年)這種格式來表示日期; 如果要把文字檔中﹐原為 mm-dd-yy 的日期格式轉換成 yy-mm-dd (年-月-日)時 該怎麼辦呢?
    [說明]
      為求簡單起見﹐我們假設要處理的檔案中不會出現 23-55-99 這種不合理的日期;因此﹐ 我們可以把符合 mm-dd-yy 這種格式的所有字串用

      [01][0-9]-[0-3][0-9]-[0-9][0-9]

      這個 RE 來描述﹐它的意義表列如下:

      欲搜尋字串的
      笫幾個字元
      1 2、5、
      7、8
      4 3、6
      允許值 0 或 1 0 到 9 0 到 3 -

      接著利用一對對的 \( 及 \) 分別把代表年、月、日的部分標記起來﹐ 把上述的 RE 改寫成

      \([01][0-9]\)-\([0-3][0-9]\)-\([0-9][0-9]\)

      觀察這個新出爐的 RE 內共有三組由 \( 及 \) 所構成的部分﹐ 組別的編號是從左至右﹐從一開始算起:

      組別 內容 說明 如何取用符合 RE
      的字串的該部分
      1 [01][0-9]屬於『月』\1
      2 [0-3][0-9]屬於『日』\2
      3[0-9][0-9]屬於『年』\3

      用來取代的字串是 年-月-日﹐ 其中代表年、月、日的字串分別是第 3、1、及第 2 組用 \( 及 \) 標記起來的部分﹐ 要取用這部分的字串時﹐分別用 \3、\1 及 \2 來代表; 因此﹐我們把用來取代的字串寫成

      \3-\1-\2

      把這兩個部分組合成 s 命令:

      s/\([01][0-9]\)-\([0-3][0-9]\)-\([0-9][0-9]\)/\3-\1-\2/g

      試試看對不對呢?

    定址-限制 sed 只對某些資料行執行編輯命令

    到目前為止﹐在所介紹的範例中﹐sed 會對檔案中每一行的內容執行編輯命令。 事實上﹐我們可以要求 sed 只對某些資料行(如第二行﹐第五行)執行命令﹐ 我們用例子逐一說明。

    首先製作測試檔 data3﹐內容如下:

    編輯命令 s/xx/yy/g 可以把每一行中的 xx 都換成 yy:

    如果只要把第二行內所有的 xx 換成 yy 時﹐執行

    即可﹐其中編輯命令前的 2 代表第二行﹐如果是 3 則代表第三行﹐依此類推; 符號 $ 在此有特殊的意義--用來代表檔案的最後一行:

    我們也可以要求 sed 只對某個範圍內(從第幾行起到第幾行為止)的資料行﹐ 執行把 xx 換成 yy 的命令:

    編輯命令前的 3,$ 代表只對第三行起到最後一行為止的資料行執行該編輯命令。

    在上述的例子中﹐指定第二、三及最後一行時所用的 2、3 與 $﹐ sed 稱它們為位址(address)﹐而要編輯命令只執行在某些資料行的動作 稱為定址(addressing)。sed 在執行編輯命令之前﹐ 會先判斷目前處理的資料行是否屬於位址所指定的範圍。 在前幾個例子中﹐我們看到編輯命令前的位址格式有三種:

    • 不指定位址(0-位址):sed 會對每一行執行該編輯命令。
    • 有一個位址(1-位址):sed 只會對行號等於該位址的資料行執行編輯命令。
    • 有兩個位址(2-位址):sed 只會對行號在該二位址間的資料行執行編輯命令。
    在往後有關 sed 的說明中﹐所提到的『命令』是由位址(可以是 0,1,2-位址)以及編輯命令兩個部分結合而成的。

    位址與正規表示式的結合

    在上面幾個例子中﹐我們指定位址的方式不外乎是數字或是符號 $。 另有一種更有彈性的方式是

    /正規表示式/

    若應用在 1-位址 的位址格式下﹐ 只有當一行中含有符合該正規表示式的字串時﹐ sed 才會對該行執行編輯命令。例如

    /yes/s/xx/yy/g

    這個命令含了兩個部分:

    • /yes/ 為位址的部分﹐代表一個資料行中必須含有 yes 這個字串 才是我們所要的。
    • s/xx/yy/g 是編輯命令﹐用來把一行中所有的(所以最後加了個 g 選項) xx 換成 yy。
    因此 sed 處理這個命令的方式是 只對含有 yes 的資料行執行 s/xx/yy/g 的動作﹐觀察以下的執行結果:

    但這種寫法解讀起來有些不方便﹐ 我們可以利用一組大括弧把命令中的位址與編輯命令分開來:

    讀者應注意:

    • 左大括弧 { 後要直接按 ENTER﹐不可以有任何其他的字元。
    • 右大括弧 } 必須是一行的第一個字元。

    另外﹐我們可以把編輯命令存在檔案中﹐再指定 sed 到該檔案中擷取命令: 首先把

    存入 script 這個檔案中﹐然後執行

    sed -f script data3

    即可得到相同的結果。其中選項 -f 後為命令檔(script file)的名稱。

    把正規表示式應用在 2-位址 的位址格式上

    /正規表示式/ 也可以應用在兩個位址的格式上﹐一共有三種組合:

    • 組合 1- 行號 X, /RE/
      指定的範圍從第 X 行起﹐到第 X 行後第一個含有符合 RE 字串的資料行為止。

      在上述這個例子中﹐第 2 行之後含有 yes 字串的是第 5 行﹐因此 sed 只會把從第 2 行起﹐到第 5 行為止的 xx 換成 yy。

      為了能理解 sed 處理 2-位址 的方式﹐ 讀者不妨想像有一個開關﹐而 on 與 off 是由兩個位址來把關的﹐ 當在 on 的狀態時﹐sed 就執行位址後的編輯命令;而在 off 狀態時則否:

      • sed 在從資料檔讀入第 1 行之前﹐開關是處在 off 的狀態。
      • 如果開關是處在 off 狀態﹐ 當碰到一個符合第一個位址的資料行時﹐sed 會把開關切為 on。
      • 從開關被切為 on 的下一行起﹐當 sed 碰到第一個符合第二個位址 的資料行時﹐會在對該行執行完編輯命令後把開關切為 off。 之後若有資料行符合第一個位址時(在組合 3 中有範例說明)﹐ 仍會再把開關切為 on。
      • 對於同一個開關﹐一資料行只能把它由 on 切到 off﹐或由 off 切到 on﹐ 而不會同時執行。
      因此﹐在上例中﹐開關在第 2 行時被切為 on﹐而在處理完第 5 行後被切為 off。

      但如果第 2 行之後﹐都沒有任何一行含有 yes 字串時會怎樣呢? 由於開關不會被切到 off﹐因此 sed 會對第 2 行起到最後一行止﹐每一行執行 s/xx/yy/g 的命令:

    • 組合 2-/RE/, 行號 X
      所指定的範圍為從第一個含有符合 RE 字串的資料行起到第 X 行為止。

      在這個例子中﹐第一個含有 no 的資料行是第 3 行﹐因此開關在第 3 行被切為 on﹐ 一直要到最後一行($)處理完畢後才會被切為 off。

    • 組合 3-/RE1/, /RE2/
      可用來處理單純的(非巢狀,non-nested)區塊﹐我們用例子來說明:

      [說明]

      • 在處理第 1 行之前﹐開關處於 off 的狀態。
      • 第 2 行含有 begin﹐因而 /begin/ 成立﹐開關切為 on﹐從現在起﹐ sed 會對每一行執行 s/xxxx/yyyy/g 的命令; 而到第 5 行時﹐由於含有 end﹐因而 /end/ 成立﹐處理完第 5 行後開關被切到 off;請注意﹐如果第 5 行含有 xxxx﹐一樣會被換成 yyyy。
      • 到第 7 行時開關又被切為 on﹐一直到第 9 行才再被切為 off。

    刪除資料行-編輯命令 d

    編輯命令 d 可用來刪除資料行﹐它必須與位址合用才有意義; 例如執行

    時﹐sed 沒有輸出任何資料而直接回到 shell 的提示符號﹐ 因為 sed 每讀進一行﹐我們就要求它把該行刪掉﹐當然就沒有資料可以輸出了。

    而命令 /yes/d 可以把含有 yes 字串的資料行刪除:

    所有含有 yes 字串的資料行都被刪掉了。 如果要把空行刪掉呢?在編輯命令 d 前加上只有空行能符合的條件﹐ 也就是 /^$/ 即可:

    不對某些資料行執行編輯命令

    剛才我們學會了利用位址來要求 sed 只對某些資料行執行編輯命令; 如果在位址與編輯命令之間加入 !﹐ sed 會對不屬於該位址的資料行執行編輯命令; 例如

    /yes/!d

    會把不含有 yes 字串的資料行刪除掉:

    字的邊界

    在上一期中﹐我們曾提到找出 cat 這個英文單字的『暫時性』解法﹐ 是把 RE 設計成

    cat

    也就是 cat 前後各有一個空格才行。這樣雖然可以找出

    It indicates that cat is too fat.

    that 之後的 cat 而不是夾在 indicates 中的 cat﹐但這個解法並無法找出

    I have a cat.

    中的單字 cat。在設計更好的 RE 之前先想想看﹐ 當要找 cat 這個『單字』時﹐為什麼我們能一眼分辨出在這兩個句子中﹐真正的單字 cat 呢? 原因是真正的單字 cat 前後並不會緊接著英文字母。 根據這項推論﹐我們可以設計出新的 RE:

    [^A-Za-z]cat[^A-Za-z]

    試試看效果如何?

    我們發現有些單字 cat 並沒有被找出來; 事實上﹐單字 cat 在一行中的位置可歸類為四種:

    組別 單字 cat 的分佈情形 有效的 RE 符合左方 RE
    的字串的長度
    1 只由 cat 這三個字元
    所構成的資料行
    ^cat$ 3
    2 位在一行的開頭 ^cat[^A-Za-z] 4
    3 位在一行的結尾 [^A-Za-z]cat$ 4
    4 一行的中間 [^A-Za-z]cat[^A-Za-z] 5

    剛才所想的新 RE 只能找出屬於第 4 組的 cat;看來要找出單字 cat 還真是件不容易的事。還好在 GNU 版本的 grep 以及 sed 內提供了 \b 這個特殊序列(meta-sequence)﹐ 用來代表字的邊界(word boundary)﹐它是一種不佔空間的定位字元; 有了它﹐要找出以上四種位置的 cat 就可以用

    \bcat\b

    這個 RE 來解決:

    (註:grep 與 sed 的 \b 所設定的條件比前表所列的更嚴格﹐我們採用的是 [^A-Za-z]﹐而它們採用的是 [^A-Za-z0-9])

    限制符合的次數

    在上一期的內容中曾提到 * 這個特殊字元﹐在它之前必須有一個佔有一個位置的 RE;如果 X 是這樣的 RE﹐一個字串只要符合由連續 n 個 X 所結合成的 RE﹐如 X, XX, XXX, ... 等的任何一個﹐或它是空字串﹐ 就算符合 X* 這個 RE。

    而在基本型 RE 中﹐我們可以利用 \{ 與 \} 可用來限制符合 X 的次數; 與 * 相同﹐它們也必須與佔有一個位置的 RE 共用。 以下的表格將列出 \{ 與 \} 的用法:

    格式字串須符合怎樣的 RE
    才合乎要求
    X\{n\} 由連續 n 個 X 所構成
    X\{n,m\}連續 n 到 m 個 X
    X\{n,\} 至少連續 n 個 X
    • X 為佔有一個位置的 RE
    • n 與 m 可以是 0 到 256 的整數

    如果要找的字串是 1 之後接著連續 2 到 4 個 0﹐再接著 1 時﹐ 我們可以把 RE 寫成 10\{2,4\}1﹐實驗一下:

    [說明]

      為了能容易辨識出符合 10\{2,4\}1 的字串﹐我們在找到的字串前後各加上 < 與 >﹐ 因此在 s 命令中﹐把『用來取代的字串』這個部分寫成 <&>﹐其中 & 在此處 代表符合 10\{2,4\}1 這個 RE 的字串。

    [問題 10]

    1. 把每行第 10 個字元起的資料清除掉。
    2. 在每行第 9 個字元之後加入 HERE 這個字串。
    [說明]
      我們可以利用編輯命令 s 的取代功能來進行刪除與加入字串的工作。
      1. 很明顯的﹐我們必須把一資料行分成兩個部分來處理:
        • 前半由前 9 個字元所構成﹐ 這部分利用『字串的再利用』保留下來。
        • 後半由第 10 個字元起的資料所構成﹐ 藉由用空字串取代的方式把這部分清除掉。
        而起碼有 9 個字元的資料行可以用

        ^..........*$ 或 ^.\{9\}.*$ 來描述

        (註:由於 * 有 greedy 的特性﹐把 RE 寫成 ^.\{9\}.* 也可以)。

        而要保留前 9 個字元﹐我們把它們利用一對 \( 與 \) 圈起來﹐ RE 變成了:

        ^\(.\{9\}).*$

        而編輯命令 s/^\(.\{9\}).*$/\1/ 則保留前 9 個字元﹐而把之後的字元刪除。

      2. 利用編輯命令 s/^.\{9\}/&HERE/ 即可。

    對一資料行執行多個編輯命令

    到目前為止﹐我們在所舉的範例中﹐只對每一行執行一個編輯命令。 事實上﹐sed 可以對每一行的內容執行多個命令:

    sed -e 命令1 -e 命令2 .... -e 命令n 資料檔名

    讀者也可以採用一行列出一個命令的方式﹐如

    命令1
    命令2
    :
    命令n

    把這些命令依序集合在一個命令檔(script file)內﹐ 然後使用

    sed -f 命令檔檔名 資料檔名

    的方式來執行。

    而 sed 處理資料的方式﹐是依序對資料檔的每一行執行以下步驟:

    1. 把該行的資料載入 pattern space。
    2. 對 pattern space 依序執行命令1、命令2、...、命令n。
      • 在上期的說明中﹐在這個步驟內只有一個編輯命令。
      • 要執行命令x 之前﹐sed 會檢查資料行是否符合命令x 的位址部分﹐若是則執行編輯命令;若否則跳過不執行。
    3. 把 pattern space 的內容輸出。

    [問題 11]

      在問題 8 的(2)與(3)中分別介紹了如何把行首的空格刪掉﹐ 以及把多個空格用一個空格代替﹐現在請把這兩項工作結合在一起。
    [說明]
      如果要先把行首的空格刪掉﹐再把多個空格用一個空格代替時﹐ 命令檔的內容應為

      下圖為 sed 對

      這行資料執行命令檔的過程:

    [補充說明]
    1. 前面曾提到 s/xx/yy/g 是『把資料行內所有的 xx 換成 yy』﹐ 如果只對資料行執行一個命令時﹐這種說法是正確的; 而當對資料行執行多個命令時﹐上述說法應調整成

      『把當時 pattern space 內﹐所有的 xx 換成 yy』

      在問題 11 的圖示中﹐很清楚的可以看到

      • 第 1 個命令 s/^*// 編輯的是原始的資料。
      • 第 2 個命令 s/*//g 編輯的是經過第 1 個命令 s/^*// 編輯過的內容。
      因此﹐命令x(x>1)所編輯的內容是依序經過 命令1、...、命令x-1 編輯過的內容﹐在撰寫多個命令的命令檔時應留意。
    2. /RE/ 是否成立也是由進行決策時當時 pattern space 的內容來決定。 因此

      更嚴謹的說法是

      『若此時 pattern space 內含有字串 yes﹐則把所有的 xx 換成 yy』

    在下一期的內容中﹐我們將介紹延伸型的 RE、egrep、以及如何在 perl 中使用 RE。

    參考文獻

    • Sed & Awk 第二版
      由 Dale Dougherty & Arnold Robbins 合著﹐ O'Reilly and Associates 出版﹐這本書前半部描述 RE 的觀念、 sed 基本以及進階命令的介紹﹐堪稱為 sed 的聖經﹐適合初學者入門建立觀念 與進階者進修。
    • SED 常問問題集
      http://www.cornerstonemag.com/sed/sedfaq.html 這份文件內容有
      • sed 的基本介紹(2.1 節)
      • 不同平台上的 sed(2.2 節)
      • 參考書籍(2.3.1 節)、教學文件的網址(2.3.3 節)、 線上相關資源(2.3.4 節)
      • sed 進階使用(第 3 章)
      • 範例 (第 4 章)
      • 疑難解答(第 5 章)
      • 其他
      內容相當豐富﹐值得讀者參考。