読者です 読者をやめる 読者になる 読者になる

ochalog

Ruby と MediaWiki が好きな電子・情報系の学生のブログ。

Wikipedia から国道の通過都道府県の情報を抽出する方法

KOKUDOU.COM の RDS-KE のような国道データベースを自力で作成したい人(いるのか?)向け。もちろん一つずつ調べるのは厳しいので、Ruby を使って自動化してみた。

前提知識

  • 日本の国道は国道 1〜507 号まである。このうち、国道 59〜100、109〜111、214〜216 号は欠番。
  • Wikipedia の国道の記事のデータ部分はかなり信頼できる。国道の通過都道府県の情報は「/(通過|経由)(する)?(自治体|市町村?)/」という見出しで始まる節に入っている(なんと全部の国道について!)。
  • MediaWiki API を使うと MediaWiki の記事のソース(MediaWiki 記法)を取得できる。
    • JSON 形式で取得した場合、object["query"]["export"]["*"] に格納される。

JSON のダウンロード

環境は Ruby 1.9.3p286。

まず、適当な作業ディレクトリ(以下 ./)を用意。その下に json というディレクトリを作る。

以下のコードを ./get-json.rb として保存(文字コードUTF-8 で)。

#encoding: utf-8
 
require "net/http"
require "cgi"
 
URL_WP_API = "http://ja.wikipedia.org/w/api.php"
 
uri = URI(URL_WP_API)
params = {
  "action" => "query",
  "format" => "json",
  "export" => "",
  "redirects" => ""
}
 
nr_nums = []
[(1..58), (101..108), (112..213), (217..507)].each do |r|
  r.each {|n| nr_nums << n}
end
 
nr_nums.each do |n|
  route_name = "国道#{n}"
  file_name = "./json/wp_#{n}.json"
 
  params["titles"] = route_name
  uri.query = URI.encode_www_form(params)
 
  begin
    json = Net::HTTP.get_response(uri).body
    File.open(file_name, "w") do |f|
      f.print json
    end
 
    puts "Exported #{file_name}"
 
    sleep 1 # サーバー負荷抑制のための休止
  rescue
    puts "Can't get json of #{route_name}"
  end
end

これを実行。

$ ruby get-json.rb

459 秒以上 ≒ 8 分ほどかかるので、気長に待ちましょう。終わったら準備は完了。

抽出

考え方はこんな感じ。

  1. 記事のソースから「/(通過|経由)(する)?(自治体|市町村?)/」という見出し(以下 h)を探す。h のレベルを記録しておく。
  2. h の下の行から都道府県名を探す。見つかったら記録する。
  3. 次に h と同じか h より上のレベルの見出しが見つかったら、探索を終了する。

コードは以下のとおり。これを ./ext-prefs.rb として保存する(文字コードUTF-8)。

#encoding: utf-8
 
require "json"
 
# 都道府県名の配列
PREF_NAMES = ["北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県",
  "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都",
  "神奈川県", "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県",
  "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府",
  "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県",
  "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県",
  "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"]
 
# スキャンの状態を表す定数
STATUS_READY = 0
STATUS_HEADING_FOUND = 1
STATUS_DONE = 2
 
# 見出し行ならば真を返す
def mw_heading?(line)
  /^=/ =~ line
end
 
# 見出しのレベルを返す
def mw_h_level(line)
  line.scan(/^=+/)[0].length
end
 
# 通過する都道府県名を抽出する
def extract_prefs(range)
  result = {}
  not_found_roads = []
 
  range.each do |n|
    route_name = "国道#{n}"
    file_name = "./json/wp_#{n}.json"
 
    begin
      json = nil
      File.open(file_name) do |f|
        json = f.read
      end
    rescue
      next
    end
 
    h = JSON.parse json
    lines = h["query"]["export"]["*"].strip.split("\n")
 
    passing_prefs = []
    h_level = nil
    search_target = ""
    status = STATUS_READY
    lines.each do |l|
      case status
      when STATUS_DONE
        break
      when STATUS_READY
        if mw_heading?(l) && /通過|経由/ =~ l && /自治体|/ =~ l
          h_level = mw_h_level(l)
          status = STATUS_HEADING_FOUND
        end
      when STATUS_HEADING_FOUND
        if mw_heading?(l) && mw_h_level(l) <= h_level
          status = STATUS_DONE
        else
          search_target << l
        end
      end
    end
 
    PREF_NAMES.each do |p|
      passing_prefs << p if search_target.include? p
    end
 
    if passing_prefs.empty?
      not_found_roads << route_name
    else
      result[route_name] = passing_prefs
    end
  end
 
  result["未発見"] = not_found_roads
 
  result
