[Ruby] ファイルの終わりから行ごとに取得する。

まえがき

次のことを求められた為、テキストファイルを最後から遡って取得するプログラムを書きました。

  • 新しいログから順にキーワードの有無を探したい。
  • 行毎に処理したい。
  • 空白や空行は無視してよい。

これを使う場面として思いつくのは「今日はある処理の完了をログから知ることができるので再度実行する必要がない」くらいです。DBやどこか別の場所に置いけよという話はありますが、そうできない事情もあるのです。

前提

  • 行を最後から最初に向かって扱いたい。
  • 改行や空白がファイルの先頭や最後に存在するかもしれない。
  • 空白や改行のみの行は無視してよい。
  • ほとんどの場合、途中で何らかの条件を満たし処理を打ち切る。

作ったもの

メインのプログラム

こちらになります。お目汚し失礼します。

# coding: utf-8

def reverse_read(file, buf_size:)
  if buf_size < file.size
    file.seek(0, File::SEEK_END)
    file.seek(-buf_size, File::SEEK_CUR)
  else
    buf_size = file.size
  end

  while true
    in_str = file.read(buf_size)
    buf = "#{in_str}#{buf}"

    # prevent inifinite loop with newline char at beginning of file
    return if file.pos - buf_size <= 0 && buf.strip.size == 0

    idx = buf.rindex($/)

    case idx
    when nil
      if file.pos - buf_size <= 0
        yield(buf)
        return
      end
    when 0 # last newline char at buffer[0]
      yield(buf[1..-1]) if buf[1..-1].strip.size > 0
      buf = buf[0]
    else
      yield(buf[idx+1..-1]) unless idx == buf.size - 1  # last newline char NOT at buffer[-1]
      buf = buf[0..idx-1]
    end

    if file.pos - buf_size * 2 > 0
      file.seek(-buf_size * 2, File::SEEK_CUR)
    else
      # change buffer size before last seek
      buf_size = file.pos - buf_size
      file.seek(0, File::SEEK_SET)
    end
  end
end

実行用コード

  • 開くファイル名(in.txt)は実験に応じて随時変更
  • 一度にファイルから読み込む大きさは2^1〜2^10まで変えながら実行
#
# 先述のファイルに追記して使う(横着)
#
log_file = File.open('./in.txt', 'r')

# テスト用コード
(1..10).each do |i|
  cnt = 0
  puts "#{'-'*10} #{2**i}"
  reverse_read(log_file, buf_size: 2**i) do |row|
    puts "[#{cnt}] #{row}"
    cnt += 1
  end
end

実験

実際に動かしてみましょう。

ケース1: 何とも不揃いな汚いファイル

なぜこういうファイルを作るのか理解に苦しみますね。

しかし可能性があるなら対処しなければなりません。それが現実です。

合理性だけで世の中回るならデスマーチは生まれません。

% cat kitanai.txt
 
  a
 c
abcdefg
hijklmn
opqrstu
vwxyz

kll
 d
  e

z

実行結果は次の通り。大丈夫ですね。

---------- 2
[0] z
[1]   e
[2]  d
[3] kll
[4] vwxyz
[5] opqrstu
[6] hijklmn
[7] abcdefg
[8]  c
[9]   a
---------- 4
[0] z
[1]   e
[2]  d
[3] kll
[4] vwxyz
[5] opqrstu
[6] hijklmn
[7] abcdefg
[8]  c
[9]   a
#
# 途中省略
#
---------- 512
[0] z
[1]   e
[2]  d
[3] kll
[4] vwxyz
[5] opqrstu
[6] hijklmn
[7] abcdefg
[8]  c
[9]   a
---------- 1024
[0] z
[1]   e
[2]  d
[3] kll
[4] vwxyz
[5] opqrstu
[6] hijklmn
[7] abcdefg
[8]  c
[9]   a

ケース1: 形式が揃っているログファイル

俗に言うログファイルのようなものです。

こんな綺麗なファイルばかりであれば世の中幸せです。

毎日定時上がり間違いなしです。

