嗨,Scripting Guy!對規則運算式挑大眉

The Microsoft Scripting Guys

下載本文程式碼: HeyScriptingGuy2008_05.exe (150KB)

在 2007 年 11 月 ,Scripting Guy 必須在巴黎度過一天,途中再去參加於巴塞隆納舉辦的 Tech•Ed IT 論壇。在一日的行程中,我們趁機造訪了舉世聞名的藝術博物館 ─ 羅浮宮。

那麼 Scripting Guy 是怎麼看羅浮宮呢?很簡單:我們走到聖母院,然後左轉。

喔,您是指我們覺得羅浮宮好玩嗎?大部分還不錯。唯一的問題是,羅浮宮就像很多博物館一樣,只准看不准摸。大家都曉得蒙娜麗莎要是有眉毛的話,看起來美多了,但是不知道為什麼,如果有人想要動這些畫作,羅浮宮的管理人員就會很生氣。

備註: 其實兩位 Scripting Guy 都很喜歡蒙娜麗莎。而且那次的經驗又愉快又驚喜;經過這麼多媒體炒作,我們本來擔心它可能只是普通的一幅畫。但其實不然,它真的很酷 (不過加上幾根眉毛更棒)。然而有趣的是,我們兩個都對同樣高人氣的「米羅的維納斯」感到失望。我們都不覺得這個作品有這麼了不起,而且撰寫這篇專欄的 Scripting Guy 被「米羅的維納斯」的意象搞糊塗了。一尊沒有手臂的女人雕像?她要怎麼擦地板或洗碗盤!?

給女性讀者的話 (如果我們還有女性讀者沒被氣跑的話):很顯然這裡印刷錯誤。我們的原意是:一尊沒有手臂的女人雕像?她還是能做比男人多兩倍的工作,而且做得很不賴。

如果原文引發任何誤解,Scripting Guy 深感抱歉。

無論如何,正當我們欣賞著羅浮宮的偉大珍藏時,兩位 Scripting Guy 突然想到同一件事:廁所在哪裡?而執筆本專欄的 Scripting Guy 在整理資料時想到另一件事:Scripting Guy 真是偽君子。畢竟,我們一方面很氣羅浮宮不讓我們修正蒙娜麗莎的畫像。而另一方面,我們其實也犯了同樣的罪。在 2008 年 1 月份的《TechNet Magazine》中,我們寫了一篇有關在指令碼中使用規則運算式的文章。那篇文章可說是標準的只准看不准摸的例子:我們示範如何使用規則運算式來識別文字檔中的問題,但是並未示範如何修正這些問題。真該死!

備註: 如果 Scripting Guy 在 2007 年 11 月到羅浮宮,那麼撰寫這篇專欄的 Scripting Guy 怎麼會靈機一動,想到一篇直到 2008 年 1 月才登上《TechNet Magazine》的文章呢?哇,這真是個謎啊,不是嗎?這一定跟里德蒙與巴黎的時差脫不了干係。

但還好 Scripting Guy 不像那些羅浮宮的管理人員,Scripting Guy 樂於承認自己的錯誤。我們不該只是示範如何使用規則運算式進行搜尋;我們也應該示範如何使用規則運算式來執行取代動作。事實上,我們應該要示範如 [圖 1] 所示的指令碼。

Figure 1 搜尋和取代

      Set objRegEx = _
    CreateObject("VBScript.RegExp")

objRegEx.Global = True   
objRegEx.IgnoreCase = True
objRegEx.Pattern = "Mona Lisa"

strSearchString = _
    "The Mona Lisa is in the Louvre."
strNewString = _
    objRegEx.Replace(strSearchString, _
                     "La Gioconda")

Wscript.Echo strNewString 

老實說,這種規則運算式用法其實很普通:我們只是將所有 Mona Lisa 字串值的例項都取代為 La Gioconda (這是義大利文,意思是「我把眉毛放哪兒去了?」)。的確,我們可以使用 VBScript Replace 函式,讓這個取代作業更容易執行。但是別怕:我們將使用這個簡單的一小段指令碼來說明如何使用規則運算式執行搜尋和取代作業,而且當我們這麼做之後,我們還會示範如何使用這些運算式來執行一些更花俏的作業。

