Hey, Scripting Guy!正規表現に眉をひそめる

The Microsoft Scripting Guys

この記事で使用しているコードのダウンロード: HeyScriptingGuy2008_05.exe (150KB)

2007 年 11 月 、Scripting Guys はバルセロナで開催された Tech·Ed IT フォーラム カンファレンスに向かう途中、パリで 1 日を過ごすことになりました。この日、私たちはここぞとばかりに、この町の世界的に有名な美術館である、ルーブル美術館を訪れました。

Scripting Guys がルーブルにどう行ったか、ですか。簡単ですよ。ノートルダム寺院まで歩いていって、左に曲がりました。

ああ、すみません。聞き違えました。ルーブルに対してどういった印象を持ったか、ですね。ほとんどの部分については、おもしろいと思いました。唯一問題だったのは、ルーブルが多くの美術館と同様、"お手を触れずにご覧ください" 主義で運営されていたことです。モナリザに眉があったらずっとすてきに見えることはだれでも知っています。しかし、少しでも絵に手を加えようとすると、どういうわけかルーブルの管理者は激怒するのです。

注 : 実のところ、美術館に行った 2 人の Scripting Guys はモナリザが気に入りました。これはうれしい驚きでした。あれほど派手に宣伝され、売り込まれていたので、平凡な絵ではないかと心配していましたから。しかし平凡ではなく、とてもすばらしい絵でした (眉があればもっとよかったのですが)。しかし、興味深いことに、同じくらい派手に宣伝されていたミロのビーナスを見て、私たち 2 人はがっかりしました。2 人とも、この作品がそれほど見事だとは思いませんでした。また、このコラムを執筆している Scripting Guy は、ミロのビーナスの趣旨そのものに困惑しました。「腕のない女性の像だって。一体どうやって床をモップで掃除したり、皿を洗ったりするのだろう」と。

女性読者の皆さん (まだいるとして) への注 : 明らかに今の表現はタイプミスでした。本当は次のように言うつもりだったのです。「腕のない女性の像だって。それなのに男性の 2 倍の仕事をこなすことができて、おまけにその仕事の内容も正確ときたか」

最初の発言が招いた誤解について、お詫び申し上げます。

とにかく、ルーブルのすばらしい財産を目にしたとき、2 人の Scripting Guys はまったく同じことを考えました。それは、トイレはどこにあるのかということです。トイレを探している間、このコラムを執筆している Scripting Guy は別のことが頭に浮かびました。Scripting Guys は偽善者だということです。私たちは、モナリザに眉を書き加えることをルーブルが許可してくれなかったことに腹を立てています。ですが、私たちは同じような罪を犯していたのです。TechNet Magazine の 2008 年 1 月号で、私たちはスクリプト内で正規表現を使用することに関する記事を書きました。これこそ、"お手を触れずにご覧ください" の典型的な例です。私たちは正規表現を使用してテキスト ファイルの問題を特定する方法について説明しましたが、特定した問題を解決する方法については説明しませんでした。ああ、まったくもう。

注 : Scripting Guys が 2007 年の 11 月にルーブルに行ったとしたら、このコラムを執筆している Scripting Guy は、なぜ 2008 年 1 月まで公開されない TechNet Magazine の記事の内容に突然思い至ったのでしょうか。これは難問ですね。きっとレドモンドとパリの時差が関係しているのでしょう。

しかしさいわいにも、ルーブルの管理者と違って、Scripting Guys は進んで間違いを認めます。正規表現を使用して検索を実行する方法だけではなく、正規表現を使用して置換を実行する方法も説明する必要がありました。つまり、図 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 スクリプト内で正規表現を使用できるようになります。オブジェクトを作成したら、そのオブジェクトの次の 3 つのプロパティに値を割り当てます。

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) の 2 つをパラメータとして渡します。次のコードでこの処理を実行します。

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

これで処理は完了です。変更されたテキストは変数 strNewString に格納されます。ここで strNewString の値をエコー バックすると、次の文字列が返されます。

The La Gioconda is in the Louvre.

文法的には少々問題があるかもしれませんが、基本的な考え方はおわかりいただけたと思います。

