(8) 06/05の授業内容:正規表現の活用

正規表現のパターンをキーボードから入力

正規表現のパターンは//でくくって示した。

/パターン/

前回の課題では特定のパターンにマッチする行を検索するために、毎回パターンを書き直して保存をし、ktermで実行をするという手順をとった。このようにプログラム中に直接パターンを記入する方法は、様々なパターンを試してみる場合には使い勝手が悪い。ここでは、キーボードから正規表現のパターンを入力し、そのパターンにマッチする行を表示するという方法について考えてみよう。このためには、もう1つの正規表現のパターンの指定方法を把握する必要がある。

reg= Regexp.new("[AB]C+")

これは[AB]C+を正規表現のパターンとし、このパターンをregという変数に代入するという表記方法である。実際にプログラムを作成して確認してみよう(reg.rb)。

#!/usr/koeki/bin/ruby

reg = Regexp.new("[AB]C+")
p reg

これを実行すると以下の通りとなる。

irsv{naoya}% ruby reg.rb
/[AB]C+/

pメソッドは変数内の値を表示するメソッドである。配列内の値の格納状況を調べるために使用したが、配列以外の変数にも利用することができる。プログラムを実行するとpメソッドが変数regの中身を表示し、/[AB]C+/という結果を返す。

つまりRegexp.new("パターン")とすることで、()内は正規表現のパターンとなる。上記のプログラムでは("")内に[AB]C+という具体的なパターンを指定しているが、この部分は変数にすることができる。つまり、キーボードからパターンを入力して適当な変数に代入しておき、Regexp.new("")で("")内にその変数を指定すれば、キーボードから入力された正規表現のパターンで検索をすることが可能になるわけである。この考えに基づき、前回のプログラムを変更してみよう。変更箇所を黄色で示す。

#!/usr/koeki/bin/ruby

STDERR.print "検索パターンは?:"
reg = STDIN.gets.chomp!
expression = Regexp.new(reg, true, "e")

while line=gets
  if expression =~ line
    print line
  end
end

続いてプログラムの変更点について解説する。

reg = STDIN.gets.chomp!

キーボードから入力された値を読み込み、改行を取り除いてregという変数に代入する。文字列のままでよいのでto_iをつけて整数には変換していない。この行ではgetsのまえにSTDINという見かけないものがついているが、これは標準入力という意味を持つ。前回のプログラムは実行する際に

irsv{naoya}% ruby kensaku.rb meibo.txt

というように、検索対象となるファイルを指定した。そして、getsメソッドはファイル内を1行ずつ読み込むという作業を行っていた。今回のプログラムでは、検索をするデータは外部ファイルから読み込み、検索パターンはキーボードから読み込む。つまり、データを読み込む元が2種類あることになる。プログラムを実行する際にはgetsメソッドがどこからデータを読み込むのかを指定しないとコンピュータはどうすればよいかわからず困ってしまう(プログラムが意図したとおりに動かなくなるので、実際に困るのはコンピュータではなくプログラマ)。これを解決するために以下の方法が採用される。

  • データの読み込み元がキーボードのみ:gets
  • データの読み込み元がファイルのみで、ファイルはプログラム実行時に指定:gets
  • データの読み込み元がキーボードとファイルで、ファイルはプログラム実行時に指定:ファイルはgets、キーボードはSTDIN.gets

標準的には入力元はキーボードとなるため、ファイル、キーボードの双方からgetsメソッドで値を読み込む必要がある場合は、キーボードから読み込む場合に、標準入力であることを示すSTDINをつけSTDIN.getsと標記する。

