解説 12月〜1月(1)

今回の学習項目です。


正規表現とはなにか

正規表現は「文字列のパタン」、つまり文字列の集合を「一つの文字列で表現」する方法のことです。
例えば、"abc" と "abd" という「2つの文字列」を考えましょう。 この「2つの文字列」は、正規表現を使うと abc|abd というように「一つの文字列」で表現できます。

ここで | は特殊な意味をもつメタ文字というも のです。この| は、それが現れたところで文字列を2つにわけ、左の文字列と右の文字列の「選択」を意味します。 だから、abc|abd の例では、abc と abd という2つの文字列を表したことになります。 (ちなみに、 abc|abd|abe だと abc, abd, abeの3つの文字列を表します)

正規表現には | のようなメタ文字が幾つか用意されています。いろいろなメタ文字を学ぶことでよりよく正規表現を使いこなすことができます。メタ文字については課題2で学びます。

課題1では正規表現の使い方を学びます。

課題1. 正規表現の使い方

Rubyでは正規表現をパタンマッチ(文字列照合)に使います。パタンマッチとは、ある文字列に対して別な(正規表現で指定された)文字列が出現しているかどうか(部分として含まれているかどうか)を調べる操作のことを言います。例えば、"ねんねここねこここねねこおやねここねこおおねこねんねこ" という文字列の中に「こねこ」があるかどうかを調べることを考えましょう。これがパタンマッチです。そして確かに「こねこ」という文字列が含まれていればパタンマッチ成功、そうでなければ失敗となります。

この問題は今まで学んできたRubyの方法ではできません(正直言えば、できなくはありませんが、難しい問題になります)。これを解決してくれるのが正規表現、というわけです。

まず、一つ注意があります。Rubyでは「正規表現」は「文字列の集合を表現」しますが、「正規表現」は文字列とも配列とも異なるクラスです。
正規表現を表すのによく用いられる表現方法では、以下に示すように文字列の前後に / (スラッシュ)を書いて表します。

   /abc/           # 文字列 abc を表す正規表現
   /abc|abd/       # 文字列 abcとabdの「選択」を表す正規表現
   /こねこ/        # 日本語も使えます

[正規表現のいろいろな表現方法]

正規表現を「文字列の前後に / を書いて表す」と書きましたが、他にも別な表現方法があります。これはスラッシュ(/)を正規表現に含めたい場合などに使われることがあります。それらを例で示します。

Regexp.new(pat)           # 変数patの値が文字列のとき
%r|これは、正規表現|      # %r|と|で囲む。|は任意の英数文字、例えば!などで置き換え可能
%r(これも 正規表現)       # %r(と)で囲む。括弧は[と]、{と}のように対応する括弧で置き換え可能

課題1-1

先に上げた正規表現の例それぞれに対して、そのクラスが何と表示されるかを確かめなさい。具体的には、以下のようにやればできます。

p /abc/.class
p /abc|abd/.class
p /こねこ/.class

課題1-2

文字列"ねんねここねこここねねこおやねここねこおおねこねんねこ" の中に"こねこ"があるかどうかをRubyの正規表現を使って確かめましょう。 正規表現を使ってパタンマッチをするには、次のように =~ という演算子を使います。

str = "ねんねここねこここねねこおやねここねこおおねこねんねこ" 
ans = ( /こねこ/ =~ str )
これを実行し、変数 ans の値を答えなさい。

課題1-2の結果の変数ansの値は、パタンマッチに成功したことを表す true ではなく、数(整数)です。その数は、"ねんねここねこここねねこおやねここねこおおねこねんねこ" の何文字目から「こねこ」が始まるかを表しています。ただし先頭の文字は0番目という約束になっています(配列の先頭の要素のインデックスも0でしたね)。

この結果からわかるように、正規表現によるパタンマッチでは、対象とする文字列で正規表現にマッチする一番左のもの(そして、一番長いもの)が、マッチング結果として返されます。

