25 Jun 2010

Ruby 正規表現入門ドリル

Rubyの正規表現の入門編です。これを読めばリファレンスにあまり当たらなくても思い描いている正規表現を大体書けるようにというのと、他の人が書いた正規表現を読めるようになることを目的に書きました。そのため正規表現の論理構造とあまり関係ないものは割愛しました。Rubyのバージョンは得に断りがない限り1.8.7を使用しています。1.9から鬼車が採用されており、グループの扱いに機能拡張があります。そちらについても最後の方で触れています。習うより慣れろということで最後に練習問題を用意しています。

目次




基本


Regexpインスタンスの生成
>> Regexp.new("a")

//はRegexpのインスタンス。
>> /Suzuki Milan Paak/.class
=> Regexp

%記法もRegexpのインスタンス
>> %r|a| == /a/
=> true


- マッチした位置を見つける
Stringの=~もRegexpの=~も返す結果は同じ
>> /i/ =~ "aeiou"
=> 2
>> "aeiou" =~ /i/
=> 2

位置は0からカウントされる
>> "aeiou" =~ /a/
=> 0

一致しない場合はnil
>> "aeiou" =~ /s/
=> nil

$&には最初にマッチした文字列が代入される。$&はマッチが行われる度に上書きされる
>> "aei aeio" =~ /aei./
=> 0
>> $&
=> "aei "
>> "aeio" =~ /aei./
=> 0
>> $&
=> "aeio"