この行の1つ上に、STDERR.print "検索パターンは?:"という行があり、ここでもSTDERRという見慣れないものがついている。これもSTDINの場合と考え方は同じで、printが""内を表示する先はディスプレイ以外に、ファイルを指定することができる。今回は指定したファイルの中身を読み込むだけであるが、来週は指定したファイルに結果を出力する方法についても学ぶ。出力先もディスプレイとファイルの2種類になるので、標準的な出力先であるディスプレイに表示する場合はprintではなく、STDERR.printを使用する。なお、今回のプログラムに限定すれば、ファイルに結果を書き出すようには書かれていないため、出力先はディスプレイのみとなる。このためSTDERRはなくてもよい。ただし、不要な場合につけていると間違いということではない。むしろディスプレイに表示することを明示的に示すことができるので、プログラムを書いた本人以外の人がプログラムを読む場合は、書き手の意図が伝わりやすい。

expression = Regexp.new(reg, true, "e")

先ほどは()の中に[AB]C+と直接入力していたが、ここではregという変数になっている。regには1つ上の行でキーボードから入力されたパターンが代入されている。reg以外にtrueや"e"と記載されているが、これらはそれぞれ下記の意味を持つ。

第2引数

true:大文字と小文字を区別してほしくない場合。/パターン/iと同じ意味。

false:大文字と小文字を区別してほしい場合。


第3引数(日本語の検索をしたいときに指定)

"e":EUCコードで書かれているものとして照合。/パターン/eと同じ意味。

"s":Shift-JISコードで書かれているものとして照合。/パターン/sと同じ意味。


101/102教室で英数字を検索する場合

Regexp.new(変数, true)

Regexp.new(変数, false)

101/102教室で日本語を検索する場合

Regexp.new(変数, true, "e")

Regexp.new(変数, false, "e")

if expression =~ line

前回のkensaku.rbではexpressionのかわりに/sai?to/iというように具体的なパターンが入力されていた。ここをexpressionという変数にすることで、キーボードから入力した任意のパターンに対するマッチングの有無を調べることができる。

出席課題

以下の5種類の正規表現にマッチする文字列を選択肢から選びなさい。マッチするものが1つもない場合もあるし、複数の文字列がマッチする場合もある。

  1. /National/
  2. /A.+n/
  3. /\s+\S\S\S\S\S\S\S\s+/
  4. /^[^A-G].+tion$/
  5. /k/

解答の選択肢

  1. National Aeronautics and Space Administration
  2. Federal Bureau of Investigation
  3. Central Intelligence Agency
  4. Japan Aerospace Exprolation Agency
  5. National Transportation Safety Board
  6. Federal Aviation Administration
  7. National Highway Traffic Safety Administration
  8. Federal Highway Administration
  9. Federal Emergency Management Agency
  10. 該当なし

制限時間は10分。完成しない場合は、途中まででも構わないので実行し、結果をメールで送ること。出席点は2点。提出要領は下記の通り。

  • 提出先:naoya@e.koeki-u.ac.jp
  • メールのSubject:学籍番号-ruby08
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降に問題1〜5の解答を記載する。時間があれば、各正規表現のパターンについて説明を加えてみる。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:Mewによるメールの送り方はMewコマンドを参照

正規表現を用いたデータマイニング

レジを通して入手されたデータは時系列で保存される。例えば以下のような形で保存されていたとする(regi.txt)。ここで同一時刻に購入されていれば、同じ人が購入したということになる(とする)。

  売上日    時間   商品名     金額  仕入額
2006/06/05  13:12  みかん     298     100
2006/06/05  13:12  豆腐        78      20
2006/06/05  13:12  卵         198      60
2006/06/05  13:12  キャベツ   100      70
2006/06/05  13:12  さんま      58      10
2006/06/05  13:12  大根       128      45
2006/06/05  13:15  砂糖       128      60
2006/06/05  13:15  生姜       100      30
2006/06/05  13:18  せんべい   258      80
2006/06/05  13:18  おにぎり   100      38
2006/06/05  13:18  から揚げ   300     120
2006/06/05  13:18  コロッケ   198      50
2006/06/05  13:23  牛乳       158      55
2006/06/05  13:23  ニンジン   128      40
2006/06/05  13:23  玉ねぎ     198      65
2006/06/05  13:23  はたはた   178      30
2006/06/05  13:23  ごま油     158      80
2006/06/05  13:23  トマト缶   100      25