課題1-3

課題1-2ではパタンマッチに成功する場合でしたが、失敗する場合はどのような値になるでしょうか。次のプログラムを実行し、変数 ans の値を答えなさい。

str = "ねんねここねこここねねこおやねここねこおおねこねんねこ" 
ans = ( /やまねこ/ =~ str )

課題1-4

Rubyのプログラムではパタンマッチを「条件」として使うことが多いです。つまり、パタンマッチに失敗したときは「条件」が不成立、パタンマッチに成功した場合は「条件」が成立したとして扱われます。

以上のことを前提として、次のプログラムの実行後、変数 resultにはどのような値が入っているでしょうか、予想してください。次にプログラムを実行させ、結果が予想通りかどうか検証しなさい。

result = "なにもいない"
str = "ねんねここねこここねねこおやねここねこおおねこねんねこ" 
if (/やまねこ/ =~ str)
     result = "やまねこいた。"
elsif (/こねこ/ =~ str)
     result = "こねこいた。"
end    # end of if

パタンマッチの記述法:

課題1-5

正規表現を用いたパタンマッチが成功した時

                    $`        $&          $'
という3つの特殊な変数に値がセットされます。どのような値がセットされるか、例で示します。
 if (/cac|cad/ =~ "abracadabra")
     print "$` = #{$`}\n"
     print "$& = #{$&}\n"
     print "$' = #{$'}\n"
 end
この結果は次のようになります。 パタンマッチの対象とした元の文字列"abracadabra"と比べてみてください。(ここで分かりやすさのために色をつけています)

$` = abra
$& = cad
$' = abra

このように、$&はパタンマッチの対象となった文字列のうち、「正規表現にマッチした文字列」がその値となります。また、$`は文字列でマッチした部分の前(左)の文字列、$'はその後ろ(右)の文字列がそれぞれ値となります。

これを使って、"ねんねここねこここねねこおやねここねこおおねこねんねこ" に「こねこ」と「ねこ」がそれぞれ何個含まれているか答えるプログラムを書きなさい。

[課題1-5を解くためのヒント]

"ねんねここねこここねねこおやねここねこおおねこねんねこ"に対して、引数の正規表現が何回現れるかを答える関数(名前を countCatsとしておきます)を作ることを考えましょう。その関数の大枠は次のようになるでしょう。ここで、この関数は引数として"こねこ"や"ねこ"をとるものと考えています。言い換えれば、ここでは、countCats("こねこ")によって"こねこ"の出現回数を数え、countCats("ねこ")によって"ねこ"の出現回数を数えることを想定しています。(ただし、ハッシュを知っている人なら、別な方法を考えつくことでしょうね)

def countCats(pat)
  str = "ねんねここねこここねねこおやねここねこおおねこねんねこ"
  ans = 0     # マッチした回数の記録用
  #
  # strの値の文字列にpatがパタンマッチする回数をカウントし、
  # その回数を返すプログラムを書く
  #
end
関数の中身は、strにpatがパタンマッチする限りansに1加算する、というものとなるでしょう。ここで、strの値を更新しないと無限ループすることに注意してください。つまり、strの値は、パタンマッチに成功したら、成功した後ろの文字列になるようにするのです。

[さらなるヒント]

$'がパタンマッチに成功した時の後ろの文字列になることを思い出しましょう。

また、「strの値の文字列にpatがパタンマッチする回数をカウント」は次のように書けるでしょう。これには繰り返しのwhileを使います(パタンマッチに成功しない場合、whileの「条件が失敗」して繰り返しが終了する)。

  while (pat =~ str)
       ans += 1     # ansに1を加算
       str =        # 適切な値をstrに代入 
  end

ただし、このままではプログラムは完成ではありません。 完成させるにはansの値を返すコード(プログラム)が必要です。 また、この関数がちゃんと動くかどうか試すには、適切な引数を与える必要があります。