% cat kirei.txt 
# Logfile created on 2022-02-23 18:01:02 +0900 by logger.rb/66358
I, [2022-02-23T18:01:02.110708 #34936]  INFO -- : --- [a, 0] sheet: ["a0_0", "a0_1"], db: ["a0_0", "a0_1"]
I, [2022-02-23T18:01:02.110729 #34936]  INFO -- : changes: ["a0_0", "a0_1"] => ["a0_0", "a0_1"]
I, [2022-02-23T18:01:02.110744 #34936]  INFO -- : --- [a, 1] sheet: ["a1_0", "a1_1", "a1_2"], db: ["a1_0", "a1_1"]
I, [2022-02-23T18:01:02.110755 #34936]  INFO -- : create... a, 1 (["a1_2"])
I, [2022-02-23T18:01:02.110765 #34936]  INFO -- : changes: ["a1_0", "a1_1"] => ["a1_0", "a1_1", "a1_2"]
I, [2022-02-23T18:01:02.110774 #34936]  INFO -- : --- [a, 3] sheet: ["a3_0"], db: []
I, [2022-02-23T18:01:02.110784 #34936]  INFO -- : create... a, 3 (["a3_0"])
I, [2022-02-23T18:01:02.110793 #34936]  INFO -- : changes: [] => ["a3_0"]
I, [2022-02-23T18:01:02.110802 #34936]  INFO -- : --- [b, 0] sheet: ["b0_0"], db: ["x0_0"]
I, [2022-02-23T18:01:02.110810 #34936]  INFO -- : update... b, 0 (x0_0 -> b0_0)
I, [2022-02-23T18:01:02.110819 #34936]  INFO -- : changes: ["x0_0"] => ["b0_0"]
I, [2022-02-26T18:01:02.110828 #34936]  INFO -- : --- [b, 1] sheet: ["b1_0", "b1_1"], db: ["x1_0"]
I, [2022-02-26T18:01:02.110835 #34936]  INFO -- : update... b, 1 (x1_0 -> b1_0)
I, [2022-02-26T18:01:02.110842 #34936]  INFO -- : create... b, 1 (["b1_1"])
I, [2022-02-26T18:01:02.110851 #34936]  INFO -- : changes: ["x1_0"] => ["b1_0", "b1_1"]
I, [2022-02-26T18:01:02.110859 #34936]  INFO -- : --- [b, 2] sheet: ["b2_0"], db: ["x2_0"]
I, [2022-02-26T18:01:02.110866 #34936]  INFO -- : update... b, 2 (x2_0 -> b2_0)
I, [2022-02-26T18:01:02.110874 #34936]  INFO -- : changes: ["x2_0"] => ["b2_0"]
E, [2022-02-26T18:01:02.110884 #34936] ERROR -- : human processing required. prod_no: b, times: 3 (sheet (1) < db (3))
I, [2022-02-26T18:01:02.110893 #34936]  INFO -- : --- [c, 4] sheet: ["c4_0", "c4_1", "c4_2", "c4_3"], db: []
I, [2022-02-26T18:01:02.110902 #34936]  INFO -- : create... c, 4 (["c4_0", "c4_1", "c4_2", "c4_3"])
I, [2022-02-26T18:01:02.110910 #34936]  INFO -- : changes: [] => ["c4_0", "c4_1", "c4_2", "c4_3"]

実行結果は次の通り。たぶん大丈夫な筈。

% ruby a.rb                                                                                                                                                
---------- 2                                                                                                                                                                      
[0] I, [2022-02-26T18:01:02.110910 #34936]  INFO -- : changes: [] => ["c4_0", "c4_1", "c4_2", "c4_3"]                                                                             
[1] I, [2022-02-26T18:01:02.110902 #34936]  INFO -- : create... c, 4 (["c4_0", "c4_1", "c4_2", "c4_3"])                                                                           
[2] I, [2022-02-26T18:01:02.110893 #34936]  INFO -- : --- [c, 4] sheet: ["c4_0", "c4_1", "c4_2", "c4_3"], db: []                                                                  
[3] E, [2022-02-26T18:01:02.110884 #34936] ERROR -- : human processing required. prod_no: b, times: 3 (sheet (1) < db (3))                                                        
[4] I, [2022-02-26T18:01:02.110874 #34936]  INFO -- : changes: ["x2_0"] => ["b2_0"]                                                                                               
[5] I, [2022-02-26T18:01:02.110866 #34936]  INFO -- : update... b, 2 (x2_0 -> b2_0)                                                                                               
[6] I, [2022-02-26T18:01:02.110859 #34936]  INFO -- : --- [b, 2] sheet: ["b2_0"], db: ["x2_0"]                                                                                    
[7] I, [2022-02-26T18:01:02.110851 #34936]  INFO -- : changes: ["x1_0"] => ["b1_0", "b1_1"]                                                                                       
[8] I, [2022-02-26T18:01:02.110842 #34936]  INFO -- : create... b, 1 (["b1_1"])                                                                                                   
[9] I, [2022-02-26T18:01:02.110835 #34936]  INFO -- : update... b, 1 (x1_0 -> b1_0)                                                                                               
[10] I, [2022-02-26T18:01:02.110828 #34936]  INFO -- : --- [b, 1] sheet: ["b1_0", "b1_1"], db: ["x1_0"]                                                                           
[11] I, [2022-02-23T18:01:02.110819 #34936]  INFO -- : changes: ["x0_0"] => ["b0_0"]                                                                                              
[12] I, [2022-02-23T18:01:02.110810 #34936]  INFO -- : update... b, 0 (x0_0 -> b0_0)                                                                                              
[13] I, [2022-02-23T18:01:02.110802 #34936]  INFO -- : --- [b, 0] sheet: ["b0_0"], db: ["x0_0"]                                                                                   
[14] I, [2022-02-23T18:01:02.110793 #34936]  INFO -- : changes: [] => ["a3_0"]                                                                                                    
[15] I, [2022-02-23T18:01:02.110784 #34936]  INFO -- : create... a, 3 (["a3_0"])                                                                                                  
[16] I, [2022-02-23T18:01:02.110774 #34936]  INFO -- : --- [a, 3] sheet: ["a3_0"], db: []                                                                                         
[17] I, [2022-02-23T18:01:02.110765 #34936]  INFO -- : changes: ["a1_0", "a1_1"] => ["a1_0", "a1_1", "a1_2"]                                                                      
[18] I, [2022-02-23T18:01:02.110755 #34936]  INFO -- : create... a, 1 (["a1_2"])                                                                                                  
[19] I, [2022-02-23T18:01:02.110744 #34936]  INFO -- : --- [a, 1] sheet: ["a1_0", "a1_1", "a1_2"], db: ["a1_0", "a1_1"]                                                           
[20] I, [2022-02-23T18:01:02.110729 #34936]  INFO -- : changes: ["a0_0", "a0_1"] => ["a0_0", "a0_1"]                                                                              
[21] I, [2022-02-23T18:01:02.110708 #34936]  INFO -- : --- [a, 0] sheet: ["a0_0", "a0_1"], db: ["a0_0", "a0_1"]                                                                   
[22] # Logfile created on 2022-02-23 18:01:02 +0900 by logger.rb/66358
#
# 途中省略
#
---------- 1024
[0] I, [2022-02-26T18:01:02.110910 #34936]  INFO -- : changes: [] => ["c4_0", "c4_1", "c4_2", "c4_3"]
[1] I, [2022-02-26T18:01:02.110902 #34936]  INFO -- : create... c, 4 (["c4_0", "c4_1", "c4_2", "c4_3"])
[2] I, [2022-02-26T18:01:02.110893 #34936]  INFO -- : --- [c, 4] sheet: ["c4_0", "c4_1", "c4_2", "c4_3"], db: []
[3] E, [2022-02-26T18:01:02.110884 #34936] ERROR -- : human processing required. prod_no: b, times: 3 (sheet (1) < db (3))
[4] I, [2022-02-26T18:01:02.110874 #34936]  INFO -- : changes: ["x2_0"] => ["b2_0"]
[5] I, [2022-02-26T18:01:02.110866 #34936]  INFO -- : update... b, 2 (x2_0 -> b2_0)
[6] I, [2022-02-26T18:01:02.110859 #34936]  INFO -- : --- [b, 2] sheet: ["b2_0"], db: ["x2_0"]
[7] I, [2022-02-26T18:01:02.110851 #34936]  INFO -- : changes: ["x1_0"] => ["b1_0", "b1_1"]
[8] I, [2022-02-26T18:01:02.110842 #34936]  INFO -- : create... b, 1 (["b1_1"])
[9] I, [2022-02-26T18:01:02.110835 #34936]  INFO -- : update... b, 1 (x1_0 -> b1_0)
[10] I, [2022-02-26T18:01:02.110828 #34936]  INFO -- : --- [b, 1] sheet: ["b1_0", "b1_1"], db: ["x1_0"]
[11] I, [2022-02-23T18:01:02.110819 #34936]  INFO -- : changes: ["x0_0"] => ["b0_0"]
[12] I, [2022-02-23T18:01:02.110810 #34936]  INFO -- : update... b, 0 (x0_0 -> b0_0)
[13] I, [2022-02-23T18:01:02.110802 #34936]  INFO -- : --- [b, 0] sheet: ["b0_0"], db: ["x0_0"]
[14] I, [2022-02-23T18:01:02.110793 #34936]  INFO -- : changes: [] => ["a3_0"]
[15] I, [2022-02-23T18:01:02.110784 #34936]  INFO -- : create... a, 3 (["a3_0"])
[16] I, [2022-02-23T18:01:02.110774 #34936]  INFO -- : --- [a, 3] sheet: ["a3_0"], db: []
[17] I, [2022-02-23T18:01:02.110765 #34936]  INFO -- : changes: ["a1_0", "a1_1"] => ["a1_0", "a1_1", "a1_2"]
[18] I, [2022-02-23T18:01:02.110755 #34936]  INFO -- : create... a, 1 (["a1_2"])
[19] I, [2022-02-23T18:01:02.110744 #34936]  INFO -- : --- [a, 1] sheet: ["a1_0", "a1_1", "a1_2"], db: ["a1_0", "a1_1"]
[20] I, [2022-02-23T18:01:02.110729 #34936]  INFO -- : changes: ["a0_0", "a0_1"] => ["a0_0", "a0_1"]
[21] I, [2022-02-23T18:01:02.110708 #34936]  INFO -- : --- [a, 0] sheet: ["a0_0", "a0_1"], db: ["a0_0", "a0_1"]
[22] # Logfile created on 2022-02-23 18:01:02 +0900 by logger.rb/66358

なぜ自分で書いたか?

雑に思いついたもの(↓)では絶対ダメだと考えたからです。

# 全行を読み込んで逆順にループさせる
File.readlines('some.txt').reverse.each do |row|
  puts row
end

酷いですね。何の芸もありません。

公式ドキュメントではreadlinesについて次のように書かれています。

データを全て読み込んで、その各行を要素としてもつ配列を返します。

https://docs.ruby-lang.org/ja/latest/method/IO/i/readlines.html

「データを全て読み込んで」と書かれているので可能な限り避けるべきと判断しました。

ということで検索すると次のものが見つかりました。

https://qiita.com/QUANON/items/570c9c75ad0b8dccdc11

最もイメージに近かったです。プログラムを書いた後に気づきました。

「巨大なファイルを」という想定がよいですね!

後ほど勉強させてもらいます。最初に見ればよかったです。

まとめ

  • 最初に検索するのは大事
  • 目的のものがあったとしてもコピペはNG。自分の頭で考えるのは重要
  • そもそもメソッドとしてありそうでないのが不思議

業務時間ではないけど楽しく頭を使えたので満足しました。

この記事を書いた人 Wrote this article

ぜんたろう

ぜんたろう

FP2級/宅建士。お金の話が好物。インデックス投資がメインなのに個別株・ETFにも手を出す。ここ数年で投資スタイルが確立した筈だがジャンク株に心を奪われがち。 --- 永遠の見習いプログラマ (SIer複数→スタートアップ複数→大きめベンチャー)

TOP