じゃらんのWebサービスは、地域ごとに振られたエリアコードか、地図の座標を引数として情報を引っぱってくるので、任意の地域名から情報を検索しようとするとジオコーディングが必要になる。
ジオコーディングを提供するWebサービスはGoogleをはじめとしていくつかあるが、もう少し手軽に使いたかったので、転置インデックスでエリアコードを検索するコードを書いてみた。*1
mkidx.rb
#!/usr/bin/env ruby $KCODE = 'u' require 'optparse' require 'rexml/document' opt = OptionParser.new xmlfil = nil opt.on('-f', '--file=XML_FILE') {|v| xmlfil = v } opt.parse!(ARGV) unless xmlfil $stderr.puts opt.help exit 1 end # parse area.xml areas = [] open(xmlfil) do |source| attrs = lambda {|e| { :name => e.attributes['name'], :cd => e.attributes['cd'] } } REXML::Document.new(source).each_element('Area/Prefecture') do |pref| pref_attrs = attrs[pref] pref.each_element('LargeArea') do |larea| larea_attrs = attrs[larea] larea.each_element('SmallArea') do |sarea| sarea_attrs = attrs[sarea] areas << { :pref => pref_attrs, :larea => larea_attrs, :sarea => sarea_attrs } end end end end # make index index = {} areas.each_with_index do |area, id| id += 1 name = [:pref, :larea, :sarea].map {|k| area[k][:name] }.join name.gsub!(/\343\203\273|\357\275\245|\357\274\210|\357\274\211/, '') cs = name.split(//) bigrams = ([nil] + cs).zip(cs) bigrams.shift bigrams.pop bigrams.each do |i| bigram = i.join index[bigram] ||= [] index[bigram] << id end end # output index puts areas.length index.each do |bigram, ids| puts "#{bigram}:#{ids.join(',')}" end
作成されたインデックスはこんな感じ。
$ wget http://jws.jalan.net/content/data/area.xml
$ ./mkidx.rb -f area.xml > area.idx
679
天王:210,452,452,453,454
騨下:375
住浜:478,479,480
………
mklst.rb
次にエリアコードと地域名のリストを作る
#!/usr/bin/env ruby $KCODE = 'u' require 'optparse' require 'rexml/document' opt = OptionParser.new xmlfil = nil opt.on('-f', '--file=XML_FILE') {|v| xmlfil = v } opt.parse!(ARGV) unless xmlfil $stderr.puts opt.help exit 1 end # parse area.xml areas = [] open(xmlfil) do |source| attrs = lambda {|e| { :name => e.attributes['name'], :cd => e.attributes['cd'] } } REXML::Document.new(source).each_element('Area/Prefecture') do |pref| pref_attrs = attrs[pref] pref.each_element('LargeArea') do |larea| larea_attrs = attrs[larea] larea.each_element('SmallArea') do |sarea| sarea_attrs = attrs[sarea] areas << { :pref => pref_attrs, :larea => larea_attrs, :sarea => sarea_attrs } end end end end # output index areas.each_with_index do |area, id| id += 1 names = [:pref, :larea, :sarea].map {|k| area[k][:name] }.join(',') cds = [:pref, :larea, :sarea].map {|k| area[k][:cd] }.join(',') puts "#{id}:#{names}:#{cds}" end
作成されたリストはこんな感じ。
$ ./mklst.rb -f area.xml > area.lst
1:北海道,札幌,ススキノ・大通:010000,010200,010202
2:北海道,札幌,北大・丘珠:010000,010200,010205
3:北海道,札幌,琴似・テイネ:010000,010200,010208
………
search.rb
最後に任意の地域名からエリアコードを検索する。
#!/usr/bin/env ruby $KCODE = 'u' require 'nkf' require 'optparse' opt = OptionParser.new idxfil = nil lstfil = nil phrase = nil nkfopt = nil opt.on('-i', '--index=INDEX_FILE') {|v| idxfil = v } opt.on('-l', '--list=LIST_FILE') {|v| lstfil = v } opt.on('-p', '--phrase=PHRASE') {|v| phrase = v } opt.on('-x', '--nkf=OPTION') {|v| nkfopt = v } opt.parse!(ARGV) if nkfopt phrase = NKF.nkf(nkfopt, phrase) end unless idxfil and lstfil and phrase $stderr.puts opt.help exit 1 end # read index n = nil index = {} open(idxfil) do |f| n = f.readline.to_i f.each_line do |l| bigram, ids = l.strip.split(/:/, 2) ids = ids.split(/,/).map {|i| i.to_i } index[bigram] = ids end end # read list list = {} open(lstfil) do |f| f.each_line do |l| id, names, cds = l.strip.split(/:/, 3) names = names.split(/,/) cds = cds.split(/,/) list[id.to_i] = [names, cds] end end # calc TF phrase = phrase.split(/\s|\343\200\200/).select {|i| not i.empty? } tfs = (0...phrase.length).map { {} } phrase.each_with_index do |w, i| cs = w.split(//) bigrams = ([nil] + cs).zip(cs) bigrams.shift if cs.length > 1 bigrams.pop bigrams.each do |chars| bigram = chars.join tfs[i][bigram] ||= 0 tfs[i][bigram] += 1 end end # calc score scores = {} (1...n).each {|i| scores[i] = Array.new(phrase.length, 0) } tfs.each_with_index do |ws, i| ws.each do |w, tf| if w.split(//).length > 1 bigrams = [w] else r = Regexp.compile(Regexp.escape(w)) bigrams = index.keys.select {|key| r =~ key } end bigrams.each do |bigram| ids = index[bigram] || [] df = ids.length idf = Math.log(n / (df + 1)) tfidf = tf * idf ids.each do |id| scores[id][i] += tfidf end end end end # sort list sorted = scores.sort_by {|id, score| score.map {|i| -i } } sorted = sorted.map do |id, score| names = list[id][0] cds = list[id][1] [id, names, cds, score] end # output result sorted.each do |id, names, cds, score| if score.all? {|i| i.nonzero? } puts "#{cds[2]}:#{names.join('/')}(#{score.join(', ')})" end end
「横浜」をキーワードにして検索するとこんな感じになる。
$ ./search.rb -i area.idx -l area.lst -p "横浜" -x "-Sw" | nkf -s
140202:神奈川県/横浜/横浜・ベイエリア(9.45477563742468)
140211:神奈川県/横浜/新横浜・青葉(9.45477563742468)
140208:神奈川県/横浜/戸塚・港南(4.72738781871234)
ちなみ「津」と入力すると、検索されない…orz
→てきとーに修正。これはひどいかも。
最後の「if score.all? {|i| i.nonzero? }」を「if score.any? {|i| i.nonzero? }」にするとOR検索になる。