課題1-6

英文字が含まれるパタンマッチでは、大文字と小文字とを区別しないことが多いです。例えば、ウェブページ(HTML文書)では、タグは大文字でも小文字でも(またそれを混在しても)構いません。ですからウェブページのタイトルを取り出そうとするためにパタンマッチを行う場合には、titleTITLETiTletitLEのような大文字小文字の混在したいろいろなパタンを使わないとできません。これはとても大変なことです。 そこで、大文字と小文字を区別せずにパタンマッチをさせる方法がRubyの正規表現には備わっています。それは、次のように書きます。

    例: /meta/i =~ "MeTametaMetaMETA"

このように、正規表現(この例では/meta/)の後ろに i というオプションをつけると大文字と小文字を区別しないパタンマッチが行われます。 この例の結果は 0 、つまり先頭の "MeTa"とマッチが成功します。

ウェブページではいろいろな文字コードが使われています。それを判別するプログラムを作ることを考えましょう。以前学んだように、HTML文書ではmetaタグの中でcharsetパラメタで文字コードが設定されています。例えば、www.st.chukyo-u.ac.jp のページではshift_jisが使われています。

次に上げるページではどのような文字コードが使われているかを答えるプログラムを書きなさい。 ただし、open-uriを使える人はそれを用いて、 そうでない人はページの内容をあらかじめダウンロードしてファイルとして保存 (ウェブページを表示してから「ファイル」メニューを選び、「名前をつけてファイルを保存」 を選ぶ)したものを使いなさい。

[open-uriとは]

