Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

ログ管理 | LTSV | LTSVの簡易パーサーを作成してみた(高速化版)

概要

LTSVの簡易パーサーを作成してみた(高速化版)

詳細

下記の記事で作成したパーサーは低速だったため、高速化しました。
ログ管理 | LTSV | LTSVの簡易パーサーを作成してみた(高速化版)
http://d.hatena.ne.jp/tbpg/20130714/1373819084

根本的な作りを変更し、パース処理の実態はシェルのegrepコマンドになっています。
検索条件に合わせてegrepコマンドを生成してRubyからシェルを実行しています。

サンプルで作成した10MBのファイルに対して検索を行ったところ、
元処理では9秒かかっていたところが、改善後は1.1秒になりました。

仕様

・シェルから引数を指定しての呼び出しを想定
・第一引数は対象ログファイル
・第二引数以降をオプションとして扱い、key:value形式で絞込み要素を指定
※ログファイル内のltsvのvalue部に角括弧([])があってもなくても動作します
オプションの指定時は角括弧不要です。
・valueは大文字小文字無視で絞り込まれる
複数の絞り込み内容を指定した場合はAND条件で絞る
・絞り込みは部分一致検索とする

Gemfile
source "https://rubygems.org"

gem "active_support", "~> 3.0.0"
実装コード
# encoding: utf-8
require "shell"
require 'active_support'

module StandardIo
  extend ActiveSupport::Concern

  module ClassMethods
    def validate_args_counts(count)
      define_method "validate_args_counts" do
        $*.size == count
      end
    end

    def validate_file(index)
      define_method "validate_file_#{index}" do
        return false unless args_has_index?(index)
        FileTest.exist?($*[index])
      end
    end
  end

  def validate
    self.methods.grep(/^validate_.*/).each do |m|
      return false unless method(m).call
    end
    true
  end

  def args_has_index?(index)
    $*.size > index
  end
end

module LtsvParser
  class Parser
    include StandardIo
    validate_file 0
    attr_accessor :ltsv_file, :filter_words

    def initialize
      @ltsv_file = $*[0]
      @filter_words = get_filter_words
    end

    def output
      commands = ""
      if @filter_words.size == 0
        puts `cat #{ltsv_file}`
        return
      end

      commands << "egrep -i \"#{@filter_words.first[0]}:\\\[{0,1}[^\t]*#{@filter_words.first[1]}[^\t]*\\\]{0,1}\" #{ltsv_file} | "
      @filter_words.delete @filter_words.first[0]
      @filter_words.each do |k, v|
        commands << "egrep -i \"#{k}:\\\[*?[^\t]*#{v}[^\t]*\\\]{0,1}\" | "
      end
      puts `#{commands.chop.chop}`
    end

    private
    def get_filter_words
      filter_words = {}
      filters.each do |fil|
        kv = fil.split(":")
        filter_words[kv[0].to_sym] = kv[1] if (fil.count(":")) == 1
      end
      filter_words
    end

    def filters
      $*[1..-1]
    end
  end
end

ps = LtsvParser::Parser.new
exit unless ps.validate
ps.output

サンプル実行

対象ログLTSV:hoge.log
key1:value1_1	key2:value2_1
key1:value1_2_1	key2:value2_2_1
key1:value1_2_2	key2:value2_2_2
key1:value1_3	key2:value2_3
実行結果
$ruby ltsv_parser.rb hoge.log
key1:value1_1   key2:value2_1
key1:value1_2_1 key2:value2_2_1
key1:value1_2_2 key2:value2_2_2
key1:value1_3   key2:value2_3
$ruby ltsv_parser.rb hoge.log key1:value1_2
key1:value1_2_1 key2:value2_2_1
key1:value1_2_2 key2:value2_2_2
$ruby ltsv_parser.rb hoge.log key1:value1_2 key2:value2_2_2
key1:value1_2_2 key2:value2_2_2