Ruby によるGML版基盤地図情報パーザーを改善・確認・ベンチマーク

d:id:hfu:20090723 の続きとしてプログラムを改善し、動作確認とベンチマークを兼ねて基盤地図情報のレコード数を数えてみました。

プログラムを改善

d:id:hfu:20090723 の改善で 25000 データに反応しなくなっていたので、25000 データも処理できるようにしました。25000 データの場合、Parser#city_code に都道府県コードを持ちます。

# Usage of the works is permitted provided that this insturment is retained
# with the works, so that any entity that uses the works is notified of this
# instrument.
# 
# DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.

require 'find'
require 'rubygems'
require 'nokogiri'
require 'zip/zip'

class Parser < Nokogiri::XML::SAX::Document
  ATTR_NAMES = %w{fid lfSpanFr lfSpanTo devDate orgGILvl orgMDId name type admCode alti}
  GEOM_NAMES = %w{pos loc area}
  FEATURE_TYPES = %w{AdmArea AdmBdry BldA BldL CommBdry CommPt CstLine ElevPt RailCL RdCompt RdEdg WL}

  def initialize
    @mode = 'default'
    @feat = Hash.new {|h, k| h[k] = ''}
  end

  attr_reader :city_code, :type, :dev_date, :serial

  def start_element(name, attrs)
   if FEATURE_TYPES.include? name
     @feat = {:feature_type => name.to_sym}
   elsif ATTR_NAMES.include? name
     @mode = name
   elsif GEOM_NAMES.include? name
     @mode = name
     @wkt = case @mode
     when 'pos':
       'POINT ('
     when 'loc':
       'LINESTRING ('
     when 'area':
       'POLYGON ('
     end
   end
  end

  def end_element(name)
    if FEATURE_TYPES.include? name
      @block.call(@feat)
    elsif ATTR_NAMES.include? name
      @mode = 'default'
    elsif GEOM_NAMES.include? name
      @wkt.chop!.chop! if @mode == 'area'
      @wkt += ')'
      @feat[:the_geom] = @wkt
      @mode = 'default'
    end
  end

  def characters(text)
    return if @mode == 'default'
    text.chomp!
    size = text.size
    return if size == 0
    if ATTR_NAMES.include? @mode
      @feat[@mode.to_sym] = text
    elsif GEOM_NAMES.include? @mode
      geometry(text)
    end
  end

  def geometry(posList)
    @wkt += '(' if @mode == 'area'
    posList.split("\n").each {|s|
      coords = s.chomp.split(' ')
      next unless coords.size == 2
      @wkt += "#{coords[1]} #{coords[0]}, "
    }
    @wkt.chop!.chop!
    @wkt += '), ' if @mode == 'area'
  end

  def process_zip_directory(src_dir, &block)
    $stderr.print "#{Time.now}: #{src_dir}\n"
    Find.find(src_dir) {|path|
      next unless /zip$/.match path
      Zip::ZipFile.foreach(path) {|zip_entry|
        if /FG-GML-(\d*?)-(.*?)-(\d{8})-(\d{4})\.xml$/.match zip_entry.name
          @city_code = $1
          @type = $2
          @dev_date = $3
          @serial = $4
          $stderr.print "  #{zip_entry.name}\n"
          process_stream(zip_entry.get_input_stream) {|feat|
            yield feat
          }
        end
      }
    }
  end

  def process_stream(stream, &block)
    @block = block
    parser = Nokogiri::XML::SAX::Parser.new(self)
    parser.parse(stream)
  end
end

if __FILE__ == $0
  parser = Parser.new
  parser.process_zip_directory('../../../src/fgd/2500/') {|feat| 
    print "#{parser.city_code}: #{feat.inspect}\n"
  }
end

縮尺別にレコード数を数えるプログラム

上のプログラムのクラス Parser の動作確認と、ダウンロードした基盤地図情報の確認、Parser のベンチマークを兼ねて、基盤地図情報のレコード数を縮尺別に数えるプログラムを作りました。

require 'parse_nokogiri' # 前の節のプログラムを parse_nokogiri.rb とする

print (['city_code'] + Parser::FEATURE_TYPES + 
  ['count', 'process_time', 'rps']).join(","), "\n"

Dir.glob('../../../src/fgd/2500/?????').sort.each {|path|
  city_code = File.basename(path)
  stat = {}
  Parser::FEATURE_TYPES.each {|name|
    stat[name.to_sym] = Hash.new {|h, k| h[k] = 0}
  }
  start_time = Time.now
  count = 0
  Parser.new.process_zip_directory(path) {|feat|
    count += 1
    stat[feat[:feature_type]][feat[:orgGILvl].to_i] += 1
  }
  process_time = Time.now - start_time
  r = [city_code]
  Parser::FEATURE_TYPES.each {|name|
    r << "\"#{stat[name.to_sym].inspect}\""
  }
  r << count
  r << process_time
  r << count / process_time
  print "#{r.join(',')}\n"
}

プログラム実行結果

結果はあとで書くことにします。