💎
【Ruby 10日目】基本文法 - 正規表現の基本
はじめに
Rubyの正規表現について、基本的な使い方からパターンマッチングまで詳しく解説します。
正規表現は文字列の検索、置換、バリデーションなど、様々な場面で使用される強力な機能です。Ruby 3.4では正規表現のパフォーマンスがさらに改善されています。
基本概念
正規表現(Regular Expression, Regex)は、文字列のパターンを表現するための記法です。
Rubyでは以下の方法で正規表現を作成できます:
-
/パターン/- スラッシュで囲む(最も一般的) -
%r{パターン}- %記法 -
Regexp.new('パターン')- Regexpクラスを使用
基本的な使い方
マッチング演算子
# =~ 演算子:マッチした位置を返す
text = "Hello, Ruby!"
puts text =~ /Ruby/ #=> 7(マッチした位置)
puts text =~ /Python/ #=> nil(マッチしない)
# match メソッド:MatchDataオブジェクトを返す
result = text.match(/Ruby/)
puts result[0] #=> "Ruby"
# match? メソッド:true/falseを返す(Ruby 2.4+)
puts text.match?(/Ruby/) #=> true
puts text.match?(/Python/) #=> false
基本的なパターン
# 文字クラス
puts "abc123" =~ /[0-9]/ #=> 3(数字が位置3でマッチ)
puts "abc123" =~ /[a-z]/ #=> 0(小文字が位置0でマッチ)
puts "abc123" =~ /[A-Z]/ #=> nil(大文字はマッチしない)
# 特殊文字
text = "test@example.com"
puts text =~ /\w+@\w+\.\w+/ #=> 0(位置0からマッチ)
# \d - 数字 [0-9]
# \w - 英数字とアンダースコア [a-zA-Z0-9_]
# \s - 空白文字
puts "price: 100" =~ /\d+/ #=> 7("100"が位置7でマッチ)
# 量指定子(すべて位置0でマッチ)
puts "aaa" =~ /a+/ #=> 0(1回以上)
puts "aaa" =~ /a*/ #=> 0(0回以上)
puts "aaa" =~ /a?/ #=> 0(0回または1回)
puts "aaa" =~ /a{3}/ #=> 0(ちょうど3回)
puts "aaa" =~ /a{2,4}/ #=> 0(2〜4回)
よくあるユースケース
ケース1: メールアドレスのバリデーション
実務でよく使われるメールアドレスの検証パターンです。
def valid_email?(email)
# シンプルなメールアドレスパターン
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
puts valid_email?("user@example.com") #=> true
puts valid_email?("invalid.email") #=> false
puts valid_email?("user+tag@test.co.jp") #=> true
# より厳密なパターン
EMAIL_REGEX = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
def strict_email_validation(email)
email.match?(EMAIL_REGEX)
end
ケース2: 電話番号の抽出
文字列から電話番号を抽出する例です。
text = "連絡先: 03-1234-5678 または 090-9876-5432"
# scan メソッドで全てマッチ
phone_numbers = text.scan(/\d{2,4}-\d{4}-\d{4}/)
puts phone_numbers.inspect #=> ["03-1234-5678", "090-9876-5432"]
# より柔軟なパターン
def extract_phone_numbers(text)
# ハイフンあり・なし両方に対応
text.scan(/\d{2,4}-?\d{4}-?\d{4}/)
end
text2 = "電話: 0312345678 または 09098765432"
puts extract_phone_numbers(text2).inspect
ケース3: URLの抽出とマッチング
text = "詳細はhttps://example.com/path?query=1を参照してください"
# URLを抽出
url = text[/https?:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+/]
puts url #=> "https://example.com/path?query=1"
# 複数のURLを抽出
text2 = "リンク: https://example.com と http://test.org"
urls = text2.scan(/https?:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+/)
puts urls.inspect #=> ["https://example.com", "http://test.org"]
# URLの各部分を取得
url = "https://example.com:8080/path/to/page?query=value"
if match = url.match(%r{^(https?)://([^:/]+)(?::(\d+))?(/.*)?$})
protocol = match[1] #=> "https"
host = match[2] #=> "example.com"
port = match[3] #=> "8080"
path = match[4] #=> "/path/to/page?query=value"
end
ケース4: 文字列の置換
# sub - 最初のマッチを置換
text = "Hello, Hello, Hello"
puts text.sub(/Hello/, "Hi") #=> "Hi, Hello, Hello"
# gsub - 全てのマッチを置換
puts text.gsub(/Hello/, "Hi") #=> "Hi, Hi, Hi"
# 後方参照を使った置換
text = "2024-10-17"
puts text.gsub(/(\d{4})-(\d{2})-(\d{2})/, '\3/\2/\1') #=> "17/10/2024"
# ブロックを使った置換
text = "price: 100, total: 200"
result = text.gsub(/\d+/) { |num| num.to_i * 2 }
puts result #=> "price: 200, total: 400"
# 大文字小文字を変換
text = "hello world"
puts text.gsub(/\b\w/) { |c| c.upcase } #=> "Hello World"(各単語の先頭を大文字に)
puts text.gsub(/\w+/) { |word| word.capitalize } #=> "Hello World"(各単語を capitalize)
ケース5: データの抽出とパース
# ログファイルからデータを抽出
log = "[2024-10-17 10:30:45] ERROR: Connection failed"
if match = log.match(/\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\] (\w+): (.+)/)
date = match[1] #=> "2024-10-17"
time = match[2] #=> "10:30:45"
level = match[3] #=> "ERROR"
message = match[4] #=> "Connection failed"
puts "#{level} at #{date} #{time}: #{message}"
end
# 名前付きキャプチャを使用(より読みやすい)
if match = log.match(/\[(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2})\] (?<level>\w+): (?<message>.+)/)
puts "Date: #{match[:date]}"
puts "Time: #{match[:time]}"
puts "Level: #{match[:level]}"
puts "Message: #{match[:message]}"
end
注意点とベストプラクティス
注意点
- 貪欲マッチと非貪欲マッチ
# BAD: 貪欲マッチ(デフォルト)
text = "<div>content</div><div>more</div>"
puts text[/<div>.*<\/div>/] #=> "<div>content</div><div>more</div>"(全体がマッチ)
# GOOD: 非貪欲マッチ(?を追加)
puts text[/<div>.*?<\/div>/] #=> "<div>content</div>"(最初のdivのみ)
- 特殊文字のエスケープ
# BAD: 特殊文字をエスケープしない
text = "price: $100"
puts text =~ /$100/ #=> エラーまたは予期しない結果
# GOOD: 特殊文字をエスケープ
puts text =~ /\$100/ #=> 7
# または Regexp.escape を使用
price = "$100"
pattern = Regexp.new(Regexp.escape(price))
puts text =~ pattern #=> 7
- アンカーの使用
# BAD: 部分一致
def validate_number?(text)
text.match?(/\d+/)
end
puts validate_number?("abc123def") #=> true(意図しない結果)
# GOOD: 完全一致
def validate_number?(text)
text.match?(/\A\d+\z/) # \A=文字列の先頭, \z=文字列の末尾
end
puts validate_number?("abc123def") #=> false
puts validate_number?("123") #=> true
ベストプラクティス
- 正規表現を定数化する
# GOOD: 定数として定義
EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
PHONE_REGEX = /\A\d{2,4}-\d{4}-\d{4}\z/
def valid_email?(email)
email.match?(EMAIL_REGEX)
end
def valid_phone?(phone)
phone.match?(PHONE_REGEX)
end
- 複雑な正規表現はコメントを付ける
# 拡張モードを使用(/x オプション)
EMAIL_REGEX = /
\A
[\w+\-.]+ # ローカル部(ユーザー名)
@
[a-z\d\-]+ # ドメイン名
(\.[a-z\d\-]+)* # サブドメイン
\.
[a-z]+ # トップレベルドメイン
\z
/ix # i=大文字小文字を区別しない, x=拡張モード
- match? を使ってパフォーマンス向上
# match? は MatchData オブジェクトを作らないので高速
# BAD: キャプチャが不要なのに match を使用
if text.match(/pattern/)
# ...
end
# GOOD: キャプチャが不要なら match? を使用
if text.match?(/pattern/)
# ...
end
Ruby 3.4での改善点
- Prismパーサーによる正規表現の解析が高速化
- 正規表現のコンパイルキャッシュが改善され、繰り返し使用時のパフォーマンスが向上
- 不正な正規表現のエラーメッセージがより詳細でわかりやすくなりました
# Ruby 3.4では不正な正規表現のエラーメッセージが改善
begin
# 不正な正規表現(閉じ括弧がない)
/(?<name>\w+/.match("test")
rescue RegexpError => e
puts e.message # より詳細なエラー情報が表示される
end
# パフォーマンスの改善例
pattern = /\d+/
1000.times do
"test 123 test".match?(pattern) # キャッシュにより高速化
end
デバッグとテスト
# 正規表現のテスト
def test_regex
pattern = /\A\d{3}-\d{4}\z/
# 正しい形式
puts pattern.match?("123-4567") #=> true
# 誤った形式
puts pattern.match?("12-4567") #=> false
puts pattern.match?("123-456") #=> false
puts pattern.match?("abc-defg") #=> false
end
# Rubular.com などのオンラインツールで正規表現をテストするのも有効
まとめ
この記事では、Rubyの正規表現の基本について以下の内容を学びました:
- 正規表現の基本的な書き方とマッチング
- 文字クラス、量指定子、特殊文字の使い方
- 実践的なユースケース(メールアドレス、電話番号、URL、ログのパース)
- 文字列の置換とキャプチャ
- 貪欲マッチと非貪欲マッチの違い
- アンカーを使った完全一致
- パフォーマンスを考慮したベストプラクティス
正規表現は強力なツールですが、複雑になりすぎると可読性が下がります。適度にシンプルに保ち、必要に応じてコメントを付けることを心がけましょう。
Discussion