如您所見,這個指令碼其實沒什麼。我們一開始先建立 VBScript.RegExp 物件的執行個體;當然,就是這個物件能讓我們在 VBScript 指令碼內使用規則運算式。建立物件之後,我們接著指派值到物件的三個屬性:

Global 藉由將此屬性設為 True,我們告訴指令碼搜尋 (並取代) 在目標文字中找到的每個 Mona Lisa 的例項。如果 Global 屬性設為 False (預設值),指令碼就只會搜尋和取代 Mona Lisa 的第一個例項。

IgnoreCase 將 IgnoreCase 設為 True 會告訴指令碼我們要執行區分大小寫的搜尋;換言之,我們會將 mona lisa 與 Mona Lisa 一視同仁。在預設情況下,VBScript 會進行區分大小寫的搜尋,這表示 — 多虧了大小寫字母 — mona lisa 與 Mona Lisa 會被視為完全不同的值。

Pattern Pattern 屬性會保存我們要尋找的值。在此例中,我們只要尋找一個簡單的字串值:Mona Lisa。

接著,我們將要搜尋的文字指派到名為 strSearchString 的變數:

strSearchString = "The Mona Lisa is in the Louvre."

然後我們呼叫規則運算式方法 Replace,傳送兩個參數給這個方法:我們要搜尋的目標文字 (strSearchString 變數) 與取代文字 (La Gioconda)。我們在此就是這麼做:

strNewString = objRegEx.Replace(strSearchString, "La Gioconda")

這樣就大功告成了。修改過的文字會儲存在變數 strNewString 中。如果現在回應 strNewString 值,應該會得到以下結果:

The La Gioconda is in the Louvre.

文法可能有待商確,但是大致的概念就是這樣。

如先前提過的,這麼做不錯,只是太大才小用了;藉由使用下列幾行程式碼,也可以完成一模一樣的作業 (事實上,如果我們願意,就算只使用一行程式碼也辦得到):

strSearchString = "The Mona Lisa is in the Louvre."
strNewString = Replace(strSearchString, "Mona Lisa", "La Gioconda")
Wscript.Echo strNewString

換句話說,我們來看一看可以使用規則運算式來執行哪些 VBScript 的 Replace 函式做不到的有趣作業。

沒人想得到嗎?這裡舉個例子。我們 Scripting Guy 總是需要從某種類型的文件複製內文到另一種文件類型。有時進行的很順利,有時則不然。在不順利的情況下,我們常碰到奇怪的文字間距問題,這種問題是類似以下的文字產生的:

Myer Ken, Vice President, Sales and Services

哎呀;瞧瞧看這些多餘空格!在這個案例中,Replace 函數的用處有限。為什麼呢?其中的多餘空格數目沒有一定的規律:字詞之間可能有 7 個空格,可能有 2 個空格,或者可能有 6 個空格。因此很難使用 Replace 來修正問題。譬如說,若我們試著搜尋 2 個連續空格 (以單一空格取代這些 2 個空格),結果如下:

Myer Ken, Vice President, Sales and  Services

這樣好一點,但也沒多棒。雖然可以這麼做,但是我們必須搜尋任意數目的空格 (譬如 39);進行取代;從最開始的數字減 1;搜尋 38 個空格;進行取代;依此類推。或者,我們可以使用這個簡單多了的 (而且更萬無一失) 規則運算式指令碼:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = " {2,}"

strSearchString = _
"Myer Ken, Vice President, Sales and Services"
strNewString = objRegEx.Replace(strSearchString," ")

Wscript.Echo strNewString

此指令碼的關鍵 (同時也是多數規則運算式指令碼的關鍵) 在於 Pattern:

objRegEx.Pattern = " {2,}"