$` 一致した文字列の前の文字列
$& 正規表現と一致した文字列
$' 一致した文字列の後の文字列
$~ 一致情報を記憶したオブジェクト
>> "abc" =~ /b/
=> 1
>> [$`, $&, $', $~]
=> ["a", "b", "c", #<matchdata "b">]

- マッチした文字列を全て取得する
>> /(ba)/.match("foobarbaz").to_a
=> ["ba", "ba"] #=> barとbazのbaが帰っている
>> "foobarbaz".match(/(ba)/).to_a
=> ["ba", "ba"]


マッチがあるかどうかをbooleanで返す
>> /Tokyo/ === "He's from Tokyo."
=> true


リテラル



iを指定すると大文字/小文字を無視する
>> "Ruby" =~ /by/i
=> 2

xを指定すると、正規表現の空白と改行を無視する
>> "foobarbaz" =~ /bar
baz/
=> nil
>> "foobarbaz" =~ /bar
baz/x
=> 3

検索対象の文字列の改行は関係ない。
>> "foobar
baz" =~ /barbaz/
=> nil
>> "foobar
baz" =~ /barbaz/x
=> nil


mを指定すると複数行モードになる。 .が改行を含む任意の一文字と一致する。
>> "foobar
baz" =~ /bar.baz/m
=> 3

| 選択。OR
>> /^--help$|^-h$/ =~ "--help"
=> 0
>> /^--help$|^-h$/ =~ "-help"
=> nil
>> /^--help$|^-h$/ =~ "-h"
=> 0
>> /^--help$|^-h$/ =~ "--h"
=> nil


正規表現記号(メタ文字)



^が行頭
$が行末
>> ["ruby", "by"].collect{|v| v =~ /^by$/}
=> [nil, 0]

. 任意の一文字
>> "abc".scan /.c/
=> ["bc"]


量指定子(quantifiers)



? 直前の表現の0 または 1 回の繰り返し
>> /ユーザー?/ =~ "ユーザログイン"
=> 0

* 直前の表現の0回以上の繰り返し。
>> "aeiou".scan /o*u/
=> ["ou"]
>> "aeiou".scan /i*u/ #=> iの0回の繰り返し + "u" にマッチ
=> ["u"]


+ 直前の表現の1回以上の繰り返し
>> "aeiu".scan /o*u/
=> ["u"]
>> "aeiu".scan /o+u/
=> []


{m}, {m,}, {m, n}
それぞれ直前の正規表現の{m 回}、{m 回以上}、{m 回以上、最大 n 回}
str = "foofoofoo"
p str[/(foo){1}/] # => "foo"
p str[/(foo){2,}/] # => "foofoofoo"
p str[/(foo){1,2}/] # => "foofoo"


quantifire?(??, *?, +?, {m}?, {m,}?, {m, n}?)
quantifireだけの指定はできるだけ長くマッチしようとする。quantifire?はできるだけ短くマッチしようとする。

p str[/(foo){2,}?/] # => "foofoo"

orig_str = "<b>Ruby</b> and <b>Perl</b>"
p orig_str.gsub(/<b>(.*)<\/b>/, '<em>\1</em>')
# => "<em>Ruby</b> and <b>Perl</em>"
p orig_str.gsub(/<b>(.*?)<\/b>/, '<em>\1</em>')
# => "<em>Ruby</em> and <em>Perl</em>"



その他のバックスラッシュ付きのメタ文字 "\x" については割愛します。Rubyリファレンスの正規表現記号のセクションをご参照下さい。


グループ


グループは何文字かの正規表現をまとめる時に使用します。量指定子や選択と一緒に使用します。

u + eの一回以上の繰り返しにマッチ
>> "queue".scan /ue+/
=> ["ue", "ue"]

ueの一回以上の繰り返しにマッチ
>> "queue".scan /(ue)+/
=> [["ue"]]

>> /(ue)+/ =~ "queue"
=> 1
>> $&
=> "ueue"

aまたはbc
>> /a|bc/ =~ "abc"
=> 0
>> $&
=> "a"

acまたはbc
>> /(a|b)c/ =~ "abc"
=> 1
>> $&
=> "bc"


後方参照



グループ化の()にはもう一つ役割があります。それはそのグループが一致した文字列を記憶しておくことです。記憶した文字列はそれ以降の正規表現から参照可能です。これを後方参照と呼びます。後方参照は\nで指定します。nは正規表現中のn番目の()とのマッチの結果を意味します

>> "abc abcd " =~ /(\w+)\s+\1d/
=> 0
>> $&
=> "abc abcd"

(\w+)は英数字が0回以上出現することを意味します。つまり"abc"とマッチします。\1は最初の()と一致した"abc"を指します。\sは空白文字の0回以上の出現を意味します。よって全体で"abc abcd"にマッチします。この場合、/(\w+)\s+\1d/は/(\w+)\s+abcd/と等価です。

$nは\nと同じようにn番目の()とマッチした表現を参照できます。
>> "suzukimilanpaak@example.com " =~ /(\w+)@(\w+).com/
=> 0
>> p $1, $2, $3
"suzukimilanpaak"
"example"
nil

- ()の入れ子
最初に出現した(、つまり外側の()から番号が付く様です。
>> /((foo)bar)/ =~ "foobar"
=> 0
>> p $1, $2
"foobar"
"foo"


文字クラス



[]は[]中のどれか一文字とマッチします。
>> /[abc]/ =~ "b"
=> 0

[]内の^は先頭にある場合、後続の文字以外のもとマッチし、それ以外の場合文字列"^"とマッチします。
p /[^bc]/ =~ "bca" #<= 2 "^"とマッチ >> /[b^c]/ =~ "a^bc"
=> 1
>> $&
=> "^"

>> /[^]/ = "^"
# => invalid regular expression; empty character class: /[^]/

[]内とそうでない場合の違い
[]内の場合後続文字を否定し、そうでない場合行頭にマッチします
p /[^bc]/ =~ "bca" #<= 2 p /^bc/ =~ "abc" #<= nil p /^bc/ =~ "bca" #<= 0 []内の-は文字列として評価されます。 >> /[-bc]/ =~ "a-bc"
=> 1
>> $&
=> "-"



バックトラック



以下の正規表現はマッチします。
>> /(go*)ogle/ === 'google'
=> true
>> $1
=> "go"

これは正規表現のエンジンが以下の手順を踏んでいるからです。

1,
- o* が o 2つにマッチする
- ogleの o がマッチに失敗
2,
- o* を o 1つにマッチさせる (バックトラックする)
- ogleの o が 残りのo にマッチする
- gle が 残りにマッチする

(?>)を使うとバックトラックを抑止できます。"この表現はまだ試験実装中です。将来なくなる可能性もありますので、 そのつもりで使ってください。特に汎用ライブラリなどで使ってはいけません。"と本家マニュアルにありました。
>> /(?>go*)ogle/ === 'google'
=> false


Regexpオブジェクト、MatchDataオブジェクト


>> re = Regexp.new(/(\w+)\s+\1d/)
=> /(\w+)\s+\1d/
>> m = re.match("abc abcd ")
=> #
>> m[0]
=> "abc abcd"
>> m[1]
=> "abc"
>> m[2]
=> nil


\は""中でエスケープシーケンスなので、""で囲んでRegexpのインスタンスを作成する場合二重にエスケープしなければ\wや\sなどの正規表現記号を使えない。
>> re = Regexp.new("\w")
=> w
>> re =~ "aaa"
=> nil

>> re = Regexp.new("\\w")
=> w
>> re =~ "aaa"
=> 0

>> re = Regexp.new('\w')
=> w
>> re =~ "aaa"
=> 0


間違いの元なので、私の場合最初から正規表現のインスタンスを使うことにしてる。
>> /\w+/.match "aaa "
=> #


to_aは一致した文字列を配列にして返す。values_atはその配列に対してインデクスで指定された値を返す。
>> m = /(.)(.)(\d+)(\d)/.match("THX1138: The Movie")
=> #
>>m.to_a
=> ["HX1138", "H", "X", "113", "8"]
>> m.captures
=> ["H", "X", "113", "8"]
>>m.values_at(0, 2, -2)
=> ["HX1138", "X", "113"]

to_aの結果は["正規表現全体にマッチする文字列", "グループ1に一致する文字列", "グループ2..", "グループ3..", "グループ4.."]

MatchData#capturesとString#scanはグループが指定されていた場合、それぞれのグループにマッチする文字列を返す模様
>> "THX1138: The Movie".scan /\d+/
=> ["1138"]
>> "THX1138: The Movie".scan /(.)(.)(\d+)(\d)/
=> [["H", "X", "113", "8"]]



グループ(2)



名前付きグループ


1.9では正規表現エンジンに鬼車が採用されました。そのためグループに名前を付けられるようになりました。(?<group_mane>regexp)という形式で各グループに名前を付けられます。MatchDataのインスタンスからhハッシュキーと同じようにm[:group_name]で参照できます。

ドメイン名から第二レベルドメインを取得してみましょう。
$ rvm 1.9.1
$ irb
ruby-1.9.1-p378 > m = /(?<third_level>w+).(?<second_level>w+).(?<top_level>w+)/.match "engineerfiles.blogspot.com"
=> #<MatchData "engineerfiles.blogspot.com" third_level:"engineerfiles" second_level:"blogspot" top_level:"com">
ruby-1.9.1-p378 > m[:second_level]
=> "blogspot"

\k<group_name>で後方参照できます。
ruby-1.9.1-p378 > /(?<foo>bar)\k<foo>/.match("barbarbar")
=> #<MatchData "barbar" foo:"bar">

名前を指定した場合は数値で後方参照できない。
ruby-1.9.1-p378 > /(?<foo>bar)\1/.match("barbarbar")
SyntaxError: (irb):10: numbered backref/call is not allowed. (use name): /(?<foo>bar)\1/
from /home/suzukimilanpaak/.rvm/rubies/ruby-1.9.1-p378/bin/irb:17:in `<main>'


グループの位置指定、先読み(lookahead)


(?=)はマッチした文字列を記憶しない(幅がない)。
ruby-1.9.1-p378 > /(foo)\s(bar)\s(?=baz)/.match "foo bar baz"
=> #

以下の例はマッチしません。
ruby-1.9.1-p378 > /(foo)\s(?=bar)\s(?=baz)/.match "foo bar baz"
=> nil
(?=)は対象の文字列からマッチングの位置を進めないからだそうです。

(?=bar)の後に(bar)を指定するとそれにマッチする文字列を(?=bar)の位置から再度検索してくれます。
ruby-1.9.1-p378 > /(foo)\s(?=bar)(bar)\s(?=baz)/.match "foo bar baz"
=> #
いまいち使いどころがわかんのだけど...どなたか良い例がありましたらぜひ教えてください。

000 を除く 3 桁の数字
re = /(?!000)\d\d\d/
p re =~ "000" # => nil
p re =~ "012" # => 0
p re =~ "123" # => 0



日本語の取扱い



文字コードの指定



- 組み込み変数で指定
Rubyのコード中に$KCODEを指定する。正規表現以外だけでなく、全てのRubyがマルチバイトコードを認識するためのエンコードを指定します。

最初の1バイトしか意味がなく、しかも大文字小文字を区別しない。そのため以下の指定は全てUTF-8を意味する。
$KCODE = "UTF-8"
$KCODE = "U"
$KCODE = "u"

選択肢は "EUC", "SJIS", "UTF8", "NONE"-ASCII(Default)

影響範囲
インタプリタの字句解析器、Regexp のエンコーディングフラグのデフォルト値、upcase、downcase、swapcase、capitalize、inspect、split、gsub、scan

- Ruby実行時のオプションで指定
$ echo puts $KCODE > /tmp/envvar.rb
$ ruby -Ks environment_variables_sample.rb
SJIS


- 正規表現オブジェクト作成時に指定
正規表現を使用するタイミングだけ文字コードを別のものに指定する場合は以下の様なリテラルで設定が可能です。
/日本語できます/s
この場合/日本語できます/という表現をShift-JISとしてマッチを探します。


Regexp#new, compile(newのSynonym)の引数で指定することもできます。
第一引数:正規表現、第二引数:ケースセンシティブ、第三引数:文字コード
>> re = Regexp.new("cat", true, "s")
=> catis
>> p re
/cat/is


エンコード指定による違い



- . の解釈の違い
以下の内容のファイルをUTF-8で作成して実行する。
$ cat /tmp/re_enc.rb
puts /つのだ.ひろ/u.match "つのだ☆ひろ" #=> つのだ☆ひろ
puts /つのだ.ひろ/n.match "つのだ☆ひろ" #=> nil
puts /つのだ...ひろ/n.match "つのだ☆ひろ" #=> つのだ☆ひろ
エンコードにNONEが指定されている場合、.は1バイトにマッチします。最後の行はUTF-8が3バイトのためマッチします。エンコードにnone以外が指定されている場合、. は全角文字の1文字にマッチします。


- ucase
以下の内容のファイルをShift-JISで作成して実行する。
$ cat /tmp/re_enc.rb
puts "スズキミランパーク".upcase
$ ruby -Ks /tmp/re_enc.rb
スズキミランパーク
スズキミランパーク
$ ruby -Kn /tmp/re_enc.rb
ベペネミランパーハ
スズキミランケーク

- trは日本語に対応していない
以下の内容のファイルをShift-JISで作成して実行する。
$ cat /tmp/re_enc.rb
puts "スズキミランパーク".tr("ミランパーク", " ミラノ パーク")
$ ruby -Kn /tmp/re_enc.rb
々〆´<<<� パー



練習問題


習うより慣れろということで、練習問題をいくつか用意しました。まだ少ないですが、ちょっとずつ足していこうと思います。


1. 以下の与えられたコードと出力結果からSOME_PROCESSに書かれているコードを作成して下さい。
if m = /(\d{2})\s(\w{3})\s(\d{4})/.match("22 Jun 2010")
SOME_PROCESS
end
puts d, m_s, y  #=> ["22", "Jun", "2010"]


2. lsの結果から カレントディレクトリ ".", 直上のディレクトリ "..", 隠し属性ファイル/ディレクトリ ".filename" を取り除く
/var/www/yourfav$ irb
>> dirs_files = (`ls -a`).collect{|v| v.gsub("\n", "")}
=> [".", "..", "app", "Capfile", "config", "coverage", "db", "doc", "features", ".git", "lib", "log", "public", "Rakefile", "README", "rerun.txt", "script", "spec", "tmp", "vendor"]

dirs_filesから".", "..", 隠し属性ファイル/ディレクトリを除いた配列を取得して下さい。

3. HTMLファイルからtitleを抜き出す。
正規表現(1) - Ruby 練習問題集 - Ruby on Rails with OIAX
"カレントディレクトリに HTML ファイル test.html があります。
このファイルから title 要素の内容を抜き出して表示する Ruby プログラムを書きなさい。
test.html は HTML 文法に沿っており、必ず 1 個の title 要素を含むと仮定して構いません。
なお、タグの名前にアルファベットの大文字と小文字が使える点、および要素の内容に改行文字が含まれる可能性がある点に注意すること。"

test.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> 
<head> 
<title>
正規表現(1) - Ruby 練習問題集 - Ruby on Rails with OIAX</title> 
</head> 
<body> 
</body> 
</html>   

解答