💎
【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