正規表現


以下のようなリストから、組織の名前に「東北」と「大学」が含まれている項目を抽出してくださいというタスクを与えられたらどうすれば良い?テキストエディタ(Mousepadなど)やブラウザでCtrl+fを押すと完全一致の検索(検索している文字列と完全に一致する文字列を抽出する検索)を利用できる。しかし、リストには「東北」が名前に含まれていない大学もあるし、「東北」を含む大学以外の組織の名前も載っているので、一回の完全一致検索では必要な情報だけに絞り込むことができない。

phonelist.txt

氏名		電話番号		組織
酒井忠勝		123-321-1234	東北公益文科大学
田中太郎		321-123-4321	株式会社東北通信
清少納言		012-012-0123	平安女学院大学
伊達政宗		001-210-3210	東北大学
松尾芭蕉		981-871-7651	東北文化推進センター
宮本武蔵		135-975-5555	株式会社播磨製鉄
島津斉彬		653-799-3245	サツマイモ商会
近松門左衛門	223-900-5432	福井大学
紫式部		808-051-3829	京都大学
鳥居忠政		777-666-8765	東北文教大学
勝海舟		003-004-1234	長崎海軍伝習所

考えてみれば、抽出したい項目の組織名は全て「東北〇〇大学」(ここで「〇〇」は、空の文字列を含めて、任意の文字列を意味する)というパターンに従っている。今回は、コンピュータ上で検索するときにこのようなパターンを記述するための表記法である正規表現を紹介する。

たとえば、上記のパターンを正規表現で記述したい場合は、次のようになる: 東北.*大学 (「.*」は0以上の任意の文字を意味している)


egrepコマンドによる正規表現の実験

実際に正規表現で検索する練習をしてみよう。 まずは上記のデータ(phonelist.txt)を自分のディレクトリに保存しよう(リンクを右クリックし「Save link as」で、~/Ruby に保存する)。

Unixには正規表現を使ってファイルから特定の行を検索できるegrepというコマンドがある(Rubyとは別物)。以下のように使うと、指定したファイルから「正規表現パターン」にマッチする行だけが出力される。

% egrep "正規表現パターン" [ファイル名]

では、名前に「東北」を含む大学の行を全部表示してみよう(コマンドを実行する前に「cd ~/Ruby」で、ファイルが入っているディレクトリに移動することを忘れないように)。

 練習問題 
sime{c11xxxx}% egrep "東北.*大学" phonelist.txt

結果:

sime{c11xxxx}% egrep "東北.*大学" phonelist.txt
酒井忠勝		123-321-1234	東北公益文科大学
伊達政宗		001-210-3210	東北大学
鳥居忠政		777-666-8765	東北文教大学

Rubyにおける正規表現

Rubyでは、egrepと違い正規表現を / / で括って記述する。したがって、egrepの例で指定した正規表現「東北.*大学」はRubyでは、以下のように表記する。

/東北.*大学/

先ほどのegrepを使った例と同じように、指定したファイルから「東北.*大学」というパターンにマッチする行を出力するRubyプログラムを作成すると次のようになる。

 練習問題  find_tohoku_univs.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

while line=gets
  if /東北.*大学/ =~ line
    print line
  end
end

プログラムを実行してみよう。ファイルを読み込んで実行するので、プログラム名のあとにphonelist.txtを指定する

sime{c11xxxx}% chmod +x tohoku.rb
sime{c11xxxx}% ./tohoku.rb phonelist.txt
酒井忠勝		123-321-1234	東北公益文科大学
伊達政宗		001-210-3210	東北大学
鳥居忠政		777-666-8765	東北文教大学

