こんにちは。学生アルバイトの橋本です。弊社ではweb上の様々なデータをスクレイピングをしています。例えば何らかのジャンルの店舗リストをスクレイピングしてデータベースに取り込むとします。もちろんその情報には店舗の住所も含まれます。この住所について、都道府県ごと、区ごとの分布などのデータ分析をする場合、住所の文字列よりも都道府県や市区町村や地域に分かれていた方が扱いやすくなります。
今回は住所の文字列 “東京都渋谷区代々木4丁目28−8” を[“東京都”,”渋谷区”,”代々木”,”4丁目28−8″]などと分割してパースするメソッドを作成してみたのでご紹介します。

目的とする分割例

今回目的とするのは都道府県、市区町村、それ以外の部分での分割とします。例外として東京23区は東京都、区、地域、それ以外にし、また政令指定都市は都道府県、市、区、それ以外で実装します。さらに、政令指定都市や東京23区は都道府県名が省略されていても、補完してくれるものを目指します。パース例はこのようなものです。

parse("東京都渋谷区代々木4丁目28−8")
=> ["東京都", "渋谷区", "代々木", "4丁目28−8"]
parse("名古屋市中区三の丸三丁目1番1号")
=> ["愛知県", "名古屋市", "中区", "三の丸三丁目1番1号"]
parse("神奈川県藤沢市遠藤5322")
=> ["神奈川県", "藤沢市", "遠藤5322"]
parse("沖縄県島尻郡渡嘉敷村阿波連 172")
=> ["沖縄県", "島尻郡渡嘉敷村", "阿波連172"]

住所マスタの作成

日本には様々な住所があるので与えられた住所文字列だけを見て正確に分割することは難しいです。そこであらかじめ日本の住所がまとまっているcsvデータから分割したい区域のマスタを作っておき、そのマスタに当てはまる住所文字列が来たら分割してくれるメソッドを実装します。
元となるcsvは「住所.jp」こちらのサイトでダウンロードできるzenkoku.csvを利用させていただきます。
今回、様々な住所をパースしていると、「霞が関」や「保土ケ谷区」や「千駄ヶ谷」のように「が、ケ、ヶ」での表記ゆれがあるものがあることがわかりました。これらはマスタに3通り全てを含んでしまうことで対応します。

def ga_valiation(address_str)
  if md = address_str.match(/[がケヶ]/)
    address_ga_str_array = [
      address_str.gsub(md[0],'が'),
      address_str.gsub(md[0],'ケ'),
      address_str.gsub(md[0],'ヶ')
    ]
  else
    address_ga_str_array = [address_str]
  end
  return address_ga_str_array
end

実際のcsvからマスタとなるjsonを作成する部分は次のようなものになります。「が」の表記ゆれ対応で醜くなっていますが、東京23区、政令指定都市、それ以外の都市で、それぞれzenkoku_dictionaryに追加していない行が来たら追加していくという実装となっています。東京23区は町域CDという値で区別し、政令指定都市は数が限られているので前もって変数にいれています。

require 'csv'
require 'json'

@seirei_shitei_toshi = [
  # 政令指定都市の都市名をArrayとして羅列
]


