💎

【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

注意点とベストプラクティス

注意点

  1. 貪欲マッチと非貪欲マッチ
# 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のみ)
  1. 特殊文字のエスケープ
# 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
  1. アンカーの使用
# 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

ベストプラクティス

  1. 正規表現を定数化する
# 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
  1. 複雑な正規表現はコメントを付ける
# 拡張モードを使用(/x オプション)
EMAIL_REGEX = /
  \A
  [\w+\-.]+     # ローカル部(ユーザー名)
  @
  [a-z\d\-]+    # ドメイン名
  (\.[a-z\d\-]+)*  # サブドメイン
  \.
  [a-z]+        # トップレベルドメイン
  \z
/ix  # i=大文字小文字を区別しない, x=拡張モード
  1. 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