プログラムの内容を一行ずつ解説しよう。

  1. while line=gets

    gets はこれまで使ったことのあるメソッドである。これまでは、キーボードから一行入力する例のみを示していたが、必ずしもデータ入力がキーボードを通じて行われるとは限らない。Rubyプログラム実行時に何かのファイルを引数として指定するとそのファイルの中味を順次読むようになる。今回の場合「./tohoku.rb phonelist.txt」と起動した。引数としてphonelist.txtを指定したので、getsメソッドは、phonelist.txtファイルを1行ずつ読み込む。つまり、一回目の line=gets

    line="氏名 電話番号 組織\n"

    のような代入が起こり、二回目のgetsでは

    line="酒井忠勝 123-321-1234 東北公益文科大学\n"

    となる(\nは改行文字)。12行あるので、「while line=gets」は12回繰り返されることになる。getsはこれ以上読むデータがなくなると nil を返し、while ループは終了する。

  2. if /東北.*大学/ =~ line

    このifで、読み込んだ行が正規表現にマッチしているかの判定を行なう。「正規表現にマッチしているか?」を意味する条件式は

    正規表現 =~ 文字列

    のように書く。マッチしている場合は、マッチした部分の文字列の中の位置を整数として返す。マッチしない場合は nil を返し、条件不成立となる。

    正規表現の後ろの =~ は、「正規表現がマッチすれば…」という比較演算子である。通常1行ずつ入力した文字列をこの右辺に書く。ちなみに「マッチしなければ」を意味する演算子は !~ である。

    =~ の右辺に指定した「文字列」が正規表現の比較対象となる文字列である。

  3. print line

    line 変数にはデータファイルから読み込んだ行が入っているので、それを出力する。もちろん、一つ上の行に if があるので、正規表現にマッチした場合だけ該当行が出力される。

  4. end

    ifに対応するend

  5. end

    whileに対応するend


正規表現の特殊文字

正規表現では、"" という文字を指定すると、「あ」という文字そのものにマッチする。"AB" という文字列を指定すると、「AB」という文字列そのものにマッチする。「AB」が行の途中にあっても構わない。アルファベットや日本語など普通の文字や文字列を指定すると、それ自身とマッチする。

通常の文字以外に、特別な記号を使うことで、文字そのもの以外にマッチする表現とすることができる。検索のときに特別な意味を持つ文字のことをメタキャラクタという。以下は代表的なメタキャラクタについて解説する。

代表的なメタキャラクタ

Rubyで扱えるメタキャラクタのうち、代表的なものを示す。

. (ピリオド)

記号 . は、任意の1文字にマッチする。たとえば、正規表現 ai.awa は、"ai" の後ろに何でもよいので何か1文字が来て、その後に "awa" が続くようなもの全てにマッチする。たとえば、以下のようになる。

正規表現照合する文字列マッチするか?
/ai.awa/ aikawa
aizawa san
maisawa
aimoto ×
ai awa

照合する文字列のうち実際にマッチしている部分には下線が施してある(以下の例も同様)。

ピリオド自身を探したいときは \. とする。バックスラッシュには特殊文字の働きを打ち消す意味がある。以後の [ ] ? * + ^ $ | ( ) についても同様で、それら自身を探したいときにはそれぞれ、\[ \] \? \* \+ \^ \$ \| \( \) というパターンを指定する。バックスラッシュ自身は \\ で探す。

[ ] (大括弧)

大括弧 [ ] は、その中に列挙した文字のどれか1文字にマッチする。たとえば、正規表現 [abc] は、文字 a, b, c のどれかにマッチする。さらに、正規表現 ai[sz]awa でいくつかの文字列と照合を行なったときの結果は次のようになる。

正規表現照合する文字列マッチするか?
/ai[sz]awa/aikawa ×
aizawa san
maisawa
aimoto ×
ai awa ×

大括弧の中で - (ハイフン) を使うと文字の範囲を指定できる。たとえば正規表現 [0-9] は、文字 0 から文字 9 の範囲の全ての文字、つまり 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 のいずれにもマッチする。もし、マッチする文字としてハイフン自体を指定したいときは大括弧の先頭に記述する。たとえば、正規表現 [-a-z0-9] は、「ハイフン、または a から z のどれか、または 0 から 9 のどれか1文字」にマッチする。