売り上げの時系列データに基づいて、何が売れやすいのか、何時頃に売れやすいのか、ペアで買われやすいものは何か等、経営戦略を練る上で有用な情報を引き出すことをデータマイニングという。現時点では様々な分析をするのは難しいので、今回はデータマイニングもどきを実践してみよう。

具体的には、売り上げ額−仕入額を計算して、商品別に利益額を算出し、合計や平均値を計算する(実際には、ここから人件費や光熱費、広告費などを引かなければならないため、利益はさらに少なくなる)。

続いて以下のプログラムをmining.rbとして保存する。

#!/usr/koeki/bin/ruby

#初期設定

name = []     #商品名を代入する配列
price = []    #金額を代入する配列
cost = []     #仕入額を代入する配列

profit = 0    #利益額を代入する配列
n = 0         #配列に使用するインデックス
i = 0         #配列に使用するインデックス

#商品名と金額の読み込みと利益額の計算

while line = gets
     if /\S+\s+\S+\s+(\S+)\s+(\d+)\s+(\d+)/ =~ line
	 name[n] = $1
	 price[n] = $2.to_i
	 cost[n] = $3.to_i
	 profit += $2.to_i - $3.to_i
	 n += 1
     end
end


#個々の利益額の表示


print "- 商品名 ------- 利益 --\n"
print "\n"
while i < n
  printf ("%-10s \t %3d円\n", name[i],price[i]-cost[i])
  i += 1
end

print "-------------------------\n"
printf ("利益(合計)     %5d円\n",profit)
printf ("利益(平均)     %5d円\n",profit/n)

このプログラムを実行すると以下の結果が得られる。実行する際には、読み込むファイル(regi.txt)を指定するのを忘れないようにする。

irsv{naoya}% ruby mining.rb regi.txt
- 商品名 ------- 利益 --

みかん           198円
豆腐              58円
卵               138円
キャベツ          30円
さんま            48円
大根              83円
砂糖              68円
生姜              70円
せんべい         178円
おにぎり          62円
から揚げ         180円
コロッケ         148円
牛乳             103円
ニンジン          88円
玉ねぎ           133円
はたはた         148円
ごま油            78円
トマト缶          75円
-------------------------
利益(合計)      1886円
利益(平均)       104円

プログラムの実行結果を見ると、みかんや豆腐などが表示されている。これらはregi.txt内に記載されていたものであり、どうやらこのプログラムでは読み込んだテキストから必要な部分を取り出しているということがわかる。読み込んだテキストから必要な部分だけを取り出す方法を正規表現の後方参照という。このプログラムはで新しく出てきたのは以下の2行となるが、いずれも後方参照を行う上で把握しておかなければならない記載方法である。

後方参照とは

後方参照は、正規表現でマッチした文字列から特定の部分を取り出す方法である。正規表現のパターンを記述する際に一部を()を使って記述しておくと、()でくくられた部分にマッチした部位を、()の順番に応じて$1、$2のように$数字の形で取り出すことができる。

これを念頭に置きながら新しく出てきた2行について見てみよう。

if /\S+\s+\S+\s+(\S+)\s+(\d+)\s+(\d+)/ =~ line

まずここで使用しているメタ文字について確認しよう。

  • \S:空白以外にマッチ
  • +:1回以上の繰り返しにマッチ
  • \s:空白にマッチ
  • \d:0から9までの数字とマッチ

これらのメタ文字によって表現されるパターンは日本語で表すと、次のように表現できる。

空白以外の文字が何個かあり、空白が何個かあり、空白以外の文字が何個かあり、空白が何個かあり、(空白以外の文字が何個かあり)、空白が何個かあり、(数字が何個かあり)、空白が何個かあり、(数字が何個かある)

