ファイルから指定した行数ずつ読み込む


Ruby でファイルから n 行ずつ読み込む`IO#batch_line(batch_size: n)`みたいなメソッドがあれば良いなと思ったので、それっぽいものを書いてみました。

# きっかけ
バッチ内に、メモリリークを考慮して、ログファイルを2000行ずつ処理している以下のようなコードがありました。

```ruby:process_log.rb
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
```

つ、辛え……。

# 理想
あまりにも辛い。読む気が起きない。
こんな感じで書いて欲しい。書けて欲しい。

```ruby:process_log.rb
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`というメソッドを持ったオブジェクトでラップしても良い気がします。

```ruby:process_log.rb
file = TextFilePager.new("./log.txt", skip_header: true)
file.batch_line(batch_size: 2000) do |lines|
  result = process(lines)
  output(result) 
end
```

どちらも辛くない。

# 早速書いてみる
ファイルをオブジェクトでラップする方を書いてみました。

```ruby:text_file_pager.rb
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
```

- [necojackarc/text_file_pager.rb - Gist](https://gist.github.com/necojackarc/3e5603d344e963db84c6)

# 使い方
先ほど書いたコードがまんま動きます。
試しに動く例を書いてみます。

まずは適当なテキストファイルを準備します。

```txt:sample.txt
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](https://en.wikipedia.org/wiki/Lorem_ipsum) です。ピリオドごとに改行しており、行頭に行番号を降っています。

ここで誰かに「行番号を外して3センテンスごとの改行に直して欲しい」と頼まれたとします。その処理に`process`という曖昧過ぎてコードレビューで殺される名前をつけて実装してみます。

```ruby:process_lorem_ipsum.rb
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`を使っているだけです。

出力を確認してみます。

```txt:processed_sample.txt
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↑

# 追記
素敵なコメントを元に、つたない英語で自分のブログにまとめ直してみました。

- [How to read a text file in batches with Ruby](http://necojackarc.hateblo.jp/)