大括弧の中の先頭に ^ (山記号; キャレット) を指定すると、そのあとに列挙した文字以外であるものにマッチする。たとえば、正規表現 [^a-z] は、a から z 以外の文字であればどれでもマッチする。

?

記号 ? は、直前のパターンが0回か1回出現するという意味を付加する。直前のパターンは文字でも記号でもよい。たとえば、正規表現 sai?toh は、? の直前が i なので、i が0個でも1個でもよいことを意味する(つまり、saitosato のどちらにもマッチする)。さらに例えば、正規表現 sa[ei]?ki は、直前のパターンが [ei] なので、「ei」の文字が0個でも1個でもよいことを意味する。つまり、sa[ei]?ki にマッチするのは、saeki, saiki, saki のどれかに限ることになる。

直前のパターンが0文字の場合にもマッチするので、? を正規表現の最後に付けたパターンで検索した結果は付けないものと同じ結果となる。
例: sai?sa で検索するのと同じ結果になる(sa の次に i があった場合にマッチする範囲のみが異なる)。

*

記号 * は、直前のパターンが0回以上出現するという意味を付加する。直前のパターンは文字でも記号でもよい。たとえば、正規表現 sai*toh は、* の直前が i なので、i が0個以上なら何個でもよいことを意味する。つまり、satoh でも saitoh でも saiitoh でも saiiiiiitoh でもマッチする。さらにたとえば、正規表現 sa[ei]*ki は、直前のパターンが [ei] なので、「ei」の文字が0個以上なら何個でもよいことを意味する。

よく使われるのは .* というパターンである。* の直前のパターンが . (任意の文字) なので、何でもよい文字列が何文字続いてもよいことを意味する。つまり、どんな文字列でもマッチする。典型的には 東北.*大学 のように2つのパターンの間に .* を挟み、「『東北』と『大学』の間はなんでもいいや、なくてもいいや」という検索をしたいときに使う。

直前のパターンが0文字の場合にもマッチするので、* を正規表現の最後に付けたパターンで検索した結果は付けないものと同じ結果となる。
例: sai*sa で検索するのと同じ結果になる(sa の次に i があった場合にマッチする範囲のみが異なる)。

+

記号 + は、直前のパターンが1回以上出現するという意味を付加する。回数が「1回以上」という点を除いて記号 * と全く同じと考えてよい。

直前のパターンが1文字の場合にもマッチするので、+ が正規表現の最後になることはない(+ を最後に書くのなら、+ を省略しても同じ行がマッチする)。

^

記号 ^正規表現の先頭に指定した場合のみ文字列の先頭にマッチする。たとえば、正規表現 ^sato は、先頭が sato から始まるもののみにマッチする。文字列の途中に sato が含まれてもマッチしない。つまり、以下のようになる。

正規表現照合する文字列マッチするか?
/^sato/sato san
satoh desu
I am sato ×
Sato! ×

$

記号 $正規表現の末尾に指定した場合のみ文字列の末尾にマッチする。たとえば、正規表現 sato$ は、sato で終わる文字列のみにマッチする。

|

記号 | (縦棒; パイプ) は、「または」の意味で、| の左右のパターンどちらかにマッチすればよいことを意味する。たとえば、正規表現 SAITO|IIMORI は、SAITO または IIMORI のどちらかにマッチする。

( )

丸括弧 ( ) は、長い正規表現の一部分だけを括るときに利用する。たとえば、正規表現 a(wa|ma|yanokou)ji は、| で選択する単語の範囲を限定しているので、結果として awaji, amaji, ayanokouji のどれかを含むものにのみマッチする。もし ( ) で括らずに、awa|ma|yanokouji とすると、awa, ma, yanokouji のどれかを含むものにのみにマッチすることになる。

また、正規表現 a(re)+ は、+ の直前のパターンを括弧で括り (re) としているので、are, arere, arerere, arererere, ... などにマッチする。もし ( ) で括らずに、are+ とすると、+ の直前が e だけになるので、are, aree, areee, ... にマッチすることになる。