先ほど説明したように、これはこれで問題ありませんが、確かにやりすぎです。次の数行のコードを使用しても、まったく同じ処理を実行できます (実際、必要であれば 1 行ですべての処理を実行することもできます)。

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

VBScript の Replace 関数では実行できないようなおもしろいことを、正規表現で実行できるかどうか考えてみてください。

だれも思い付きませんか。では、1 つ紹介します。しばしば Scripting Guys は、ある種類のドキュメントから別の種類のドキュメントにテキストをコピーする必要に迫られます。これは非常にうまくいくこともありますが、そうでないこともあります。うまくいかない場合、単語の間隔がおかしくなり、以下のようなテキストが出来上がることがよくあります。

Myer Ken, Vice President, Sales and Services

これは大変ですね。こんなに余分な空白があります。しかも、このような場合、Replace 関数はあまり役に立ちません。なぜかと言えば、一見したところ、余分な空白の数が決まっていないからです。単語の間に空白が 7 つある場合も、2 つある場合も、6 つある場合も考えられます。このため、Replace を使用して問題を解決することは困難です。たとえば、2 つの連続する空白を検索 (および 2 つの空白を 1 つの空白に置換) すると、以下のようなテキストになります。

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 つ (以上) の空白を検索していることが、なぜわかるのでしょうか。二重引用符の内側には、1 つの空白に続いて {2,} という構文が記述されています。正規表現の構文では、これは直前の文字 (この場合は空白) の 2 つ以上の連続を検索することを表します。連続した空白が、3 個、4 個、または 937 個ある場合はどうでしょうか。問題ありません。このような空白もすべて取得できます (なんらかの理由で、2 つ以上 8 つ以下の空白を取得する場合は、{2,8} という構文を使用します。この 8 は一致の最大数を指定しています)。

つまり、2 つ以上の連続した空白が検出された場合は必ず、すべての連続した空白を取得して、1 つの空白に置換します。あれだけ余分な空白が含まれていた元の文字列値はどうなるでしょうか。次のようになります。

Myer Ken, Vice President, Sales and Services

どうですか。Scripting Guys には本当に物事を良い方向に変える力があります。ルーブルの管理者がモナリザに眉を書き加えるチャンスをくれれば申し分ないのですが。

よくある興味深いシナリオを紹介しましょう。企業に電話帳があり、すべての電話番号が次のような形式になっているとします。

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} が連続した 3 つの数字を探すようスクリプトに指示する構文であることを知っていたのでしょうか。覚えている限りでは、どこかで読んだのです。『ダ・ヴィンチ・コード』の衝撃的な最終章か、MSDN® オンラインの VBScript ランゲージ リファレンス (go.microsoft.com/fwlink/?LinkID=111387 を参照) のどちらかに載っていたはずです。

さて、正規表現を使用して任意の電話番号を検索できるのはとてもすばらしいことですが、まだ大きな問題が残っています。結局のところ、自由気ままに数字が並んだ電話番号を同じく自由気ままに数字が並んだ電話番号に置換することはできません。この場合、まったく同じ電話番号をやや形式が異なる電話番号に変更する必要があります。一体どのような方法を使用すればよいでしょうか。

次のような置換テキストを使用します。

"($1) $2-$3"

$1、$2、および $3 は、正規表現の "後方参照" の例です。後方参照は、単なる検出されたテキストの一部であり、保存して再利用できます。このスクリプトでは、次のような 3 つの "サブマッチ" を検索しています。

  • 3 桁の数字
  • 別の 3 桁の数字
  • 4 桁の数字

それぞれのサブマッチは自動的に後方参照に代入されます。最初のサブマッチは $1 に、次のサブマッチは $2 に代入され、同様に $9 まで処理されます。つまり、このスクリプトでは、電話番号の 3 つの部分が自動的に後方参照に代入されます (図 2 参照)。

Figure 2 電話番号の後方参照

番号部分 後方参照
555 $1
123 $2
4567 $3

