💎

【Ruby 12日目】基本文法 - 文字列操作の基礎

に公開

はじめに

Rubyの文字列操作について、基本的な使い方から実践的なテクニックまで解説します。

Rubyの文字列は非常に強力で、多くの便利なメソッドが用意されています。Ruby 3.4では文字列処理のパフォーマンスがさらに向上しています。

基本概念

Rubyの文字列には以下の特徴があります:

  • シングルクォート '...' とダブルクォート "..." の2種類
  • ダブルクォートでは式展開と特殊文字が使える
  • 文字列は可変(mutable)だが、frozen化も可能
  • エンコーディングを持つ(デフォルトはUTF-8)

文字列の基本

文字列の作成

# シングルクォート(式展開なし)
str1 = 'Hello, Ruby!'
puts str1  #=> Hello, Ruby!

# ダブルクォート(式展開あり)
name = "Alice"
str2 = "Hello, #{name}!"
puts str2  #=> Hello, Alice!

# ヒアドキュメント
text = <<~TEXT
  複数行の
  文字列を
  書けます
TEXT
puts text.chomp
#=> 複数行の
#=> 文字列を
#=> 書けます

# %記法
str3 = %q(シングルクォートと同じ)
str4 = %Q(ダブルクォートと同じ: #{name})
str5 = %(ダブルクォートと同じ: #{name})

puts str3  #=> シングルクォートと同じ
puts str4  #=> ダブルクォートと同じ: Alice

文字列の結合

# + 演算子
str1 = "Hello"
str2 = "World"
result = str1 + " " + str2
puts result  #=> Hello World

# << 演算子(破壊的)
str = +"Hello"  # Ruby 3.4では+プレフィックスで可変文字列を作成
str << " World"
puts str  #=> Hello World

# concat メソッド(破壊的)
str = +"Hello"
str.concat(" Ruby")
puts str  #=> Hello Ruby

# 配列のjoin
words = ["Ruby", "is", "awesome"]
sentence = words.join(" ")
puts sentence  #=> Ruby is awesome

# 式展開を使う方が効率的
name = "Ruby"
version = "3.4"
text = "#{name} #{version}"
puts text  #=> Ruby 3.4

文字列の検索と置換

検索

text = "Ruby is a beautiful programming language"

# include? - 部分文字列を含むか
puts text.include?("beautiful")  #=> true
puts text.include?("Python")     #=> false

# start_with? / end_with?
puts text.start_with?("Ruby")      #=> true
puts text.end_with?("language")    #=> true

# index / rindex - 位置を返す
puts text.index("beautiful")   #=> 10(最初の出現位置)
puts text.index("Python")      #=> nil(見つからない)
puts text.rindex("a")          #=> 37(最後の出現位置)

# scan - マッチするすべてを配列で返す
words = text.scan(/\w+/)
puts words.inspect  #=> ["Ruby", "is", "a", "beautiful", "programming", "language"]

# count - 文字の出現回数
puts text.count("a")     #=> 4
puts text.count("aeiou") #=> 11(母音の数)

置換

text = "I love Ruby. Ruby is great!"

# sub - 最初の1つを置換
result = text.sub("Ruby", "Python")
puts result  #=> I love Python. Ruby is great!

# gsub - すべてを置換
result = text.gsub("Ruby", "Python")
puts result  #=> I love Python. Python is great!

# 破壊的な置換(元の文字列を変更)
text_copy = +text  # 可変な複製を作成
text_copy.sub!("Ruby", "Python")
puts text_copy  #=> I love Python. Ruby is great!

# ブロックを使った置換
text = "price: 100, tax: 200"
result = text.gsub(/\d+/) { |num| num.to_i * 2 }
puts result  #=> price: 200, tax: 400

# 複数のパターンを置換
text = "apple, banana, cherry"
replacements = { "apple" => "りんご", "banana" => "バナナ", "cherry" => "さくらんぼ" }
result = text.gsub(/\w+/) { |word| replacements[word] || word }
puts result  #=> りんご, バナナ, さくらんぼ

# delete - 文字を削除
text = "Hello, World!"
puts text.delete("o")      #=> Hell, Wrld!
puts text.delete("aeiou")  #=> Hll, Wrld!(母音を削除)

# tr - 文字を変換
puts "hello".tr("el", "ip")  #=> hippo
puts "hello".tr("a-z", "A-Z")  #=> HELLO(大文字化)

文字列の分割と抽出

分割

# split - 文字列を分割
text = "Ruby,Python,JavaScript"
languages = text.split(",")
puts languages.inspect  #=> ["Ruby", "Python", "JavaScript"]

# 空白文字で分割
text = "one  two   three"
words = text.split
puts words.inspect  #=> ["one", "two", "three"]

# 正規表現で分割
text = "one,two;three:four"
parts = text.split(/[,;:]/)
puts parts.inspect  #=> ["one", "two", "three", "four"]

# 分割数を制限
text = "a,b,c,d,e"
parts = text.split(",", 3)
puts parts.inspect  #=> ["a", "b", "c,d,e"]

# lines - 行ごとに分割
text = "line1\nline2\nline3"
lines = text.lines
puts lines.inspect  #=> ["line1\n", "line2\n", "line3"]

# chars - 文字ごとに分割
text = "Ruby"
chars = text.chars
puts chars.inspect  #=> ["R", "u", "b", "y"]

抽出

text = "Hello, Ruby!"

# スライス([]演算子)
puts text[0]      #=> H(最初の文字)
puts text[-1]     #=> !(最後の文字)
puts text[0, 5]   #=> Hello(開始位置、長さ)
puts text[0..4]   #=> Hello(範囲指定)
puts text[7..-1]  #=> Ruby!(7文字目から最後まで)

# slice メソッド
puts text.slice(0, 5)   #=> Hello
puts text.slice(7..-1)  #=> Ruby!

# 正規表現でマッチした部分を抽出
email = "user@example.com"
username = email[/[^@]+/]
domain = email[/@(.+)/, 1]
puts username  #=> user
puts domain    #=> example.com

# partition - 区切り文字で3つに分割
text = "key:value"
before, sep, after = text.partition(":")
puts before  #=> key
puts sep     #=> :
puts after   #=> value

# rpartition - 右から検索
text = "path/to/file.txt"
dir, sep, file = text.rpartition("/")
puts dir   #=> path/to
puts file  #=> file.txt

文字列の変換

大文字・小文字の変換

text = "Hello, Ruby!"

# 大文字化
puts text.upcase     #=> HELLO, RUBY!

# 小文字化
puts text.downcase   #=> hello, ruby!

# 大文字⇔小文字を反転
puts text.swapcase   #=> hELLO, rUBY!

# 先頭文字のみ大文字化
puts "hello".capitalize  #=> Hello

# 各単語の先頭を大文字化
words = "hello ruby world"
result = words.split.map(&:capitalize).join(" ")
puts result  #=> Hello Ruby World

トリミング(空白の削除)

text = "  hello  "

# 両端の空白を削除
puts text.strip      #=> hello

# 左端の空白を削除
puts text.lstrip     #=> "hello  "

# 右端の空白を削除
puts text.rstrip     #=> "  hello"

# 特定の文字を削除
text = "***hello***"
puts text.delete("*")  #=> hello

# chomp - 末尾の改行を削除
text = "hello\n"
puts text.chomp      #=> hello

text = "hello\r\n"
puts text.chomp      #=> hello

# chop - 末尾の1文字を削除
text = "hello"
puts text.chop       #=> hell

パディング(埋め込み)

# ljust - 左寄せ
puts "Ruby".ljust(10)         #=> "Ruby      "
puts "Ruby".ljust(10, "*")    #=> Ruby******

# rjust - 右寄せ
puts "Ruby".rjust(10)         #=> "      Ruby"
puts "Ruby".rjust(10, "*")    #=> ******Ruby

# center - 中央寄せ
puts "Ruby".center(10)        #=> "   Ruby   "
puts "Ruby".center(10, "*")   #=> ***Ruby***

# ゼロパディング
number = "42"
puts number.rjust(5, "0")     #=> 00042

よくあるユースケース

ケース1: CSVデータのパース

# CSV行の解析
csv_line = "John,30,Tokyo"
name, age, city = csv_line.split(",")
puts "名前: #{name}, 年齢: #{age}, 都市: #{city}"
#=> 名前: John, 年齢: 30, 都市: Tokyo

# クォート付きCSV(簡易版)
csv_line = '"John Smith","30","Tokyo, Japan"'
fields = csv_line.scan(/"([^"]*)"/).flatten
puts fields.inspect  #=> ["John Smith", "30", "Tokyo, Japan"]

# 複数行のCSV処理
csv_data = <<~CSV
  Name,Age,City
  Alice,25,Tokyo
  Bob,30,Osaka
  Charlie,35,Kyoto
CSV

lines = csv_data.lines.map(&:chomp)
headers = lines.first.split(",")
rows = lines[1..-1].map { |line| line.split(",") }

rows.each do |row|
  person = headers.zip(row).to_h
  puts person.inspect
end
#=> {"Name"=>"Alice", "Age"=>"25", "City"=>"Tokyo"}
#=> {"Name"=>"Bob", "Age"=>"30", "City"=>"Osaka"}
#=> {"Name"=>"Charlie", "Age"=>"35", "City"=>"Kyoto"}

ケース2: URLの解析と構築

# URLからパラメータを抽出
url = "https://example.com/search?q=ruby&page=1&sort=desc"

# プロトコル、ホスト、パスを分離
protocol = url[/^https?/]
host = url[/\/\/([^\/]+)/, 1]
path = url[/\/\/[^\/]+(\/?[^?]*)/, 1]
query_string = url[/\?(.+)/, 1]

puts "Protocol: #{protocol}"  #=> Protocol: https
puts "Host: #{host}"          #=> Host: example.com
puts "Path: #{path}"          #=> Path: /search
puts "Query: #{query_string}" #=> Query: q=ruby&page=1&sort=desc

# クエリパラメータをハッシュに変換
params = query_string.split("&").map { |param| param.split("=") }.to_h
puts params.inspect  #=> {"q"=>"ruby", "page"=>"1", "sort"=>"desc"}

# URLの構築
base_url = "https://example.com/search"
params = { q: "ruby", page: 2, sort: "asc" }
query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
full_url = "#{base_url}?#{query_string}"
puts full_url  #=> https://example.com/search?q=ruby&page=2&sort=asc

ケース3: テンプレートエンジン(簡易版)

# プレースホルダーを置換
template = "Hello, {{name}}! You have {{count}} messages."
data = { "name" => "Alice", "count" => "5" }

result = template.gsub(/\{\{(\w+)\}\}/) do |match|
  key = $1
  data[key] || match
end

puts result  #=> Hello, Alice! You have 5 messages.

# より複雑なテンプレート
class SimpleTemplate
  def initialize(template)
    @template = template
  end

  def render(data)
    result = +@template  # 可変な複製を作成
    data.each do |key, value|
      result.gsub!("{{#{key}}}", value.to_s)
    end
    result
  end
end

template = SimpleTemplate.new("Name: {{name}}, Age: {{age}}, City: {{city}}")
output = template.render({ name: "Bob", age: 30, city: "Tokyo" })
puts output  #=> Name: Bob, Age: 30, City: Tokyo

ケース4: マークダウンの簡易パーサー

# Markdownの見出しをHTMLに変換
markdown = <<~MD
  # タイトル1
  ## タイトル2
  ### タイトル3

  通常のテキスト
MD

html = markdown.lines.map do |line|
  line = line.strip
  case line
  when /^### (.+)/
    "<h3>#{$1}</h3>"
  when /^## (.+)/
    "<h2>#{$1}</h2>"
  when /^# (.+)/
    "<h1>#{$1}</h1>"
  when ""
    ""
  else
    "<p>#{line}</p>"
  end
end.join("\n")

puts html
#=> <h1>タイトル1</h1>
#=> <h2>タイトル2</h2>
#=> <h3>タイトル3</h3>
#=>
#=> <p>通常のテキスト</p>

# 太字と斜体の変換
text = "これは**太字**で、これは*斜体*です"
result = text.gsub(/\*\*(.+?)\*\*/, '<strong>\1</strong>')
              .gsub(/\*(.+?)\*/, '<em>\1</em>')
puts result
#=> これは<strong>太字</strong>で、これは<em>斜体</em>です

ケース5: ログファイルの解析

# ログの解析とフィルタリング
log_lines = [
  "[2024-10-19 10:30:00] INFO: Application started",
  "[2024-10-19 10:30:05] DEBUG: Loading configuration",
  "[2024-10-19 10:30:10] ERROR: Connection failed",
  "[2024-10-19 10:30:15] INFO: Retrying connection",
  "[2024-10-19 10:30:20] ERROR: Max retries exceeded"
]

# ERRORログのみ抽出
error_logs = log_lines.select { |line| line.include?("ERROR") }
puts "=== エラーログ ==="
error_logs.each { |log| puts log }
#=> [2024-10-19 10:30:10] ERROR: Connection failed
#=> [2024-10-19 10:30:20] ERROR: Max retries exceeded

# ログをパースして構造化
parsed_logs = log_lines.map do |line|
  match = line.match(/\[(?<datetime>.*?)\] (?<level>\w+): (?<message>.+)/)
  if match
    {
      datetime: match[:datetime],
      level: match[:level],
      message: match[:message]
    }
  end
end.compact

# レベルごとに集計
log_counts = parsed_logs.group_by { |log| log[:level] }
                       .transform_values(&:count)
puts "\n=== ログレベル別集計 ==="
log_counts.each { |level, count| puts "#{level}: #{count}件" }
#=> INFO: 2件
#=> DEBUG: 1件
#=> ERROR: 2件

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

注意点1: 文字列の破壊的変更

# BAD: 破壊的メソッドによる副作用
def process_text(text)
  text.upcase!  # 元の文字列を変更してしまう
  text
end

original = +"hello"  # 可変文字列
result = process_text(original)
puts original  #=> HELLO(変更されている)

# GOOD: 新しい文字列を返す
def process_text(text)
  text.upcase  # 新しい文字列を返す
end

original = "hello"
result = process_text(original)
puts original  #=> hello(変更されない)
puts result    #=> HELLO

注意点2: 文字列結合のパフォーマンス

# BAD: + 演算子での結合(遅い)
result = ""
100.times { |i| result = result + i.to_s }

# GOOD: << 演算子での結合(高速)
result = +""
100.times { |i| result << i.to_s }

# BEST: 配列のjoin(最も高速)
result = []
100.times { |i| result << i.to_s }
puts result.join

注意点3: エンコーディング

# エンコーディングを確認
text = "こんにちは"
puts text.encoding  #=> UTF-8

# エンコーディングを変換
sjis_text = text.encode("Shift_JIS")
puts sjis_text.encoding  #=> Shift_JIS

# UTF-8に戻す
utf8_text = sjis_text.encode("UTF-8")
puts utf8_text  #=> こんにちは

# 不正なバイト列の処理
binary_data = "\xFF\xFE".force_encoding("UTF-8")
puts binary_data.valid_encoding?  #=> false

# エラーを無視して変換
safe_text = binary_data.encode("UTF-8", invalid: :replace, undef: :replace)
puts safe_text.valid_encoding?  #=> true

ベストプラクティス1: frozen_string_literalの理解

# Ruby 3.4では文字列リテラルがデフォルトでfrozen
str = "hello"
puts str.frozen?  #=> true

# 可変な文字列が必要な場合は + プレフィックス
str = +"hello"
puts str.frozen?  #=> false
str << " world"
puts str  #=> hello world

# frozen文字列に対する操作はエラー
# str = "hello"
# str << " world"  #=> FrozenError

ベストプラクティス2: 式展開の活用

name = "Ruby"
version = 3.4

# BAD: 連結
message = "Welcome to " + name + " " + version.to_s + "!"

# GOOD: 式展開
message = "Welcome to #{name} #{version}!"
puts message  #=> Welcome to Ruby 3.4!

# 式展開では任意の式を評価できる
numbers = [1, 2, 3]
puts "Sum: #{numbers.sum}"  #=> Sum: 6

Ruby 3.4での改善点

Ruby 3.4では以下の改善が行われています:

  • frozen_string_literal: true がデフォルト動作に
  • 文字列処理のパフォーマンスが全般的に向上
  • String#+ のメモリ効率が改善
# frozen文字列リテラル
str1 = "hello"
puts str1.frozen?  #=> true

# 可変文字列が必要な場合
str2 = +"hello"
puts str2.frozen?  #=> false

# 文字列の複製
str3 = str1.dup
puts str3.frozen?  #=> false

まとめ

この記事では、Rubyの文字列操作の基礎について以下の内容を学びました:

  • 文字列の作成と結合の方法
  • 検索、置換、分割、抽出の各種メソッド
  • 大文字・小文字変換、トリミング、パディング
  • 実践的なユースケース(CSV、URL、テンプレート、マークダウン、ログ解析)
  • パフォーマンスを考慮したベストプラクティス
  • Ruby 3.4での文字列の扱い方(frozen_string_literal)

文字列操作はプログラミングの基本です。適切なメソッドを選び、パフォーマンスとコードの可読性を両立させることを心がけましょう。

参考資料

Discussion