このように、正規表現の一部を丸括弧で括って一つのかたまりにすることを、グルーピングという。

なお、元の文字列のうち、グルーピングした部分にマッチした部分文字列はあとで抽出することができる(後述)。

\b

正規表現中の \b は、「単語の境界」にマッチする。たとえば、正規表現 cat で検索すると "category""predicate" がマッチするが、正規表現 \bcat\b は、c の前と t の後ろが単語の境目になっているものにしかマッチしないため、"my cat died" のように単語として独立した cat を含むものにしかマッチしない。

\s

正規表現中の \s は、あらゆる空白文字にマッチする。スペースやTAB文字、改行文字などにマッチする。

\S

正規表現中の \S空白文字以外の全ての文字にマッチする( \s の逆である)。

\d

正規表現中の \d は、0 から 9 (半角)のどれかにマッチする。たとえば、正規表現 \d+ は、0 から 9 が1回以上くり返したものにマッチするので、数字と思われるもの全てにマッチする。

\D

\D は、数字以外の全ての文字にマッチする。 \d の逆である。

\w

[0-9A-Za-z_] と同じ意味である、つまり英語をはじめとするローマ字で表記される言語で単語を構成する文字(英数字とアンダースコア)のどれかにマッチする。

\W

\W は、\w の逆で [0-9A-Za-z_] 以外の1字にマッチする。


正規表現オプション

/ / で括った正規表現の直後に文字を追加すると正規表現の検索方法を変えることができる。たとえば、デフォルトでは英字の大文字と小文字を区別して検索を行なうが、正規表現指定の直後に i を付けて、/saitoh/i とすると、大文字と小文字を区別せずに検索するようになる( SAITOHSaitoh にもマッチする)。これを正規表現オプションという。以下はオプションのうち覚えておくべきものを紹介する。


マッチした文字列を抽出する

正規表現が文字列にマッチした場合、対象の文字列の中のマッチした部分文字列を、特別な変数 $& によって取り出すことができる。このように正規表現にマッチした部分文字列を後から参照することは後方参照(backreference)と呼ばれる。

 練習問題  backref_test.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

text = "長い文字列のマッチした部分だけ出力しましょう"
/マ.+分/ =~ text
puts $&

結果:

マッチした部分

また、正規表現の中で丸括弧 ( ) を使うと、括弧の順番に応じて $数字 ($1, $2, ...) という特別な変数によってそれぞれの括弧にマッチした部分文字列を抽出できる。

後方参照を用いて、テキストファイルからそれぞれの商品の名前と価格と在庫数を抽出し、総価格を出力するプログラムを作成しよう。

 練習問題 

products.txt~/Ruby に保存すること)

商品       価格    在庫数
パン       99円     100
チョコレート  150円      50
おにぎり    120円      15
コーラ     159円     200
ボールペン   199円     120
total_value.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

while line=gets
  if /(\S+)\s+(\d+)円\s+(\d+)/ =~ line
    product = $1
    price = $2.to_i
    quantity = $3.to_i
    total = price * quantity
    printf("%sの総価格: %d\n", product, total)
  end
end

結果:

sime{c11xxxx}% ruby total_value.rb products.txt
パンの総価格: 9900
おにぎりの総価格: 1800
コーラの総合格: 31800
ボールペンの総価格: 23880

解説:


正規表現の指定方法

最初に紹介したRubyでの正規表現指定方法は、パターンを / / で括るというものだった。それ以外にもいくつか方法がある。 プログラム中で正規表現を指定するには以下のどれかを使う。


irb (Interactive Ruby) で試す

正規表現は、思った通りマッチしない問題などにはまりやすい機能である。Emacsktermを行ったり来たりしながら何度も試すのは困難である。そんな時に短いRubyのコードを簡単に試せる機能として、irb (Interactive Ruby) がある。

irbを起動するためには、ktermirb というコマンドを実行する。すると下記のようなプロンプトが表示され、ここにRubyのコードを入力する。