今回の置換テキストでは、これらの後方参照を使用して、正しい電話番号が再利用されるようにします。この置換テキストは、単に次のような処理を実行するよう指示しています。まず、最初の後方参照 ($1) を取得し、かっこで囲みます。次に空白を空けてから、2 つ目の後方参照 ($2) を挿入し、その後ろにダッシュを挿入します。最後に 3 つ目の後方参照 ($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

後方参照の一般的な使い方をもう 1 つ紹介します。次のような文字列値があるとします。

Myer, Ken

2 つの単語を入れ替えて、次のように名前を表示する方法はあるでしょうか。

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+ を使用して 1 つの単語を表しています)。\S+ という部分は、空白以外の文字の連続を意味します。つまり、文字、数字、記号を取得できます。実際、スペース、タブ、改行以外のあらゆる文字を取得できます。ご覧のとおり、ここでは 2 つのサブマッチが検出されます。一方は姓を表し ($1)、もう一方は名を表しています ($2)。このため、次の構文を使用して、ユーザー名を名、姓の順に表示します。

"$2 $1"

コンマはどこへ行ったのでしょうか。これは明らかに不要だったので、削除しました。

注 : おかしいですね。なぜか、編集者の顔が思い浮かびました。うーん。

今日の締めくくりに、もう 1 つスクリプトを紹介しましょう (そうですね、今月の締めくくりです)。このコラムのような初心者向けの記事をあまり複雑にしたくはありませんが、これはだれもが簡単に理解できるスクリプトではありません (正規表現は非常に複雑になりやすいという特徴があります)。いずれにせよ、以下のスクリプトを使用すると、ほとんどの場合、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

例によって、このスクリプトが機能する理由はただ 1 つ、"\b0{1,}\." というパターンが記述されているからです。まず、単語の境界 (\b) を検索します。これにより、100.546 などの値に含まれているゼロが削除されなくなります。次に、1 つ以上のゼロ (0{1,}) に続く小数点 (\.) を検索します。パターンが検出された場合は、検出されたゼロ (および小数点) を "." という 1 つの小数点に置換します。すべてが予定どおりに進めば、変換後の文字列は次のようになります。

The final value was .34500044.

今月の記事もそろそろ終わりです。最後にお伝えしておきますが、モナリザは絵の具も乾かないうちから大きな論争の的となりました。この謎めいた女性はだれだろう、なぜあのように微笑んでいるのだろう、なぜ眉がないのだろう、といったさまざまな疑問が出てきました。数人の美術史家は、モナリザは女性でさえなく、この絵はレオナルド ダ ヴィンチの自画像だという説を唱えています (それが事実なら、ダ ヴィンチは実際に新しい仕立て屋を見つけることができたでしょう)。一方、ユナリアス教育財団 (Unarius Educational Foundation) はさらに一歩踏み込んで、実はこの絵は "高等世界" にいるレオナルドの "双子の魂" を描いたもので、この双子の魂がレオナルドの手を導いたと主張しています。奇遇にも、今月の Hey, Scripting Guy! もまったく同じ方法で執筆されました。

ということで、すべての苦情は、もう 1 つのマイクロソフトでコラムを執筆している Scripting Guy の双子の魂 (the-twin-soul-of-the-scripting-guy-who-writes-that-column@the-other-microsoft.com) までお願いします。それでは。

Dr. Scripto のスクリプト パズル

パズルを解くスキルだけでなく、スクリプト作成スキルもテストする月に一度の課題です。

2008 年 5 月 : スクリプト版数独

今月は、少しひねりを加えた数独をお届けします。マス目には 1 から 9 までの数字ではなく、Windows PowerShell™ コマンドレットを構成する文字と記号が入ります。すべて解くと、横向きに読むことができる行の 1 つにコマンドレット名が現れます。

注 : 数独の解き方をまだ知らない場合は、インターネット上のおそらく何千もの Web サイトにルールが載っているので、ここでは説明を省略します。ご了承ください。

ANSWER:

Dr. Scripto のスクリプト パズル

解答 : スクリプト版数独 (2008 年 5 月)

The Microsoft Scripting Guys は、マイクロソフトの仕事をしています、というよりもマイクロソフトにより雇われています。野球をプレイしたり監督したり観戦したり (または他のさまざまな活動を) しているのでない限り、彼らは TechNet スクリプト センターを運営しています。詳細については、www.scriptingguys.com を参照してください。

© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.