def save_address_database_json(csv)
  zenkoku_dictionary = []
  shikuchoson_cds = []
  choiki_cds = []
  CSV.foreach(csv,:headers => true, encoding: "CP932:UTF-8") do |row|
    #東京23区
    if row["市区町村CD"].to_i = 13101
      # まだ追加してない町域を追加する
      unless choiki_cds.include?(row["町域CD"])
        choiki_cds << row["町域CD"]
        if row["町域補足"] == "(該当なし)"
          next
        end
        city_valiation = ga_valiation(row["市区町村"])
        choiki_valiation = ga_valiation(row["町域"])
        city_valiation.each do |city|
          choiki_valiation.each do |choiki|
            zenkoku_dictionary << [row["都道府県"],city,choiki]
          end
        end
      end
    else
      # 東京23区以外の市区町村でまだ追加してないものを追加する
      unless shikuchoson_cds.include?(row["市区町村CD"])
        shikuchoson_cds << row["市区町村CD"]
        # 政令指定都市の場合row["市区町村"]を都市名と区名で分けている
        seirei_ku_array = []
        @seirei_shitei_toshi.each do |seirei|
          if row["市区町村"][0...seirei.size] == seirei
            seirei_ku_array = [row["市区町村"][0...seirei.size],row["市区町村"][seirei.size..-1]]
          end
        end
        if seirei_ku_array.size == 0
          # 東京23区と政令指定都市以外
          city_valiation = ga_valiation(row["市区町村"])
          city_valiation.each do |city|
            zenkoku_dictionary << [row["都道府県"],city]
          end
        else
          # 政令指定都市
          seirei_valiation = ga_valiation(seirei_ku_array[0])
          ku_valiation = ga_valiation(seirei_ku_array[1])
          seirei_valiation.each do |seirei|
            ku_valiation.each do |ku|
              zenkoku_dictionary << [row["都道府県"],seirei,ku]
            end
          end
        end
      end
    end
  end
ここまでで作成したzenkoku_dictionaryの内容をjsonに書き込んでいきます。

  File.open("address_database.json","w") do |file|
    address_hash = []
    zenkoku_dictionary.each do |address|
      keys = ['prefecture','city','region']
      if address.size == 2
        address << ""
      end
      address_array = [keys, address].transpose
      address_hash << Hash[*address_array.flatten]
      p address_hash
    end
    JSON.dump(address_hash,file)
  end
end

csv = "zenkoku.csv"
save_address_database_json(csv)

これによってマスタとなるjsonが作成できました。

[{"prefecture":"北海道","city":"札幌市","region":"中央区"},{"prefecture":"北海道","city":"札幌市","region":"北区"},...
,{"prefecture":"東京都","city":"千代田区","region":"霞が関"},{"prefecture":"東京都","city":"千代田区","region":"霞ケ関"},{"prefecture":"東京都","city":"千代田区","region":"霞ヶ関"},...
,{"prefecture":"沖縄県","city":"八重山郡竹富町","region":""},{"prefecture":"沖縄県","city":"八重山郡与那国町","region":""}]

住所のパース

先ほど作成したマスタに従って住所文字列を分割します。

require 'json'
class AddressParser
  def initialize
    @zenkoku_dictionary = []
    File.open("address_database.json") do |file|
      address_database = JSON.load(file)
      address_database.each do |address_hash|
        if address_hash["region"] == ""
          address = [address_hash["prefecture"],address_hash["city"]]
        else
          address = [address_hash["prefecture"],address_hash["city"],address_hash["region"]]
        end
        @zenkoku_dictionary << address
      end
    end
  end

  def parse(address_str)
    address_str = address_str.gsub(" ","").gsub(" ","")
    @zenkoku_dictionary.each do |address|
      if address_str.include?(address.join)
        address_array = address.dup
        address_array << address_str[address.join.size..-1]
        return address_array
      elsif address_str.include?(address[1..-1].join)
        address_array = address.dup
        address_array << address_str[address[1..-1].join.size..-1]
        return address_array
      end
    end
    address_array = [address_str]
    return address_array
  end

end

initializeでjsonから先ほどのzenkoku_dictionaryを復元し、parseメソッドではzekoku_ditionaryに一致する部分が含まれているかどうかを判別しています。このparseメソッドで最初に例に示したように住所文字列を綺麗に分割することができます。

おわりに

今回、住所をパースするメソッドを作ったわけですが、実際のデータに当てはめてみると「が」のような表記ゆれの他にも、「の、ノ」の表記ゆれや、旧字体が使われている住所があったり、住所の記載ミスや省略があったりなど、全ての住所文字列に対応できるわけではありませんが、多くの文字列を整形できるようになりました。
弊社では今回のようにデータを集めるとともに使いやすい形に整形することも行っています。ご興味のある方はぜひご連絡ください。