Ruby によるGML版基盤地図情報パーザーを改善

d:id:hfu:20090716 の続きとして、基盤地図情報パーザーを改善しました。

改善項目

改善項目は、次のとおりです。

  • XML を読む部分で、REXML::StreamListener を使っていたが、基盤地図情報Shift_JIS で書かれているためか、REXML::ParseException が発生することがあった。速度の向上も期待して、nokogiri を使用するように変更した。
  • ZIP ファイルの中でフォルダを作成してデータが格納されているパターン(FG-GML-03201-ALL-Z002.zip)が発見されたので、この場合でもデータを読むように変更した。
  • 現在読んでいる ZIP ファイル中のファイル名称から、Parser#city_code, Parser#type, Parser#dev_date, Parser#serial を読めるようにした。

改善後

# 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)
    Find.find(src_dir) {|path|
      next unless /zip$/.match path
      Zip::ZipFile.foreach(path) {|zip_entry|
        if /FG-GML-(\d{5})-(.*?)-(\d{8})-(\d{4})\.xml$/.match zip_entry.name
          @city_code = $1
          @type = $2
          @dev_date = $3
          @serial = $4
          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

次回、改善したパーザーでの実験結果を書こうと思っていますが、nokogiri の採用によって、かなりの程度、処理速度が向上したようです。両極端の比較になるかと思いますが、JRuby で REXML を使う旧版を使ったときと、Ruby (CRuby, MRI) 1.8.7 で nokogiri を使う新版を使った時とでは、処理速度が 10 倍以上異なるようです。