我們在此想要尋找 2 (或更多) 個連續空格。我們怎麼知道此 Pattern 會尋找 2 (或更多) 個空格?這個嘛,我們在雙引號內加入單一空格並緊接著下面這個建構:{2,}. 在規則運算式語法中,這表示要尋找前置字元 (在本例中是空格) 至少連續出現 2 次的例項。萬一有 3 個、4 個或 937 個連續空格怎麼辦?沒關係;這些全都抓得到 (如果基於某種原因,我們想要抓取至少 2 個但不超過 8 個的空格,那麼我們可以使用語法 {2,8},這裡的 8 是用來指定符合項目的上限)。

也就是說,每當我們找到 2 個以上的相連空格時,便會抓取所有這些連續空格,並取代為單一空格。我們原本充滿多餘空格的原始字串值會變成怎麼樣呢?就是這樣:

Myer Ken, Vice President, Sales and Services

看到了嗎?Scripting Guy 真的可以把事情做的更好。只要羅浮宮人員願意讓我們解決蒙娜麗莎的問題。

有一個案例很有趣 — 而且還滿常見的。假設貴公司有一個電話目錄,所有電話號碼都採用以下格式:

555-123-4567

但是您的老闆現在突然決定要使用下列格式來顯示所有電話號碼:

(555) 123-4567

您到底該如何重新格式化這些電話號碼?請恕我們冒昧,大膽建議您使用如下的指令碼:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString, "($1) $2-$3")

Wscript.Echo strNewString

在此我們要搜尋 3 位數 (\d{3}),接著是一橫線,之後又是 3 位數和一橫線,最後再緊接 4 位數。換句話說,我們要搜尋以下格式,而每個 X 都代表 0 到 9 的一個數字:

XXX-XXX-XXXX

備註:我們怎麼知道 \d{3} 會告訴指令碼尋找三個數字三連轟?我們依稀記得在某處讀到這個用法。事實上,若非是《達文西密碼》出人意表的最後一章,想必是線上 MSDN® 提供的 VBScript 語言參照 (請參閱 go.microsoft.com/fwlink/?LinkID=111387)。

現在,我們可以使用規則運算式來搜尋任意的電話號碼,這還真不賴。不過此處還是有一個重大問題。畢竟,我們不可能也使用任意電話號碼來取代這些任意的電話號碼;而是必須使用完全一樣的電話號碼,只是格式有點不同。我們到底該怎麼做?

當然是使用下列取代文字囉:

"($1) $2-$3"

$1、$2 和 $3 是規則運算式「反向參考」的例子。反向參考就是找到的文字中可儲存並重複使用的部分。在這個特殊指令碼中,我們要尋找三個「子對應」:

  • 一組 3 位數
  • 另一組 3 位數
  • 一組 4 位數

這些子對應分別都會自動指派一個反向參考:第一個子對應式 $1;第二個是 $2;依此類推,一直到 $9。也就是說,在此指令碼中,電話號碼的三個部分都會自動指派反向參考,如 [圖 2] 所示。

Figure 2 電話號碼反向參考

電話號碼部分 反向參考
555 $1
123 $2
4567 $3

在取代字串中,我們使用這些反向參考來確保會重複使用正確的電話號碼。我們的取代文字只是表示:將第一個反向參考 ($1) 放入括弧中。空一格,然後插入第二個反向參考 ($2),接著是一橫線。最後,加上第三個反向參考 ($3)。

這樣會產生什麼結果呢?結果是得到如下所示的電話號碼:

(555) 123-4567

真是一點也不賴。

電話號碼指令碼還可以稍加變化。假設貴公司安裝了全新的電話系統,根據此轉變,所有的電話號碼現在都有相同的首碼;原本可能以 666、777 或 888 開頭的電話號碼現在全都以 333 開頭。我們可以重新格式化電話號碼,同時又變更電話號碼首碼嗎?當然沒問題:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString,"($1) 333-$3")

Wscript.Echo strNewString

看出來我們怎麼辦到的嗎?我們直接移除取代文字中的舊首碼 (反向參考 $2);將此替補為硬式編碼的標準首碼值 333。執行這段修改過的指令碼之後,電話號碼 555-123-4567 會變成什麼樣子?結果應該很像這樣:

(555) 333-4567

反向參考還有一個常見的用法。假設我們的字串值如下所示:

Myer, Ken

有什麼辦法可以調換這個值以顯示如下的名稱呢:

Ken Myer

要是沒有辦法,我們豈不是面上無光?下面就是執行這項作業的指令碼:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\S+), (\S+)"

strSearchString = "Myer, Ken"
strNewString = objRegEx.Replace _

strSearchString,"$2 $1")

Wscript.Echo strNewString

在這個特定指令碼中,我們要尋找後面接著逗號的單字 — (\S+),逗號後面緊接著空格,之後是另一個單字 (在本例中,我們使用 \S+ 來代表「單字」)。建構 \S+ 表示任何非空白字元的連續組合。換言之,字母、數字、符號皆可;事實上,除了空格、定位鍵或歸位換行以外,幾乎什麼都可以。如您所見,我們預期在這裡找到兩個子對應:一個代表姓氏 ($1),另一個代表名字 ($2)。因此,我們可以使用以下語法將使用者名稱顯示為 FirstName LastName:

"$2 $1"

逗號到哪兒去了?顯然我們並不需要逗號,所以直接丟掉了。

備註:真好玩;不曉得為什麼,我們也開始跟 Scripting Editor 有同感了。嗯...

本日課程結束前,讓我們再探索一個例子吧 (好吧,更正一下,在本月課程結束前)。這並非萬無一失的作法;畢竟,我們可不希望介紹性質的本專欄變得太複雜 (規則運算式的確有潛力變得極度複雜)。不過,有一個指令碼 — 在大部分的情況下 — 可以移除 0000.34500044 這類數值之前的零:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\b0{1,}\."

strSearchString = _
"The final value was 0000.34500044."
strNewString = objRegEx.Replace _
strSearchString,".")

script.Echo strNewString

一如往常,這能成功的原因全仰賴 Pattern:"\b0{1,}\."我們一開始先尋找字元界限 (\b);這麼做可確保我們不會將 100.546 這類數值中的零刪除。接著尋找後面緊接小數點 (\.) 的 一或多個零 — 0{1,}。如果找到此模式,就以一個小數點「.」來取代這些零 (還有小數點)。如果一切照計畫進行,字串就會轉換成下面的樣子:

The final value was .34500044.

本期專欄已接近尾聲。結束之前,我們應該提一下,在油彩未乾之前,蒙娜麗莎這幅畫就已經倍受爭議。這位詸樣的女子是誰?她為什麼要這樣微笑?她為什麼沒有眉毛?多位藝術歷史學家認為蒙娜麗莎根本不是女性,而是達文西的自畫像 (如果是真的,他實在應該換位裁縫師了)。另一方面,Unarius 教育基金會甚至更進一步宣稱這幅畫其實是達文西在「更高世界」中的「雙生靈魂」,而且這個雙生靈魂引導了達文西的作畫。出於極端巧合,本月的《嗨,Scripting Guy!》也是這樣完成的。

這表示所有抱怨都應該傳送到本專欄作者在另一家微軟工作的雙生靈魂的電子郵件信箱:the-twin-soul-of-the-scripting-guy-who-writes-that-column@the-other-microsoft.com。謝謝您。

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

這個每月一次的挑戰不僅測試您的解謎功力,更要測試您的指令碼技巧。

2008 年 5 月:指令碼獨

這個月我們要來玩玩有點不一樣的數獨。我們不在格子中填入 1 到 9 的數字,而是填入構成 Windows PowerShell™ 指令程式的字母和符號。在最後解答中,其中一橫列會拼出指令程式的名稱。

備註:如果您不清楚怎麼玩數獨,網際網路上有數千個網站提供玩法解釋,因此就不在此複述了,抱歉。

ANSWER:

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

答案是:指令碼獨,2008 年 5 月

The Microsoft Scripting Guys 為 Microsoft 做事,也就是受雇於 Microsoft。他們在不玩、不教或不看棒球 (或者其他各種活動) 的時候,就負責管理 TechNet 指令碼中心。請造訪他們的網站:https://www.microsoft.com/taiwan/technet/scriptcenter/default.mspx

© 2008 Microsoft Corporation and CMP Media, LLC. 保留所有權利;未經允許,嚴禁部分或全部複製.