-精通正則表達式-第三版-第2章筆記


例子: 檢索某台Web服務器上的頁面中的重復單詞(例如”this this”), 進行大規模文本編輯時, 這是一項常見的任務. 程序必須滿足下面的要求:
1. 能檢查多個文件, 挑出包含重復單詞的行, 高亮標記每個重復單詞(使用標准ANSI的轉義字符序列(escape sequence)), 同時必須顯示這行文字來自哪個文件.
2. 能跨行查找, 即使兩個單詞一個在某行末尾而另一個在下一行的開頭, 也算重復單詞
3. 能進行不區分大小寫的查找, 例如”The the”, 重復單詞之間可以出現任意數量的空白字符(空格符, 制表符, 換行符之類)
4. 能查找用 HTML tag 分隔的重復單詞. HTML tag 用於標記互聯網頁上的文本, 例如, 粗體單詞是這樣表示的: “···it is very very important···”

$/ = ".\n";
    while(<>){
        next if !s/\b([a-z]+)((?:\s|<[^>]+>)+)(\1\b)/\e[7m$1\e[m$2\e[7m$3\e[m/ig;
        s/^(?:[^\e]*\n)+//mg;  # 去除為標記的行
        s/^/$ARGV: /mg;  # 在行首添加文件名
        print;
    }

該程序的主要功能依靠3個正則表達式:
「\b([a-z]+)((?:\s|<[^>]+>)+)(\1\b)」
「^(?:[^\e]*\n)+」
「^」

本章包括了一些常見的問題–驗證用戶的輸入數據, 處理 E-mail header(電子郵件頭), 把純文本數據轉換為超文本格式(HTML).在構造正則表達式時, 我會做些盡可能詳細的講解, 提供一些啟示. 在這個過程中, 我們會見到一些 egrep 沒有提供的結構和特性, 也會專門花很多篇幅來探討其他重要的概念.

Perl 簡單入門

現在來看一個簡單的例子:

$celsius = 30;
$fahrenheit = ($celsius * 9 / 5) + 32;  # 計算華氏溫度
print "$celsius C is $fahrenheit F.\n";  # 返回攝氏和華氏溫度

Perl 也提供了跟其他流行的語言類似的控制結構:

$celsius = 20;
while($celsius <= 45){
    $fahrenheit = ($celsius * 9 / 5) + 32;
    print "$celsius C is $fahrenheit F.\n"
    $celsius = $celsius + 5;
}

下面我們來看在 Perl 中如何使用正則表達式.

使用正則表達式匹配文本

Perl 可以以多種方式使用正則表達式, 最簡單的就是檢查變量中的文本能否由某個正則表達式匹配. 下面的代碼檢查 $reply 中所含的字符串, 報告這個字符串是否全部由數字構成:

if($reply =~ m/^[0-9]+$/){
    print "ongly digits\n";
}
else{
    print "not only digits\n";
}

正則表達式是「^[0-9]+$」兩邊的 m/···/ 告訴 Perl 該對這個正則表達式進行什么操作. m 代表嘗試進行”正則表達式匹配(regular expression match)”, 斜線用來標記界限. 之前的 =~ 用來連接 m/···/ 和欲搜索的字符串, 即本例中的 $reply. 此程序在其他語言中的思路有所不同.
請注意, 如果$reply 中包含任意的數字字符, $reply =~ m/[0-9]+/(相比之前的表達式, 去掉了開頭的脫字符和結尾的美元符)的返回值就是 true. 兩端的「^···$」保證整個 $reply 只包含數字.
現在把上面兩個例子結合起來. 首先提示用戶輸入一個值, 接收這個輸入, 用一個正則表達式來驗證, 確保輸入的是一個數值. 如果是, 我們就計算相應的華氏溫度, 否則, 我們輸入一條警報信息:

print "Enter a temperature in Celsius:\n";
$celsius = <STDIN>  # 從用戶處接受一個輸入
chomp($celsius);  # 去掉 $celsius 后面的換行符
if($celsius =~ m/^[0-9]+$/){
    $fahrenheit = ($celsius * 9 / 5) + 32;  # 計算華氏溫度
    print "celsius C is $fahrenheit F\n";
}
else{
    print "Expecting a number, so I don't understand \"$celsius\".\n";
}

向更實用的程序前進

拓展這個例子, 容許輸入負數和可能出現的小數部分.

if($celsius =~ m/^[-+]?[0-9]+(\.[0-9]*)?$/){}

成功匹配的副作用

我們再進一步, 讓這個表達式能夠匹配攝氏和華氏溫度. 我們讓用戶在溫度的末尾加上 C/F 來表示.
溫度轉換程序:

print "Enter a temperature (e.g., 32F, 100C):\n";
$input = <STDIN>;  # 接收用戶輸入的一行文本
chomp($input);  # 去掉文本末尾的換行符
if($input =~ m/^([-+]?[0-9]+)([CF])$/)
{
    # 如果程序運行到此, 則已經匹配. $1保存數字, $2 保存"C"或者"F"
    $InputNum = $1;  # 把數據保存到已命名變量中...
    $type = $2;  # ...保證程序清晰易懂
    if($type eq "C"){  # 'eq 測試兩個字符是否相等
        # 輸入為攝氏溫度, 則計算華氏溫度
        $celsius = $InputNum;
        $fahrenheit = ($celsius * 9 / 5) + 32;
    }else{
        $fahrenheit = $InputNum;
        $celsius = ($fahrenheit - 32) * 5 / 9;
    }  # 現在得到了兩個溫度值, 顯示結果:
    print "%.2f C is %.2f F\n", $celsius, $fahrenheit;
}else{
    # 如果最開始的正則表達式無法匹配, 報警
    print "Expecting a number followed by \"C\" or \"F\",\n";
    print "so I don't understand \"$input\".\n";
}

錯綜復雜的正則表達式

在 Perl 之類的高級語言中, 正則表達式的使用與其他程序的邏輯是混合在一起的. 為了說明這一點, 我們對這個程序做三點改進: 像之前一樣能夠接收浮點數. 容許 f 或者 c 是小寫, 容許數字和字母之間存在空格.
我們希望開發一個實際應用的程序, 所以必須容許一些除空格之外的空白字符(whitespace). 例如常見的制表符(tabs). 所以我們需要一個字符組來匹配兩者「[ \t]*」
請把上面這個子表達式與「( *|\t*)」進行對比, 你能發現者其中的巨大差異嗎?
「( *|\t*)」容許 或\t的匹配, 它能夠匹配若干空格符以及若干制表符, 不過並不容許制表符和空格符的混合體.「[ \t]*」則可以.
在邏輯上, 「[ \t]*」與「( |\t)*」是等價的, 原因會在第4章解釋, 字符組的效率通常還是會高一點.
許多流派的正則表達式提供了一種方便的辦法\s.表示所有空白字符的字符組, 其中包括空格符, 制表符, 換行符和回車符, 在復雜的表達式中, \s*更易於理解.
溫度轉換程序–最終版本(Python)

import re

print("Enter a temperature (e.g., 32F, 100C):")
input_ = input()
result = re.match(r"^([-+]?[0-9]+(\.[0-9]*)?)\s*([CF])$", input_, re.I)
if result:
    InputNum_ = result.group(1)
    type_ = result.group(3)
    if type_ == "C" or type_ == "c":
        celsius = float(InputNum_)
        fahrenheit = celsius * 9 / 5 + 32
    else:
        fahrenheit = float(InputNum_)
        celsius = (fahrenheit - 32) * 5 / 9
    print("%.2f C is %.2f F" % (celsius, fahrenheit))
else:
    print("Expecting a number followed by \"C\" or \"F\",")
    print("so I don't understand \"%s\"." % input_)

使用正則表達式修改文本

例子: 公函生成程序
下面這個有趣的例子展示了文本替換的用途. 設想有一個公函系統, 它包含很多公函模板, 其中有一些標記, 對每一封具體的公函來說, 標記部分的值都有所不同:

Dear =FIRST=,
You have been chosen to win a brand new =TRINKET=! Free!
Could you use another =TRINKET= in the =FAMILY= household?
Yes =SUCKER=, I bet you could! Just respond by...
對特定的接收人, 變量的值分別為:
$given = "Tom";
$family = "Cruise";
$wunderprize = "100% genuine faux diamond";
准備好之后, 就可以用下面的語句"填寫模板":
$letter =~ s/=FIRST=/$given/g;
$letter =~ s/=FAMILY=/$family/g;
$letter =~ s/=SUCKER=/$given $family/g;
$letter =~ s/=TRINKET=/fabulous $wunderprize/g;

例子: 修整股票價格
使用 Perl 編寫的股票價格軟件時遇到的問題. 我得到的價格看起來是這樣”9.0500000037272”. 這里的價格顯然應該是9.05, 但是因為計算機內部表示浮點的原理, Perl 有時會以沒什么用的格式輸出這樣的結果. 我們可以使用 printf 來保證只輸出兩位小數, 但是此處並不適用. 如果某個價格以1/8結尾, 則應該輸出3位小數(“.125”), 而不是兩位.

$price =~ s/(\.\d\d[1-9]?)\d*/$1/

自動的編輯操作

% perl -p -i -e 's/sysread/read/g' file

這樣簡單的編輯方式是 Perl 獨有的, 但這個例子告訴我們, 即使執行的是簡單的任務, 作為腳本語言一部分的正則表達式的功能仍然非常強大.

處理郵件的小工具

一個文件中保存着 E-mail 信息, 我們需要生成一個用於回復的文件. 在准備過程中, 我們需要引用原始的信息, 這樣就能很容易地把回復插入各個部分. 在生成回復郵件的 header 時, 我們還需要刪除原始信息郵件的 header 中不需要的行.

E-mail Message 范本


From elvis Thu Feb 29 11:15 2007
Received: from elvis@localhost by tabloid.org (8.11.3) id KA8CMY
Received: from tabloid.org by gateway.net (8.12.5/2) id N8XBK
To: jfriendl@regex.info (Jeffrey Friedl)
From: elvis@tabloid.org (The King)
Date: Thu, Feb 29 2007 11:15
Message-Id: <2007022939939.KA8CMY@tabloid.org>
Subject: Be seein’ ya around
Reply-To: elvis@hh.tabloid.org
X-Mailer: Madam Zelda’s Psychic Orb [version 3.7 PL92]

Sorry I haven’t been around lately. A few years back I checked into that ole heartbreak hotel in the sky, ifyaknowwhatImean.
The Duke says “hi”.
Elvis


我們希望程序的輸出結果 king.out 包括下面的內容:
To: elvis@hh.tabloid.org (The King) # Reply-To: elvis…
From: jfriendl@regex.info (Jeffrey Friedl) # To: jfreindl…
Subject: Re: Be seein’ ya around # Subject: Be…
On Thu, Feb 29 2007 11:15 The King Wrote: # Data: Thu…
Sorry I haven’t been around lately. A few years back I checked into that ole heartbreak hotel in the sky, ifyaknowwhatImean.
The Duke says “hi”.
Elvis


Python 程序:

import re

f = open("%s/message" % path, "r")
while True:
    msg = f.readline()
    if re.match(r"^\s*$", msg):
        break
    elif re.match(r"^Subject: (.*)", msg, re.I):
        result = re.match(r"^Subject: (.*)", msg, re.I)
        subject = result.group(1)
    elif re.match(r"^Date: (.*)", msg, re.I):
        result = re.match(r"^Date: (.*)", msg, re.I)
        data = result.group(1)
    elif re.match(r"^Reply-To: (\S+)", msg, re.I):
        result = re.match(r"^Reply-To: (\S+)", msg, re.I)
        reply_address = result.group(1)
    elif re.match(r"^From: (\S+) \(([^()]*)\)", msg, re.I):
        result = re.match(r"^From: (\S+) \(([^()]*)\)", msg, re.I)
        reply_address = result.group(1)
        from_name = result.group(2)
print("To: %s (%s)" % (reply_address, from_name))
print("From: jfreidl\@regex.info (Jeffrey Friedl)")
print("Subject: Re: %s" % subject)
print()
print("On %s %s wrote:" % (data, from_name))
while True:
    msg = f.readline()
    if msg:
        print(msg, end="")
    else:
        break
f.close()

真實世界的問題, 真實世界的解法
作為第一步, 在檢查原始郵件之后(生成回復模板之前), 我們可以這樣:

if(    not defined($reply_address)
    or not defined($from_name)
    or not defined($subject)
    or not defined($date))
{
    die "couldn't glean the required information!";
}

另外一點需要考慮的是, 程序假設 From: 這一行出現在 Reply-To: 之前. 如果 From: 出現在之后, 就會覆蓋從 Reply-To 取得的 $reply_address.

用環視功能為數值添加逗號

使用一組相對較新的正則表達式特性–它們統稱為”環視(lookaround)”.
環視結構不匹配任何字符, 只匹配文本中的特定位置, 這一點與單詞分界符\b, 錨點^和$相似.但是, 環視比它們更加通用.
一種類型的環視叫”順序環視(lookahead)”, 作為表達式的一部分, 順序環視順序查看文本, 嘗試匹配子表達式, 如果能夠匹配, 就返回匹配成功信息. 肯定型順序環視(positive lookahead)用特殊的序列「(?=···)」來表示, 例如「(?=\d)」它表示如果當前位置右邊的字符是數字則匹配成功.
另一種環視稱為逆序環視, 它用特殊的序列「(?<=···)」表示, 例如「(?<=\d)」如果當前位置的左邊有一位數字, 則匹配成功.

$pop =~ s/(?<=\d)(?=(?:(\d\d\d))+$)/,/g;
print "The US population is $pop\n";

單詞分界符和否定環視
現在假設, 我們希望把這個插入逗號的正則表達式應用到很長的字符串中, 例如:

$text = "The population of 298444215 is growing";
……
$text =~ s/(?<=\d)(?=(\d\d\d)+$)/,/g;                            
print "$text\n";

很顯然程序沒有結果, 因為$要求字符串以3的倍數位數字結尾. 我們不能只去掉這里的$, 因為這樣會從左邊第一位數字之后, 右邊第三位數字之前的每一個位置插入逗號–結果是”2,9,8,4,4,4,215”!
可能初看起來這問題有些棘手, 但我們可以用單詞分界符\b來替換$. 就像\w一樣, Perl 和其他語言都把數字, 字母和下划線當做單詞的一部分. 結果, 單詞分界符的意思就是, 在此位置的一側是單詞(例如數字), 另一側不是(例如行的末尾, 或者數字后面的空格).
四種類型的環視

  • 肯定逆序環視  子表達式能夠匹配左側文本  (?<=……)
  • 否定逆序環視  子表達式不能匹配左側文本  (?<!……)
  • 肯定順序環視  子表達式能夠匹配右側文本  (?=……)
  • 否定順序環視  子表達式不能匹配右側文本  (?!……)

所以, 如果單詞分界符的意思是: 一側是\w而另一側不是\w, 我們就能用「(?<!\w)(?=\w)」來表示單詞起始分界符, 用「(?<=\w)(?!\w)」表示單詞結束分界符. 把兩者結合起來, 「(?<!\w)(?=\w)|(?<=\w)(?!\w)」就等價於\b. 在實踐中, 如果語言本身支持\b(\b更直接, 效率也更高), 這樣做有點多此一舉, 但是可能的確有地方需要用到這兩個單獨的多選分支.
對於我們的逗號插入問題來說, 我們真正需要的是(?!\d)來標記3位數字的起始計數位置. 我們用它來取代\b或者$:

$text =~ s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g

不通過逆序環視添加逗號

$text =~ s/(\d)((\d\d\d)+\b)/$1,$2/g;

結果並非我們的期望. 得到的是類似”181,421906”的字符串. 這是因為「(\d\d\d)+」匹配的數字屬於最終匹配文本, 所以不能作為”未匹配的”部分, 供/g的下一次匹配迭代使用.

Text-to-HTML轉換

現在我們寫一個把 Text 轉換為 HTML 的小工具, 如果要處理所有的情況, 程序將非常難寫.

作為正則表達式應用對象的變量都只包含一行文本. 對這個例子來說, 把我們需要轉換的所有文本放在同一個字符串中比較方便.

undef $/;  # 進入"file-slurp"(文件讀取)模式
$text = <>;  # 讀取命令行中指定的第一個文件

1.處理特殊字符
首先我們需要確保原始文件中的’&’ ‘<’和’>’字符”不會出錯”, 把它們轉換為對應的 HTML 編碼, 分別是’&amp’ ‘&lt’和’&gt’. 在 HTML 中這些字符有特殊的含義, 編碼不正確可能會導致顯示錯誤:

$text =~ s/&/&amp;/g;  # 保證基本的 HTML…
$text =~ s/</&lt;/g;  # …字符& < >…
$text =~ s/>/&gt;/g;  # …轉換后不不錯

請注意我們使用了/g來對所有的目標字符進行替換. 首先轉換&是很重要的, 因為這三者的 replacement 中都有&字符.

2.分隔段落
識別段落的簡單辦法就是把空行作為段落之間的分隔.

$text =~ s/^$/<p>/g;

對於之前看到過的 E-mail 的例子, 我們知道每一個字符串只包含一個邏輯行. ^和$通常匹配的不是邏輯行的開頭和結尾, 而是整個的字符串的開頭和結束位置. 所以, 既然目標字符串中有多個邏輯行, 就需要采取不同的辦法.
大多數支持正則表達式的語言提供了一個簡單的辦法, 即”增強的行錨點”匹配模式, 在這種模式下, ^和$會從字符串模式切換到本例中需要的邏輯行模式.在 Perl 中, 使用/m修飾符來選擇此模式:

$text =~ s/^$/<p>/mg;

不過, 如果在”空行”中包含空格符或者其他空白字符, 這么做就行不通. 所以在最終的程序中,我們會使用「^\s*$」.

3.將 E- mail 地址轉換為超鏈接形式
識別出 E-mail 地址, 然后把它轉換為”mailto”鏈接.例如, jfriedl@oreilly.com 會被轉換為
<a href="mailto:jfriedl@oreilly.com">jfriedl@oreilly.com</a>
E-mail 地址的基本形式是 username@hostname. 在思考該用怎樣的表達式來匹配各個部分之前, 我們先看看這個正則表達式的具體應用環境:

$text =~ s/\b(username regex\@hostname regex)\b/<a href="mailto:$1">$1<\/a>/g;

需要注意其中兩個反斜線, 第一個在正則表達式(\@)中, 另一個在 replacement 字符串的末尾.
我們先看第二個, Perl中查找替換的基本形式是 s/regex/replacement.modifier, 用斜線來分隔. 所以, 如果我們需要在某個部分中使用斜線, 就必須使用轉義.

4.匹配用戶名和主機名
匹配 E-mail 地址的最簡單的辦法是「\w+\@\w+(.\w+)+」, 用\w來匹配用戶名, 以及主機名的各個部分. 不過, 實際應用起來, 我們需要考慮的更周到一些. 於是我們得到用來匹配主機名的「[-a-z0-9]+(.[a[z0-9]+)*.(com|edu|info)」.
無論使用什么正則表達式, 記住它們應用的情境都是很重要的.「[-a-z0-9]+(.[a[z0-9]+)*.(com|edu|info)」這個正則表達式本身, 可以匹配’run C:\startup.command at startup’, 但是把它置入程序運行的環境中, 我們就能確認, 它會匹配我們期望的文本, 而忽略不期望的內容.

5.把 HTTP URL 轉換為鏈接形式
也就是說把”http://www.yahoo.com“轉變為
<a href=http://www.yahoo.com/>http://www.yahoo.com/</a>

6.為什么有時候@需要轉義
Perl 用@表示數組名, 而 Perl 中的字符串或正則表達式中也容許出現數組變量.
有些語言(例如Python)支持邊梁插值, 但是方法各有不同.

回到單詞重復問題

在本章開頭我給出了一堆難懂的代碼, 將其作為解法之一.
現在列在下面的程序使用了 s{regex}{replacement}modifier 的替換形式, 同時使用了/x修飾符來提高清晰程度(空間更充裕的時候, 我們使用更易懂的’next unless’替換’next if!’).

$/ = ".\n";  # ①設定特殊的"塊模式"("chunk-mode"); 一塊文本的終結為點號和換行符的結合體
    while(<>)  # ②
    {
        next unless s{  #③下面是正則表達式
            ### 匹配一個單詞:
            \b  # 單詞的開始位置
            ([a-z]+)  # 把讀取的單詞存儲至$1 (和\1)
            ### 下面是任意多的空白字符和/或tag
            (  # 把空白保存到%2
                (?:  # 使用非捕捉型括號
                    \s  #空白字符
                    |
                    <[^>]+>  # <TAG>形式的tag
                )+  # 至少需要出現一次, 多次不受限制
            )
            ### 現在再次匹配第一個單詞:
            (\1\b)  # \b保證用來避免嵌套單詞的情況, 保存到$3
        }  # 正則表達式結束
        # 上面是正則表達式. 下面是 replacement 字符串, 然后是修飾符 /i /g /x
        {\e[7m$1\e[m$2\e[7m$3\e[m}/ig;  # ④
        s/^(?:[^\e]*\n)+//mg;  # ⑤去除為標記的行
        s/^/$ARGV: /mg;  # ⑥在行首添加文件名
        print;
    }

1.因為單詞重復問題必須應對單詞重復位於不同行的情況. 在程序中使用特殊變量$/能讓<>不再返回單行文字, 而返回或多或少的一段文字, 返回的數據仍然是一個字符串, 只是這個字符串可能包含多個邏輯行.

2.你是否注意到,<>沒有值賦給任何變量? 作為 while 中的使用條件時,<>能夠把字符串的內容賦給一個特殊的默認變量. 該變量保存了 s/…/…/ 和 print 作用的默認字符串. 使用這些默認變量能夠減少冗余代碼, 但 Perl 新手不容易看明白.

3.如果沒有進行任何替換, 那么替換命令之前的 next unless 會導致 Perl 中斷處理當前字符串(轉而開始下一個字符串). 如果在當前字符串中沒有找到單詞重復, 也就不必進行下一步工作.

4.ANSI 轉義序列, 把兩個重疊的詞標記為高亮, 中間的部分則不標記.轉義序列\e[7m用於標注高亮的開始,\e[m用於標注高亮的結束(在 Perl 的正則表達式中, \e用來表示 ASCII 的轉義字符, 該字符表示之后的字符為 ANSI 轉義序列).

5.這個字符串可能包括多個邏輯行, 不過在替換命令標記了所有的重復單詞之后, 我們希望只保留那些包含轉義字符的邏輯行. 正則表達式「^([^\e]*\n)+」能夠找出不包含轉義字符的邏輯行.


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com