ここで、()でくくり赤色で書かれている部分が、上記の正規表現のパターンで()内に書かれているメタ文字に相当する。

そして、この正規表現のパターンはregi.txtに書かれている各行の記載内容にマッチする。()でくくられた3つの部分はそれぞれ黄色で網掛けされた部分に該当する。

2006/06/05 13:23 トマト缶 100 25

name[n] = $1 および price[n] = $2.to_i

正規表現のパターンを記述する際に()を使うと、()内のメタ文字にマッチした部分を$1や$2で取り出すことができる(後方参照)。$1は1つ目の()である(\S+)にマッチする文字列である商品名(トマト缶)、$2は2つ目の()である(\d+)にマッチする文字列である金額(100)、$3は3つ目の()である(\d+)にマッチする文字列である仕入額(25)が代入されている。これをname[n] = $1やprice[n] = $2.to_iの行において、nameやpriceなどの配列変数に代入している。[n]の部分はインデックスで、具体的な値を用いずに初期値をn=0とし、whileの繰り返しの中でn+=1をすることでインデックスを1ずつ増やしながら配列内に順次値を代入している。なお、外部ファイルであるregi.txtはgetsメソッドで1行ずつ読み込んでいるため、このままでは文字列となってしまう。後で計算をするため、ここではprice[n] = $2.to_iというようにto_iメソッドをつけて整数に変換している。

レポート課題

mining.rbを改良する。改良の方法は以下の2種類であり、1番と2番のいずれかを選んで実施する。1番は比較的易しく、2番は驚異的に難しい。このため、1番を選んだ場合は7点満点、2番を選んだ場合は8点満点で採点する。

  1. regi.txtの仕入額の右横に購入数のデータを追加する(値は適当でよい)。これにより利益額の計算は、n個購入した場合、金額×n−仕入額×nとなる(サンプルプログラムは全て1つしか購入していないので金額−仕入額になっている)。利益額を計算したらその後の結果出力はサンプルプログラムと同一とする。
  2. 客単価を計算して結果を表示できるようにする。客単価とは会計1回あたりの購入金額であり、regi.txtでは時刻が同一のものが1回の会計で購入された商品であることをあらわす。結果の出力は以下のようにすること。
- 時刻 ------- 購入額 --

13:12           XXX円
13:15           YYY円
13:18           ZZZ円
  :               :
----------------------
合計           TTTT円
平均            AAA円

今回のプログラムの改良を行うにあたり、1番を選んだ場合は正規表現のパターンを変更する必要がある。変更を忘れるとregi.txtを1行ずつ読み込んでも正規表現にマッチする行が全くなくなってしまうので注意すること。2番を選んだ場合は後方参照すべき場所が異なってくる。時刻が同一の場合に金額を加算していく方法が思いつけば、全く太刀打ちできないというほどのものでもない。

  • 提出先:naoya@e.koeki-u.ac.jp
  • 提出期限:6/11(日)23:59(1限履修者)·6/18(日)23:59(2限履修者)
  • メールのSubject:学籍番号-kadai06
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は下記の構成とする
  1. 作成したプログラム
  2. プログラムの実行結果
  3. プログラムの説明
  4. 感想

  • 採点基準:期限内提出点(2点)、メールの体裁(1点)、プログラム(3点;1番を選んだ場合は2点)、プログラムの説明(2点)
  • プログラムの説明は、今回は変更点の説明のみでよい。1番でも2番でも(1)正規表現のパターン、(2)途中の計算式に必ず手を加えるはずなので、この2点についてサンプルプログラムからどのように改良したのかをわかりやすく説明すること。
  • 説明に関して、文章の意味がわかりづらい場合や、Webページを単にコピー&ペーストしたものは減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点の8点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする

Tips:Mewによるメールの送り方はMewコマンドを参照