Rubyにおける正規表現を確認する為には、以下のサイトと記事を参考にさせて頂きました。
参考記事
正規表現に触れてみる
電話番号を抽出する
12-3456-7890
\d
は半角数字を抽出するメタ文字。これを活用する事で、上記の電話番号を以下のように表現できる。
/\d\d-\d\d\d\d-\d\d\d\d/
電話番号のパターンを増やす
12-3456-7890
この電話番号以外の数字の羅列も抽出できるようにする。例えば、以下のような数字群。
03-1234-5678
090-1234-5678
0795-12-3456
04992-1-2345
これら全てを抽出するためには、まず抽出したいパターンを見出す。
今回は「"数字2~5個"、ハイフン、"数字1~4個"、ハイフン、"数字4個"」
文字の個数を限定するときは
{n,m}
や{n}
というメタ文字を使う
上にメタ文字は量指定子と言う。
{n,m}
:「直前の文字が n 個以上、m 個以下」
{n}
:「直前の文字がちょうど n 文字」
よってこれらの知識を追加する事で、以下のように表現できる。
\d{2,5}-\d{1,4}-\d{4}
電話番号の繋ぎを「-ハイフンまたは(かっこ」にする
次は以下のパターンに対応できるようにする。
#これまで
03-1234-5678
090-1234-5678
0795-12-3456
04992-1-2345
#今回
03-1234-5678
090(1234)5678
0795-12-3456
04992(1)2345
このパターンを言語化すると、
「"数字2~5個"、ハイフンまたはかっこ開き、"数字1~4個"、ハイフンまたはかっこ閉じ、"数字4個"」
となります。
この際に活用するメタ文字は、**「AまたはB」**のパターンです。[]
これで表現します。
具体例をメタ文字で表現すると、以下のようになります。
[ABC]
:AまたはBまたはC
[-(]
:ハイフンまたはかっこ開き
これらの知識を追加すると、以下のように表現できます。
\d{2,5}[-(]\d{1,4}[-)]\d{4}
03-1234-5678
090(1234)5678
0795-12-3456
04992(1)2345
[]内でのハイフン"-"の扱いの注意
[]内で扱うハイフンは、配置する場所によって意味合いが異なります。
例えば以下のような場合には、抽出する範囲を指定する事になります。
[A-Z]
:AまたはB...またはZ
[0-9]
:0または1...または9
[A-Za-z0-9]
:大文字アルファベットまたは小文字アルファベットまたは半角数字1文字 = 半角英数字1文字
微妙な違いを許容しつつ置換する
今回の例題では、参考記事と同様に「クープ バゲット」を取り扱わせて頂きます。
正確な表現は、「クープ、半角スペース、バケット」です。これに対する微妙な違いを処理していきます。
AまたはB
前回の知識を参考に、以下の例にマッチさせます。
クープ バケット
クープ バケット
クープ・バケット
クープ バゲット
これらのパターンを言語化すると、
「クープ、半角スペースまたは全角スペースまたは・、バ、ケまたはゲ、ット」になります。
これを抽出する表現は以下の通りです。
/クープ[ ・]バ[ケゲ]ット/
クープ バケット
クープ バケット
クープ・バケット
クープ バゲット
AまたはB、もしくは無し
次に以下の例にマッチさせます。
クープ バケット
クープ バケット
クープ・バケット
クープ バゲット
クープバケット #追加、区切り文字なし
この場合を言語化すると、区切り文字が
「半角スペースまたは全角スペースまたは・もしくは無し」になります。
この際のメタ文字が、?
と言う量指定子です。これを導入すると以下のようになります。
/クープ[ ・]?バ[ケゲ]ット/
クープ バケット
クープ バケット
クープ・バケット
クープ バゲット
クープバケット
任意の一文字
区切り文字をこれまでは、[ ・]で表現してきた。しかし今回の例では、これらを任意の一文字と言う扱いで十分だと仮定する。その場合は、任意の一文字を表すメタ文字である.
を扱う。
/クープ.?バ[ケゲ]ット/
クープ バケット
クープ バケット
クープ・バケット
クープ バゲット
クープバケット
ただこの場合は「クープ*バケット」「クープ@バケット」とかも反応してしまう、けどね。
表現に該当する文章を抽出する
text = <<-TEXT
クープバゲットのパンは美味しかった。
今日はクープ バゲットさんに行きました。
クープ バゲットのパンは最高。
ジャムおじさんのパン、ジャムが入ってた。
また行きたいです。クープ・バゲット。
クープ・バケットのパン、売り切れだった(><)
TEXT
text.split(/\n/).grep(/クープ.?バ[ゲケ]ット/)
# => ["クープバゲットのパンは美味しかった。", "今日はクープ バゲットさんに行きました。", "クープ バゲットのパンは最高。", "また行きたいです。クープ・バゲット。", "クープ・バケットのパン、売り切れだった(><)"]
改行を表すメタ文字である\n
でひあドキュメントを分割し配列にする(text.split(/\n/)
)。
その配列の中から正規表現(/クープ.?バ[ゲケ]ット/
)に合致する要素のみの配列を作成する(grep
)。
正規表現を用いて置換する
今回の例ではHTMLの構文をcsvの形式に置換することを考える。以下が具体例
# 変化前
<select name="game_console">
<option value="none"></option>
<option value="wii_u" selected>Wii U</option>
<option value="ps4">プレステ4</option>
<option value="gb">ゲームボーイ</option>
</select>
# 変化後
<select name="game_console">
none,
wii_u,Wii U
ps4,プレステ4
gb,ゲームボーイ
</select>
この置換の方法を習得する。
それぞれに合致する正規表現を考える
以下のhtmlから、valueと表示テキストそれぞれに合致する正規表現を考える。
<option value="wii_u">Wii U</option>
<option value="ps4">プレステ4</option>
<option value="gb">ゲームボーイ</option>
valueの場合
「半角英数字またはアンダーバー1文字以上」 → [a-z0-9_]+
表示テキストの場合
「>, 任意の文字1文字以上, <」 → `>.+<`
このようになる。
これらの正規表現を踏まえて、今回の置換の場合には以下のような手順で行う。
- 行全体にマッチする正規表現を作る
- valueと表示テキストの部分をそれぞれ
( )
で囲んでキャプチャする - キャプチャを利用して新しい文字列を組み立てる
1. 行全体にマッチする正規表現を作る
前項の正規表現を参考に、以下のように表現できる。
注意点としては、スラッシュからスラッシュまでが正規表現である為、抽出したい範囲内にあるスラッシュ(</option>
のスラッシュなど)については、バックスラッシュでエスケープする必要がある。
/<option value="[a-z0-9_]+">.+<\/option>/
2. valueと表示テキストの部分をそれぞれ ( )
で囲んでキャプチャする
正規表現に ( ) を使うと、その部分がキャプチャ(捕捉)され、連番が付けられるのです。
/<option value="([a-z0-9_]+)">(.+)<\/option>/
3. キャプチャを利用して新しい文字列を組み立てる
vscodeの場合、search機能を使って、
find:<option value="([a-z0-9_]+)">(.+)<\/option>
replace:$1, $2
と$1
や$2
を利用する事で、キャプチャされている連番を扱った置き換えが可能となる。
任意の文字0文字以上で抽出
以下の正規表現の場合、表示文字がない場合は抽出できない(.+
, 任意の文字1文字以上)。
なので、表示文字が存在しない場合にも対応できるよう、0文字以上に変更する。
その場合には、+
ではなく*
を扱う。
# 表示文字1文字以上
/<option value="([a-z0-9_]+)">(.+)<\/option>/
# 表示文字0文字以上
/<option value="([a-z0-9_]+)">(.*)<\/option>/
<option value="none"></option> # これも抽出できるようになる。
" selected"があり、または無し ← 任意の文字列があり、または無し
次に下記の場合を抽出したい。この場合は" selected"があり、または無しの文字列にマッチする必要がある。
<option value="wii_u" selected>Wii U</option>
A?
:Aまたは無し ← この場合は1文字に対応。
今回の場合は2文字以上の羅列に対応する必要がある為、文字列をグループ化する必要がある。
( selected)?
:文字列" selected"があり、または無し
よって、以下のような正規表現になる。
/<option value="([a-z0-9_]+)"( selected)?>(.*)<\/option>/
<option value="wii_u" selected>Wii U</option>
キャプションはなし
しかし前項で確認した通り、()
はキャプション機能を表す。なので今回のように、文字列をグループ化したいだけ、つまりキャプションしたくない場合は、以下のように記載する必要がある。
(:?文字列)?
:文字列があり、または無し。そしてキャプションはしない。
/<option value="([a-z0-9_]+)"(:? selected)?>(.*)<\/option>/
<option value="wii_u" selected>Wii U</option>
[a-z0-9]を置き換える
[0-9]
:0から9のうち1文字 → 任意の半角数字1文字 = \d
→ [a-z\d]
また上記の正規表現は\w
に置換可能。
\w
は「英単語を構成する文字」の意味
RubyやJavaScriptでは「\w=[a-zA-Z0-9_](半角英数字とアンダースコア1文字) 」という仕様になっています。
これより、以下のように変換可能。
/<option value="([a-z0-9_]+)"(:? selected)?>(.*)<\/option>/
/<option value="([\w]+)"(:? selected)?>(.*)<\/option>/
Rubyで置換してみる
vscodeではなく、Rubyのコード内で文字列置換を実行する
Rubyのサンプルコードでは、キャプチャされた文字列を $1
ではなく \1
で参照する。
html = <<-HTML
<select name="game_console">
<option value="none"></option>
<option value="wii_u" selected>Wii U</option>
<option value="ps4">プレステ4</option>
<option value="gb">ゲームボーイ</option>
</select>
HTML
replaced = html.gsub(/<option value="(\w+)"(?: selected)?>(.*)<\/option>/, '\1,\2')
puts replaced
# <select name="game_console">
# none,
# wii_u,Wii U
# ps4,プレステ4
# gb,ゲームボーイ
# </select>
gsub(pattern, replace) -> String
文字列中で pattern にマッチする部分全てを文字列 replace で置き換えた文字列を生成して返します。
まとめ
以下参考記事のまとめの部分をそのまま引用させてもらいます。
? は「直前の文字が1個、または無し」を表す
. は「任意の1文字」を表す
- は「直前の文字が1個以上」を表す
- は「直前の文字が0個以上」を表す
( ) はマッチする部分をキャプチャ(捕捉)する
キャプチャした部分は置換するときに $1 や \1 で参照できる
\w は「英単語を構成する文字(半角英数字とアンダースコア)」を表す
[^AB] は「AでもなくBでもない任意の1文字」を表す
正規表現中の特別な文字は \ でエスケープする
( ) はキャプチャだけでなく、グループ化にも使われる
(ABC)? は「文字列 ABC があり、または無し」を表す
(?: ) はキャプチャ無しでグループ化する場合に使う - と + は「貪欲」で最長マッチを返すため、使い方を誤ると思いがけない結果が返る
*? や +? にすると、最短マッチを返す
空白を自在に扱おう
こちらの記事で学べる内容
^、$、\s、\t、\n の意味
| を使ったOR検索
環境によって異なる改行コードと正規表現の関係
使われる場所によって異なる ^ の意味
こちらの章は、主に空白文字と「位置」を表すメタ文字であるアンカーを中心に扱っている。
こちらの記事のまとめとしては、登場をした正規表現を言語化する事にする。
+
:スペースが1文字以上を続く
^ +
:行頭からスペースが1文字以上続く
^
これは、位置を示すメタ文字であり、アンカーと言う。
$
では、「行末」を示す。
^ +$
:行頭から行末までスペースが1文字以上続く
^[ \t]+$
:行頭から行末まで、スペースまたはタブが、1文字以上続く
:[ \t]*
:コロンの後ろにスペースまたはタブ文字が0文字以上続く(:はメタ文字ではなく、ただのコロン
\s
:空白文字全般、(:[ \t]*
= :\s*
)
*注意 - \s
について
1. 言語や環境によって異なる
Rubyの場合、\s
= [ \t\r\n\f]
2. 改行文字や復帰文字も含まれる
\f
:改ページ
\r
:リターン
^.+heroku\/(api|scheduler).+\n
:
「行頭からの何らかの文字が1文字以上続き(^.+
)、
"heroku/"が現れ(heroku\/
)、
"api" または "scheduler" が続き((api|scheduler)
)、
その後何らかの文字が1文字以上続いて(.+
)、
改行文字で終わる(\n
)」
まとめ
正規表現はとても便利であると感じました。また正規表現には、アンカーや量指定子など、複数の種類があることを知りました。
これまで呪文のように見えていた正規表現をいくらか理解できるようになりました。
恐らくrubocopなどは、この正規表現の鬼なのだな、と予想しています。