end
 
def output_hash(h)
  h.each do |k, v|
    puts "#{k}: #{v.join(", ")}"
  end
end
 
h_output = extract_prefs(1..507)
 
output_hash(h_output)

ポイントは 66〜68 行目と 76〜78 行目。いったん通過都道府県を数字として配列に格納することで、重複を除き、ソートすることが容易になる。ここでの並び順は JIS X 0401 の都道府県コードに合わせている。ソート後に都道府県名に再変換する。

数字→文字列の変換は筋が悪いので書き直し。検索対象文字列を用意、対象範囲の行をそれに追加し、その文字列に対して都道府県名がヒットするかを判定する方法に変更。都道府県名の配列がソート済みであるから後でソートする必要はないし、行ごとの探索ではなくなったので重複排除も要らなくなった。

さて、実行結果は…

$ ruby ext-prefs.rb
国道1号: 東京都, 神奈川県, 静岡県, 愛知県, 三重県, 滋賀県, 京都府, 大阪府
国道2号: 大阪府, 兵庫県, 岡山県, 広島県, 山口県, 福岡県
国道3号: 福岡県, 佐賀県, 熊本県, 鹿児島県
国道4号: 青森県, 岩手県, 宮城県, 福島県, 茨城県, 栃木県, 埼玉県, 東京都
国道5号: 北海道
国道6号: 宮城県, 福島県, 茨城県, 千葉県, 東京都
国道7号: 青森県, 秋田県, 山形県, 新潟県
国道8号: 新潟県, 富山県, 石川県, 福井県, 滋賀県, 京都府
国道9号: 京都府, 兵庫県, 鳥取県, 島根県, 山口県
国道10号: 福岡県, 大分県, 宮崎県, 鹿児島県
国道11号: 徳島県, 香川県, 愛媛県
国道12号: 北海道
国道13号: 秋田県, 山形県, 福島県
国道14号: 千葉県, 東京都
国道15号: 東京都, 神奈川県
国道16号: 埼玉県, 千葉県, 東京都, 神奈川県
国道17号: 群馬県, 埼玉県, 東京都, 新潟県
国道18号: 群馬県, 新潟県, 長野県
国道19号: 長野県, 岐阜県, 愛知県
国道20号: 東京都, 神奈川県, 山梨県, 長野県
国道21号: 岐阜県, 滋賀県
国道22号: 岐阜県, 愛知県
国道23号: 愛知県, 三重県
国道24号: 京都府, 奈良県, 和歌山県
国道25号: 三重県, 大阪府, 奈良県
国道26号: 大阪府, 和歌山県
国道27号: 福井県, 京都府
国道28号: 兵庫県, 徳島県
国道29号: 兵庫県, 鳥取県
国道30号: 岡山県, 香川県
国道31号: 広島県
国道32号: 徳島県, 香川県, 高知県
国道33号: 愛媛県, 高知県
国道34号: 佐賀県, 長崎県
国道35号: 佐賀県, 長崎県
国道36号: 北海道
国道37号: 北海道
国道38号: 北海道
国道39号: 北海道
国道40号: 北海道
国道41号: 富山県, 岐阜県, 愛知県
国道42号: 静岡県, 愛知県, 三重県, 和歌山県
国道43号: 大阪府, 兵庫県
国道44号: 北海道
国道45号: 青森県, 岩手県, 宮城県
国道46号: 岩手県, 秋田県
国道47号: 宮城県, 山形県
国道48号: 宮城県, 山形県
国道49号: 福島県, 新潟県
国道50号: 茨城県, 栃木県, 群馬県
国道51号: 茨城県, 千葉県
国道52号: 山梨県, 静岡県
国道53号: 鳥取県, 岡山県
国道54号: 島根県, 広島県
国道55号: 徳島県, 高知県
国道56号: 愛媛県, 高知県
国道57号: 長崎県, 熊本県, 大分県
国道58号: 鹿児島県, 沖縄県
国道101号: 青森県, 秋田県
国道102号: 青森県
国道103号: 青森県, 秋田県
国道104号: 青森県, 秋田県
国道105号: 秋田県
国道106号: 岩手県
国道107号: 岩手県, 秋田県
国道108号: 宮城県, 秋田県
国道112号: 山形県
国道113号: 宮城県, 山形県, 福島県, 新潟県
国道114号: 福島県
国道115号: 福島県
国道116号: 新潟県
国道117号: 新潟県, 長野県
国道118号: 福島県, 茨城県
国道119号: 栃木県
国道120号: 栃木県, 群馬県
国道121号: 山形県, 福島県, 栃木県
国道122号: 栃木県, 群馬県, 埼玉県, 東京都
国道123号: 茨城県, 栃木県
国道124号: 茨城県, 千葉県
国道125号: 茨城県, 埼玉県, 千葉県
国道126号: 千葉県
国道127号: 千葉県
国道128号: 千葉県
国道129号: 神奈川県
国道130号: 東京都
国道131号: 東京都
国道132号: 神奈川県
国道133号: 神奈川県
国道134号: 神奈川県
国道135号: 神奈川県, 静岡県
国道136号: 静岡県
国道137号: 山梨県
国道138号: 神奈川県, 山梨県, 静岡県
国道139号: 東京都, 山梨県, 静岡県
国道140号: 埼玉県, 山梨県
国道141号: 山梨県, 長野県
国道142号: 長野県
国道143号: 長野県
国道144号: 群馬県, 長野県
国道145号: 群馬県
国道146号: 群馬県, 長野県
国道147号: 長野県
国道148号: 新潟県, 長野県
国道149号: 静岡県
国道150号: 静岡県
国道151号: 長野県, 愛知県
国道152号: 長野県, 静岡県
国道153号: 長野県, 愛知県
国道154号: 愛知県
国道155号: 愛知県
国道156号: 富山県, 岐阜県
国道157号: 石川県, 福井県, 岐阜県
国道158号: 福井県, 長野県, 岐阜県
国道159号: 石川県
国道160号: 富山県, 石川県
国道161号: 福井県, 滋賀県
国道162号: 福井県, 京都府
国道163号: 三重県, 京都府, 大阪府, 奈良県
国道164号: 三重県
国道165号: 三重県, 大阪府, 奈良県
国道166号: 三重県, 大阪府, 奈良県
国道167号: 三重県
国道168号: 大阪府, 奈良県, 和歌山県
国道169号: 三重県, 奈良県, 和歌山県
国道170号: 大阪府
国道171号: 京都府, 大阪府, 兵庫県
国道172号: 大阪府
国道173号: 京都府, 大阪府, 兵庫県
国道174号: 兵庫県
国道175号: 京都府, 兵庫県
国道176号: 京都府, 大阪府, 兵庫県
国道177号: 京都府
国道178号: 京都府, 兵庫県, 鳥取県
国道179号: 兵庫県, 鳥取県, 岡山県
国道180号: 鳥取県, 島根県, 岡山県
国道181号: 鳥取県, 岡山県
国道182号: 岡山県, 広島県
国道183号: 鳥取県, 広島県
国道184号: 島根県, 広島県
国道185号: 広島県
国道186号: 島根県, 広島県
国道187号: 島根県, 山口県
国道188号: 山口県
国道189号: 山口県
国道190号: 山口県
国道191号: 島根県, 広島県, 山口県
国道192号: 徳島県, 愛媛県
国道193号: 徳島県, 香川県
国道194号: 愛媛県, 高知県
国道195号: 徳島県, 高知県
国道196号: 愛媛県
国道197号: 愛媛県, 高知県, 大分県
国道198号: 福岡県
国道199号: 福岡県
国道200号: 福岡県
国道201号: 福岡県
国道202号: 福岡県, 佐賀県, 長崎県
国道203号: 佐賀県
国道204号: 佐賀県, 長崎県
国道205号: 長崎県
国道206号: 長崎県
国道207号: 佐賀県, 長崎県
国道208号: 福岡県, 佐賀県, 熊本県
国道209号: 福岡県
国道210号: 福岡県, 大分県
国道211号: 福岡県, 大分県
国道212号: 熊本県, 大分県
国道213号: 大分県
国道217号: 大分県
国道218号: 熊本県, 宮崎県
国道219号: 熊本県, 宮崎県
国道220号: 宮崎県, 鹿児島県
国道221号: 熊本県, 宮崎県
国道222号: 宮崎県, 鹿児島県
国道223号: 宮崎県, 鹿児島県
国道224号: 鹿児島県
国道225号: 鹿児島県
国道226号: 鹿児島県
国道227号: 北海道
国道228号: 北海道
国道229号: 北海道
国道230号: 北海道
国道231号: 北海道
国道233号: 北海道
国道234号: 北海道
国道235号: 北海道
国道236号: 北海道
国道237号: 北海道
国道238号: 北海道
国道239号: 北海道
国道240号: 北海道
国道241号: 北海道
国道242号: 北海道
国道243号: 北海道
国道244号: 北海道
国道245号: 茨城県
国道246号: 東京都, 神奈川県, 静岡県
国道247号: 愛知県
国道248号: 岐阜県, 愛知県
国道249号: 石川県
国道250号: 兵庫県, 岡山県
国道251号: 長崎県
国道252号: 福島県, 新潟県
国道254号: 群馬県, 埼玉県, 東京都, 長野県
国道255号: 神奈川県
国道256号: 長野県, 岐阜県
国道257号: 岐阜県, 静岡県, 愛知県
国道258号: 岐阜県, 三重県
国道259号: 愛知県, 三重県
国道260号: 三重県
国道261号: 島根県, 広島県
国道262号: 山口県
国道263号: 福岡県, 佐賀県
国道264号: 福岡県, 佐賀県
国道265号: 熊本県, 宮崎県
国道266号: 熊本県
国道267号: 熊本県, 鹿児島県
国道268号: 熊本県, 宮崎県, 鹿児島県
国道269号: 宮崎県, 鹿児島県
国道270号: 鹿児島県
国道271号: 神奈川県
国道272号: 北海道
国道273号: 北海道
国道274号: 北海道
国道275号: 北海道
国道276号: 北海道
国道277号: 北海道
国道278号: 北海道
国道279号: 北海道, 青森県
国道280号: 北海道, 青森県
国道281号: 岩手県
国道282号: 青森県, 岩手県, 秋田県
国道283号: 岩手県
国道284号: 岩手県, 宮城県
国道285号: 秋田県
国道286号: 宮城県, 山形県
国道287号: 山形県
国道288号: 福島県
国道289号: 福島県, 新潟県
国道290号: 新潟県
国道291号: 群馬県, 新潟県
国道292号: 群馬県, 新潟県, 長野県
国道293号: 茨城県, 栃木県
国道294号: 福島県, 茨城県, 栃木県, 千葉県
国道295号: 千葉県
国道296号: 千葉県
国道297号: 千葉県
国道298号: 埼玉県, 千葉県, 東京都
国道299号: 群馬県, 埼玉県, 長野県
国道300号: 山梨県
国道301号: 静岡県, 愛知県
国道302号: 愛知県
国道303号: 福井県, 岐阜県, 滋賀県
国道304号: 富山県, 石川県
国道305号: 石川県, 福井県
国道306号: 三重県, 滋賀県
国道307号: 滋賀県, 京都府, 大阪府
国道308号: 大阪府, 奈良県
国道309号: 三重県, 大阪府, 奈良県
国道310号: 大阪府, 奈良県
国道311号: 三重県, 奈良県, 和歌山県
国道312号: 京都府, 兵庫県
国道313号: 鳥取県, 岡山県, 広島県
国道314号: 島根県, 広島県
国道315号: 山口県
国道316号: 山口県
国道317号: 広島県, 愛媛県
国道318号: 徳島県, 香川県
国道319号: 徳島県, 香川県, 愛媛県
国道320号: 愛媛県, 高知県
国道321号: 高知県
国道322号: 福岡県
国道323号: 佐賀県
国道324号: 長崎県, 熊本県
国道325号: 福岡県, 熊本県, 宮崎県
国道326号: 大分県, 宮崎県
国道327号: 熊本県, 宮崎県
国道328号: 鹿児島県
国道329号: 沖縄県
国道330号: 沖縄県
国道331号: 沖縄県
国道332号: 沖縄県
国道333号: 北海道
国道334号: 北海道
国道335号: 北海道
国道336号: 北海道
国道337号: 北海道
国道338号: 北海道, 青森県
国道339号: 青森県
国道340号: 青森県, 岩手県
国道341号: 秋田県
国道342号: 岩手県, 宮城県, 秋田県
国道343号: 岩手県
国道344号: 秋田県, 山形県
国道345号: 山形県, 新潟県
国道346号: 岩手県, 宮城県
国道347号: 宮城県, 山形県
国道348号: 山形県
国道349号: 宮城県, 福島県, 茨城県
国道350号: 新潟県
国道351号: 新潟県
国道352号: 福島県, 栃木県, 新潟県
国道353号: 群馬県, 新潟県
国道354号: 茨城県, 群馬県, 埼玉県
国道355号: 茨城県, 千葉県
国道356号: 千葉県
国道357号: 千葉県, 東京都, 神奈川県
国道358号: 山梨県
国道359号: 富山県, 石川県
国道360号: 富山県, 石川県, 岐阜県
国道361号: 長野県, 岐阜県
国道362号: 静岡県, 愛知県
国道363号: 岐阜県, 愛知県
国道364号: 石川県, 福井県
国道365号: 石川県, 福井県, 岐阜県, 三重県, 滋賀県
国道366号: 愛知県
国道367号: 福井県, 滋賀県, 京都府
国道368号: 三重県, 奈良県
国道369号: 三重県, 奈良県
国道370号: 奈良県, 和歌山県
国道371号: 大阪府, 奈良県, 和歌山県
国道372号: 京都府, 兵庫県
国道373号: 兵庫県, 鳥取県, 岡山県
国道374号: 岡山県
国道375号: 島根県, 広島県
国道376号: 山口県
国道377号: 徳島県, 香川県
国道378号: 愛媛県
国道379号: 愛媛県
国道380号: 愛媛県
国道381号: 愛媛県, 高知県
国道382号: 佐賀県, 長崎県
国道383号: 佐賀県, 長崎県
国道384号: 長崎県
国道385号: 福岡県, 佐賀県
国道386号: 福岡県, 大分県
国道387号: 熊本県, 大分県
国道388号: 熊本県, 大分県, 宮崎県
国道389号: 福岡県, 長崎県, 熊本県, 鹿児島県
国道390号: 沖縄県
国道391号: 北海道
国道392号: 北海道
国道393号: 北海道
国道394号: 青森県
国道395号: 岩手県
国道396号: 岩手県
国道397号: 岩手県, 秋田県
国道398号: 宮城県, 秋田県
国道399号: 宮城県, 山形県, 福島県
国道400号: 福島県, 茨城県, 栃木県
国道401号: 福島県, 群馬県
国道402号: 新潟県
国道403号: 新潟県, 長野県
国道404号: 新潟県
国道405号: 群馬県, 新潟県, 長野県
国道406号: 群馬県, 長野県
国道407号: 栃木県, 群馬県, 埼玉県
国道408号: 茨城県, 栃木県, 千葉県
国道409号: 千葉県, 神奈川県
国道410号: 千葉県
国道411号: 東京都, 山梨県
国道412号: 神奈川県
国道413号: 神奈川県, 山梨県
国道414号: 静岡県
国道415号: 富山県, 石川県
国道416号: 石川県, 福井県
国道417号: 福井県, 岐阜県
国道418号: 福井県, 長野県, 岐阜県
国道419号: 岐阜県, 愛知県
国道420号: 愛知県
国道421号: 三重県, 滋賀県
国道422号: 三重県, 滋賀県, 奈良県
国道423号: 京都府, 大阪府
国道424号: 和歌山県
国道425号: 三重県, 奈良県, 和歌山県
国道426号: 京都府, 兵庫県
国道427号: 兵庫県
国道428号: 兵庫県
国道429号: 京都府, 兵庫県, 岡山県
国道430号: 岡山県
国道431号: 鳥取県, 島根県
国道432号: 島根県, 広島県
国道433号: 広島県
国道434号: 広島県, 山口県
国道435号: 山口県
国道436号: 兵庫県, 香川県
国道437号: 山口県, 愛媛県
国道438号: 徳島県, 香川県
国道439号: 徳島県, 高知県
国道440号: 愛媛県, 高知県
国道441号: 愛媛県, 高知県
国道442号: 福岡県, 熊本県, 大分県
国道443号: 福岡県, 熊本県
国道444号: 佐賀県, 長崎県
国道445号: 熊本県
国道446号: 熊本県, 宮崎県
国道447号: 宮崎県, 鹿児島県
国道448号: 宮崎県, 鹿児島県
国道449号: 沖縄県
国道450号: 北海道
国道451号: 北海道
国道452号: 北海道
国道453号: 北海道
国道454号: 青森県, 秋田県
国道455号: 岩手県
国道456号: 岩手県, 宮城県
国道457号: 岩手県, 宮城県
国道458号: 山形県
国道459号: 福島県, 新潟県
国道460号: 新潟県
国道461号: 茨城県, 栃木県
国道462号: 群馬県, 埼玉県, 長野県
国道463号: 埼玉県
国道464号: 千葉県
国道465号: 千葉県
国道466号: 東京都, 神奈川県
国道467号: 神奈川県
国道468号: 茨城県, 埼玉県, 千葉県, 東京都, 神奈川県
国道469号: 山梨県, 静岡県
国道470号: 富山県, 石川県
国道471号: 富山県, 石川県, 岐阜県
国道472号: 富山県, 岐阜県
国道473号: 静岡県, 愛知県
国道474号: 長野県, 静岡県, 愛知県
国道475号: 岐阜県, 愛知県
国道476号: 福井県
国道477号: 三重県, 滋賀県, 京都府, 大阪府, 兵庫県
国道478号: 京都府
国道479号: 大阪府
国道480号: 大阪府, 和歌山県
国道481号: 大阪府
国道482号: 京都府, 兵庫県, 鳥取県, 岡山県
国道483号: 兵庫県
国道484号: 岡山県
国道485号: 島根県
国道486号: 岡山県, 広島県
国道487号: 広島県
国道488号: 島根県, 広島県
国道489号: 山口県
国道490号: 山口県
国道491号: 山口県
国道492号: 徳島県, 香川県, 高知県
国道493号: 高知県
国道494号: 愛媛県, 高知県
国道495号: 福岡県
国道496号: 福岡県, 大分県
国道497号: 福岡県, 佐賀県, 長崎県
国道498号: 佐賀県, 長崎県
国道499号: 長崎県, 鹿児島県
国道500号: 福岡県, 佐賀県, 大分県
国道501号: 福岡県, 熊本県
国道502号: 大分県
国道503号: 熊本県, 宮崎県
国道504号: 鹿児島県
国道505号: 沖縄県
国道506号: 沖縄県
国道507号: 沖縄県
未発見: 国道232号, 国道253号

2個を除いて成功。まあ十分でしょう。

「未発見」の国道が出たのは、記事の「通過市町村」の節に市町村名しか書かれていなかったことが原因。いずれも 1 つの道・県のみ通る国道なので、道・県名を書く必要はないと判断されていたのだろう。

データの利用先

こんな面倒くさいことを考えたのはもちろん目的があったためで。「JARTIC 交通規制情報」の国道別表示で必要なデータだったから。出力の形を変えて無事組み込めた。冒頭に書いたとおり、手作業では厳しすぎるので、うまくできてよかった。これは完全に Wikipedia の書式が整っていたおかげ。ありがたい。