以下のようなリストから、組織の名前に「東北」と「大学」が含まれている項目を抽出してくださいというタスクを与えられたらどうすれば良い?テキストエディタ(Mousepadなど)やブラウザでCtrl+fを押すと完全一致の検索(検索している文字列と完全に一致する文字列を抽出する検索)を利用できる。しかし、リストには「東北」が名前に含まれていない大学もあるし、「東北」を含む大学以外の組織の名前も載っているので、一回の完全一致検索では必要な情報だけに絞り込むことができない。
氏名 電話番号 組織
酒井忠勝 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以上の任意の文字を意味している)
実際に正規表現で検索する練習をしてみよう。
まずは上記のデータ(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では、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 東北文教大学
プログラムの内容を一行ずつ解説しよう。
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 ループは終了する。
if /東北.*大学/ =~ line
このifで、読み込んだ行が正規表現にマッチしているかの判定を行なう。「正規表現にマッチしているか?」を意味する条件式は
正規表現 =~ 文字列
のように書く。マッチしている場合は、マッチした部分の文字列の中の位置を整数として返す。マッチしない場合は nil を返し、条件不成立となる。
正規表現の後ろの =~ は、「正規表現がマッチすれば…」という比較演算子である。通常1行ずつ入力した文字列をこの右辺に書く。ちなみに「マッチしなければ」を意味する演算子は !~ である。
=~ の右辺に指定した「文字列」が正規表現の比較対象となる文字列である。
print line
line 変数にはデータファイルから読み込んだ行が入っているので、それを出力する。もちろん、一つ上の行に if があるので、正規表現にマッチした場合だけ該当行が出力される。
end
ifに対応するend
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個でもよいことを意味する(つまり、saito と sato のどちらにもマッチする)。さらに例えば、正規表現 sa[ei]?ki は、直前のパターンが [ei] なので、「e か i」の文字が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] なので、「e か i」の文字が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 とすると、大文字と小文字を区別せずに検索するようになる( SAITOH や Saitoh にもマッチする)。これを正規表現オプションという。以下はオプションのうち覚えておくべきものを紹介する。
i - 大文字小文字を区別しない
m - 文字列中に改行が含まれていても . でマッチするようになる
正規表現が文字列にマッチした場合、対象の文字列の中のマッチした部分文字列を、特別な変数 $& によって取り出すことができる。このように正規表現にマッチした部分文字列を後から参照することは後方参照(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=getsif /(\S+)\s+(\d+)円\s+(\d+)/ =~ lineproduct = $1price = $2.to_iquantity = $3.to_itotal = price * quantityprintf("%sの総価格: %d\n", product, total)endend
結果:
sime{c11xxxx}% ruby total_value.rb products.txt
パンの総価格: 9900
おにぎりの総価格: 1800
コーラの総合格: 31800
ボールペンの総価格: 23880
解説:
line という変数に保管する。if文の条件が成立せず何も表示されない。この正規表現パターンでは丸括弧を使って3つのグループを指定している。1つ目は空白文字ではない1つ以上の文字(\S+)、つまり商品名にマッチする(実際にマッチした部分文字列は特別な変数 $1 で抽出できる)。2つ目は「円」の直前にある1つ以上の数字(\d+)、つまり価格にマッチする(実際にマッチした部分文字列は特別な変数 $2 で抽出できる)。3つ目は在庫数にマッチする(実際にマッチした部分文字列は特別な変数 $3 で抽出できる)。また、3つのグループの間では1つ以上の空白文字(\s+)が期待されている。(\S+) にマッチした部分文字列(商品名)を product 変数に代入する。(\d+) にマッチした部分文字列(価格)を整数に変換し price 変数に代入する。(\d+) にマッチした部分文字列(在庫数)を整数に変換し quantity 変数に代入する。最初に紹介したRubyでの正規表現指定方法は、パターンを / / で括るというものだった。それ以外にもいくつか方法がある。
プログラム中で正規表現を指定するには以下のどれかを使う。
/パターン/
もっとも簡単な指定方法。既に利用した。
%r,パターン,
%r の直後の文字でパターンを挟む。上記の例では , (カンマ) だが、括弧で括ったり、括弧以外の任意の文字で挟むことができる。%r(パターン) でも %r|パターン| でも %r!パターン! でもよい。ただし、挟むための右側の文字がパターン中に現れてはいけない。ここでは使わないが、すこし複雑な正規表現を指定するときにはどうしても必要。たとえば、/ をたくさん含む文字列を検索したいときはパターンの区切り文字に / 以外を利用できるこの表記が便利で、日付けらしき文字列を探すときに
%r:\d\d\d\d/\d\d?/\d\d?:
などとすると「4桁の数値/1、2桁の数値/1、2桁の数値」にマッチするパターンが書ける。%r を使わず / /
で括ると、
/\d\d\d\d\/\d\d?\/\d\d?/
となり、スラッシュの境目が分かりづらい。
Regexp.new(パターン文字列)
文字列を正規表現に変換する。プログラム中で
pattern = "sai?toh"
/sai?toh/ に変換したいときは、以下のように書く。
regexp = Regexp.new(pattern)
プログラム作成時ではなく、実行したときに検索パターンを読み込みそれを正規表現に変換するのであれば、Regexp.new が必要である。
大文字小文字を区別して欲しくない場合は、第2引数に true を指定する。
regexp = Regexp.new(pattern, true)
とすると /sai?toh/i と同じことになる。
正規表現は、思った通りマッチしない問題などにはまりやすい機能である。Emacsとktermを行ったり来たりしながら何度も試すのは困難である。そんな時に短いRubyのコードを簡単に試せる機能として、irb (Interactive Ruby) がある。
irbを起動するためには、ktermで irb というコマンドを実行する。すると下記のようなプロンプトが表示され、ここに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 を作成せよ。
学籍番号 科目 成績
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
ソースコード:
...
実行結果:
...
発展課題
与えられた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"]