Ruby でファイルから n 行ずつ読み込むIO#batch_line(batch_size: n)
みたいなメソッドがあれば良いなと思ったので、それっぽいものを書いてみました。
きっかけ
バッチ内に、メモリリークを考慮して、ログファイルを2000行ずつ処理している以下のようなコードがありました。
File.open("./log.txt") do |file|
result = {}
file.readline # skip header
file.each_line do |line|
key, value = process(line)
result[key] = value
next if result.size < 2000
output(results)
result = {}
end
output(result)
end
つ、辛え……。
理想
あまりにも辛い。読む気が起きない。
こんな感じで書いて欲しい。書けて欲しい。
File.open("./log.txt", skip_header: true) do |file|
file.batch_line(batch_size: 2000) do |lines|
result = process(lines)
output(result)
end
end
テキストファイルをbatch_line
というメソッドを持ったオブジェクトでラップしても良い気がします。
file = TextFilePager.new("./log.txt", skip_header: true)
file.batch_line(batch_size: 2000) do |lines|
result = process(lines)
output(result)
end
どちらも辛くない。
早速書いてみる
ファイルをオブジェクトでラップする方を書いてみました。
class TextFilePager
DEFAULT_BATCH_SIZE = 1000
def initialize(file_path, skip_header: false, delete_line_break: false)
@file_path = file_path
@skip_header = skip_header
@delete_line_break = delete_line_break
end
def batch_line(batch_size: DEFAULT_BATCH_SIZE)
File.open(@file_path) do |file|
file.gets if skip_header?
loop do
line, lines = "", []
batch_size.times do
break if (line = file.gets).nil?
lines << (delete_line_break? ? line.chomp : line)
end
yield lines
break if line.nil?
end
end
end
def skip_header?
@skip_header
end
def delete_line_break?
@delete_line_break
end
end
使い方
先ほど書いたコードがまんま動きます。
試しに動く例を書いてみます。
まずは適当なテキストファイルを準備します。
01 Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
02 Aenean commodo ligula eget dolor.
03 Aenean massa.
04 Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
05 Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
06 Nulla consequat massa quis enim.
07 Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
08 In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo.
09 Nullam dictum felis eu pede mollis pretium.
10 Integer tincidunt.
11 Cras dapibus.
12 Vivamus elementum semper nisi.
13 Aenean vulputate eleifend tellus.
14 Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim.
15 Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
単なる15行の Lorem ipsum です。ピリオドごとに改行しており、行頭に行番号を降っています。
ここで誰かに「行番号を外して3センテンスごとの改行に直して欲しい」と頼まれたとします。その処理にprocess
という曖昧過ぎてコードレビューで殺される名前をつけて実装してみます。
def process(lines)
lines.map do |line|
_, *content = line.split
content.join("\s")
end.join("\s")
end
# Main
sample_text = TextFilePager.new("sample.txt", delete_line_break: true)
sample_text.batch_line(batch_size: 3) do |lines|
puts process(lines)
end
理想として挙げたコードとほぼ一緒です。違いはoutput
の代わりにputs
を使っているだけです。
出力を確認してみます。
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.
Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.
Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi.
Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
問題なく動きました。
感想
ググり力が低いのか、探しても全然見つからなかったので自分で書いてみました。
複数行ずつ読み込むって結構需要がありそうな気もするので、良かったら参考にしてみてください。
IO
やFile
を直接拡張して#batch_line
を生やすのもありかなって思ったんですが、標準クラスの拡張はあまり好きじゃないのでオブジェクトでラップしてみました。
ブロックを使いこなして関数型力あげてきたいです(´∀`∩)↑age↑
追記
素敵なコメントを元に、つたない英語で自分のブログにまとめ直してみました。