irb(main):001:0>

たとえば、puts 10 / 2 と書いてEnterを押すと下記のような結果が出力される。

 練習問題 
irb(main):001:0> puts 10 / 2
5
=> nil

=> の後ろに表示される部分は実行したコードの結果として返された値である(putsは常にnilを返す)。

正規表現を試したい場合は、下記のように記述する。

 練習問題 
irb(main):001:0> "価格は200円です。" =~ /\d+/
=> 3
irb(main):002:0> $&
=> "200"

4文字目から始まる200にマッチしているので、=> 3 と表示されている(文字列は配列と同様に、文字のインデックスは0から始まる)。また、$& を参照して実際にマッチした部分文字列を確認できた。

ktermのシェルと同様に、前に実行した処理を再度実行したい時は、カーソルキー で読み出せる。ぜひ活用してみてください。


本日の課題

 基本課題 

正規表現を使って、以下のファイル grades.txt の中から「基礎プログラミングI」の成績が「可」か「不可」の行だけ出力するプログラム grad_filter.rb を作成せよ。

grades.txt

学籍番号		科目			成績
C199000001	情報リテラシー		秀
C199000002	情報リテラシー		優
C199000003	情報リテラシー		優
C199000004	情報リテラシー		優
C199000005	情報リテラシー		秀
C199000006	情報リテラシー		可
C199000007	情報リテラシー		優
C199000008	情報リテラシー		不可
C199000009	情報リテラシー		優
C199000010	情報リテラシー		優
C199000011	情報リテラシー		可
C199000012	情報リテラシー		優
C199000013	情報リテラシー		優
C199000001	データリテラシー		良
C199000002	データリテラシー		優
C199000003	データリテラシー		優
C199000004	データリテラシー		可
C199000005	データリテラシー		優
C199000006	データリテラシー		秀
C199000007	データリテラシー		良
C199000008	データリテラシー		可
C199000009	データリテラシー		秀
C199000010	データリテラシー		優
C199000011	データリテラシー		不可
C199000012	データリテラシー		良
C199000013	データリテラシー		優
C199000001	基礎プログラミングI	優
C199000002	基礎プログラミングI	秀
C199000003	基礎プログラミングI	可
C199000004	基礎プログラミングI	優
C199000005	基礎プログラミングI	優
C199000006	基礎プログラミングI	不可
C199000007	基礎プログラミングI	良
C199000008	基礎プログラミングI	秀
C199000009	基礎プログラミングI	優
C199000010	基礎プログラミングI	優
C199000011	基礎プログラミングI	可
C199000012	基礎プログラミングI	可
C199000013	基礎プログラミングI	優

実行結果:

sime{c11xxxx}% ruby grad_filter.rb grades.txt
C199000003	基礎プログラミングI	可
C199000006	基礎プログラミングI	不可
C199000011	基礎プログラミングI	可
C199000012	基礎プログラミングI	可

本文は下記の通り記入してください.

氏名: 苗字名前
学籍番号: C11xxxxx

ソースコード:
...

実行結果:
...

 発展課題 

  1. grad_filter.rb を、どの科目とどの成績の行を出力するかはユーザーが選べるように改良すること。
  2. 与えられたHTML文書内に含まれるタグを抽出し、ユニークなタグ名のリストを作成するプログラムを作成すること。例えば、以下のHTMLテキストが与えられた場合:

    <html>
    <head>
      <title>Sample Page</title>
    </head>
    <body>
      <h1>Welcome to my Website</h1>
      <p>This is a sample page.</p>
      <div class="container">
        <p>This is a nested paragraph.</p>
        <ul>
          <li>Item 1</li>
          <li>Item 2</li>
        </ul>
      </div>
    </body>
    </html>

    プログラムの実行結果として、以下のようなタグ名のリストが表示されることが期待される:

    ["html", "head", "title", "body", "h1", "p", "div", "ul", "li"]

目次