たのしいRuby 第23章 郵便番号検索

膨大なデータから素早く検索処理をしたい!!そうなると、データベースの力を借りるのがいい。しかし、サーバー型のデータベースを使うほどでも無い。

そんな中途半端な問題を解決してくれるのが、ハッシュ型のデータベース。今回はgdbmを使った郵便番号の検索です。

まずは、実行結果から

% ./jzipcode.rb 907-1800
沖縄県八重山郡与那国町
search time : 8.533465
% ./jzipcode.rb 907-1800
沖縄県八重山郡与那国町
search time : 0.002622

DB作成時には9秒近くかかっていた処理が、DB作成後には、0.002秒で検索出来てしまう!!

凄いね。

改造ポイント

  • 自作makeモジュールを使ってデータベース更新タイミングを正確にした。
    • 動的makeはなかなかいい感じ。
    • 副作用として、プログラム更新時にいちいちDBを作りなおしてしまう。
  • 000-0000の形式でも検索できるように。
  • 本では、nkfの使い方が間違ってたので修正。UTF-8の場合、「nkf -w」だ!!

実際のコード

今回は長いよ・・・。

jzipcode.rb
#!/usr/bin/ruby

=begin
  郵便番号検索
  Usage : jzipcode.rb [郵便番号]
=end

module Make
  # makeが必要かどうか判断する
  # true 必要
  def make?(target, *prereq)
    return true unless File.exist?(target) # targetが存在しない
    return true if prereq.length.zero? # 依存ファイルが無い
    
    target_ctime = File.stat(target).ctime
    prereq.each do |req|
      return true if target_ctime < File.stat(req).ctime
    end
    return false
  end
  
  # target : prereq1 prereq2 ....
  #         {  ruby code  }
  def make(target, *prereq)
    if make?(target, *prereq)
      yield
    end
  end
end

module ZipCode
  ZIP_FILE = "ken_all.csv"
  DB_FILE = "ken_all.db"
  COLUMN_ZIP = 2
  
  include Make
  require "gdbm"
  require "csv"
  
  def make_database()
    make(DB_FILE, $0, ZIP_FILE) do
      open(ZIP_FILE) do |io|
        GDBM.open(DB_FILE, 0644, GDBM::NEWDB) do |db|
          io.each do |line|
            colums = line.split(",")
            zipcode = colums[COLUMN_ZIP].delete('"')
            if tmp = db[zipcode]
              line = tmp + line
            end
            db[zipcode] = line
          end
        end
      end
    end
  end

  def find(code)
    make_database()
    GDBM.open(DB_FILE, nil, GDBM::READER|GDBM::NOLOCK) do |db|
      if line = db[code]
        return CSV.parse(line)
      end
    end
    return nil
  end
end

# main

t = Time.now

include ZipCode
require "nkf"

begin
  str = ARGV[0] || "100-0000"
  if rows = find(str.delete("-"))
    rows.each do |row|
      address = row[6] + row[7]
      puts NKF.nkf("-w", address)
    end
  else
    puts "can't find #{str}"
  end
rescue => ex
 abort ex.message
end

puts "search time : #{Time.now - t}"

まとめと課題

  • 今回のサンプルはあまり実用性がなかったが、gdbmモジュールがあれば、wikiやblogが作れそう。
  • SQLiteを使ってみたい。
  • keyの検索処理は問題ないが、valueの検索処理についてはまだ試してない。
  • rakeを調べる必要あり。