open-uriとは、指定されたURI(少し前まではURLと言っていました。ウェブ上のファイルなどリソースへのパス(経路)情報の表記のことです。例えばhttp://www.st.chukyo-u.ac.jp/index.htmlがそれです)を普通のファイルと同様にアクセスするためのライブラリです。

 require 'open-uri'
 fin=open("http://search.yahoo.co.jp/search?tt=c&ei=UTF-8&fr=sfp_as&aq=-1&oq=&p=ruby&meta=vc%3D")
 while (line=fin.gets)
    print line
 end
 fin.close
とすると、Yahooの検索ページ(これ自体はCGI)を開き、 そこからgetsメソッドで内容を読み、画面にソースを表示できます。

課題2. 正規表現のメタ文字

課題2では、機能ごとに分類したメタ文字を中心に学びます。

  1. 指定された一文字にマッチ
  2. ¥ (英語環境ではバックスラッシュ \)から始まるパタン
  3. グループ化と特殊変数 $1, $2, ...
  4. 文字または文字列の繰り返しにマッチ
  5. 文字ではなく場所にマッチ

課題2-1 一文字にマッチ

正規表現では | ^ $ ( ) [ ] { } + * ? . ¥ (Rubyではさらに #、また正規表現を / でくくる形式では / も)が特殊な意味を持ちます。

それ以外の文字、例えば英数字や ! " ' % = & > < などの記号やスペースは「それ自体」のパタンを表します。つまり、例えば正規表現で使われる a は a という文字とマッチします(ただし、課題1-6のところで述べたように、i オプションが使われた場合は A という大文字にもマッチします)。

正規表現ではさらに次のようなパタンが一文字にマッチします:

以下を実行した場合、どのような結果が表示されるか。最初に結果を予想してからプログラムを実行し、予想と比較しなさい。結果が予想と異なる場合、その理由を考察しなさい。

  if (/[ a-c].[a^c]/ =~ "xay^abc")
              p $&
  end # if

  if (/[^ a-c].[a^c]/ =~ "xay^abc")
	      p $&
  end # if

課題2 文字コード指定によるパタンマッチの振る舞いの違い

以下を実行した場合、どのような結果が表示されるか。最初に結果を予想してからプログラムを実行し、予想と比較しなさい。なぜこのような結果となるか、その理由を考察しなさい。

  $KCODE = ""      # $KCODEの値のデフォルト
  if (/こ../ =~ "これやこの")
              print  $&,"\n"
  end # if
  $KCODE = "s"     # Windows環境の場合。UNIXでは $KCODE = "e"とすること
  if (/こ../ =~ "これやこの")
              print  $&,"\n"
  end # if

課題2-3 ¥ から始まるパタン

先ほど 「正規表現では | ^ $ ( ) [ ] { } + * ? . ¥ (Rubyではさらに #、また正規表現を / でくくる形式では / も)が特殊な意味を持ちます」ということを書きました(大事な事なので、繰り返します)。それではこれらとマッチするようなパタンはどう書いたらよいでしょうか?それを可能にするのが ¥ です。つまり、例えば/を正規表現に含めたい場合は

     /pattern including \// =~ "this is a sample pattern including /."
とします(これを「エスケープする」と呼びます---本来の「特殊な」意味をキャンセルする、という意味合いです)。他の特殊な意味を持つ文字についても同様です。

このように¥ が特殊な文字の前についた場合は、その文字の持つ特殊性を打ち消す働きをします。これは ¥ 自体についても当てはまります。つまり¥¥¥とマッチするパタンを表します。

もう一つ¥は普通の文字の前についた場合にも特殊な働きをすることがあります。その例を上げます。これらも、条件を満たす一文字にマッチします:

また、¥sの逆、つまりスペースとタブ「以外」の一文字にマッチするパタンは¥Sです。このように小文字を大文字にすると「それ以外」の意味を表すことがあります。この仲間には、¥Dと¥Wがあります。

以下を実行した場合、どのような結果が表示されるか。最初に結果を予想してからプログラムを実行し、予想と比較しなさい。結果が予想と異なる場合、その理由を考察しなさい。

  if (/\S\D\w/ =~ " xay^abc ")
              p $&
  end # if

  if (/\D\W\S/ =~ " xay^abc ")
	      p $&
  end # if

課題2-4 グループ化

このページの最初で正規表現を紹介する時に、|というメタ文字を紹介しました。これは幾つかのパタンの選択を表すものでした。つまり /large|small/largeにもsmallにもマッチするパタンとなります。

ここでlarge sizeにもsmall sizeにもマッチするパタンを書きたいとしましょう。それには、やはり|を用いて/large size|small size/とすればよいのですが、 sizeの部分が共通に現れているのでやや冗長なような気がします。この不満を解消するのがグループ化です。グループ化には丸括弧()を使います(丸括弧もメタ文字です)。グループ化を使うと今のパタンは次のように表されます:

   /(large|small) size/
これは、largesmallの選択をグループ化しています。そしてこれらのマッチングがまず行われ、それが成功したらその後に sizeのマッチングをすることを表すパタンになります。

グループ化をうまく使って、"青森県、宮城県、東京都、愛知県、大阪府、兵庫県。"というような文字列から都道府県名(県名は漢字2文字とする)を次のように表示するプログラムを書きなさい。(注意: 文字列には2文字からなる県名が「、」で区切られて書かれており、最後は「。」で終わるものとします。この例だけ答えられるプログラムではなく、この規則を守る文字列すべてに対処できるようにしてください)

青森県
宮城県
東京都
愛知県
大阪府
兵庫県

グループ化をする別なメリットもあります。それは、パタンマッチに成功した場合、$1 $2 ... $9という特殊な変数に値がセットされる、ということです。この数字は、正規表現の先頭から数えて何番目に左括弧「(」が出てきたものかを表しています。そして、$n(nは1〜9)には、左括弧から対応する右括弧までの部分にマッチした文字列が値としてセットされます。

以下の例を実行して学んでください。
    if (/(((a.).*)(e.+))h/  =~ "(abcdefghij)")
       print "$1 = "; p $1
       print "$2 = "; p $2
       print "$3 = "; p $3
       print "$4 = "; p $4
    end
この結果は次のようになります。
$1 = "abcdefg"
$2 = "abcd"
$3 = "ab"
$4 = "efg"

課題2-5 繰り返しにマッチ

ここでは次の3通りの文字列の「繰り返し」にマッチする方法を紹介します。

具体的な例をみていきましょう。
  p $& if (/\w?/ =~ "abc")        # ?の例1
  p $& if (/\w?/ =~ "(abc)")      # ?の例2
  p $& if (/\w+/ =~ "(abc)")      # +の例
  p $& if (/\w*/ =~ "(abc)")      # +の例1
  p $& if (/\W\w+/ =~ "(abc)")      # +の例2

これらを実行してどのような表示がされるかを見てください。?+*も、直前のパタン(文字)の繰り返しを表しています。この場合は¥wという「一文字パタン」の繰り返しとなります。注意すべきは「0回」の扱いです。「0回」というのは「そのパタンが現れていなくてもマッチ」を意味します。これを考慮すれば、これらの振る舞いが理解できるでしょう。

この繰り返しのパタンではどのくらいの長さのパタンとマッチするか、気になるところでしょう。正規表現では「最長のパタン」にマッチ、というのが原則です。ただし、 +*の後ろに?をつけた+?*?では「最短のパタン」にマッチします。

実はこのような繰り返しが力を発揮するのは、グループ化と組み合わされた場合です。今の例では「直前の一文字パタン」の繰り返ししかできませんでしたが、グループ化と組み合わされれば、いろいろなパタンの繰り返しとマッチするパタンがかけます。例を見ましょう。

 p $& if (/(abra|cad)+/ =~ "target is abracadabra nanchatte")

この結果は"abracadabra"です。これは"abra"+"cad"+"abra"という2つのパタンの繰り返しになっています。

これらを前提として次の問題に答えなさい。

次はあるページのソースの一部です。

 <a href="http://digital.asahi.com/?ref=comtop_middle_chokan">今日の朝刊</a>

このような文字列を引数とし、その文字列からhrefタグによるリンク先のURL(ウェブのサイトとパスからなる文字列)を取り出して表示する関数(メソッド)extractURL を作りなさい。例えば今の例では以下が結果として返されます。

http://digital.asahi.com/?ref=comtop_middle_chokan
[課題2-5のヒント]

HTMLでは他のページへのリンクは<a href="URL">と書かれます。 ここでURLの部分が取り出したいURL情報です。注意すべきは、ページによって、ahrefタグは大文字で書かれることもある、ということです(大文字小文字の混同も許されている)。これを扱うには i オプションが便利でしょう。

またURLの部分は二重引用符(")でくくられていることが特徴です。これを利用した正規表現を書きましょう。

課題2-6 場所にマッチ

今まで扱ってきたパタンは、基本的に文字にマッチするものでしたが、文字列の先頭から始まるパタンや、文字列の最後にある文字列にマッチするパタン、というように、マッチする「場所」を指定したいこともあります。これを可能にするのが次のメタ文字です。

重ねて注意しますが、これらのメタ文字は特定の文字にマッチするものではなく、場所にマッチするものです。これらを使った正規表現の例が以下です。
    p $1 if (/^(a|the)/i =~ sentence)
    p $1 if (/(\w+)[.;:"'\s]$/ =~ sentence)
最初の例は、変数 sentence がaもしくはtheから始まる単語(大文字小文字を区別せず)から始まる場合にその単語を表示し、二番目の例は、変数 sentence が.;:"'もしくはスペースで終わる場合に、その直前の単語を表示するものです。

指定されたファイル(EUCで書かれています)を読み込み、それぞれの行の先頭の一文字を切り出し、それらをつなげて表示するプログラムを書きなさい。またそれぞれの行の最後の一文字をつなげて表示するプログラムも書いてみましょう。

プログラミングIIIのホームに戻る