01. 郵便番号APIサーバーを構築します。
はじめに
郵便番号から住所を提供してくるツールなど調べていたらこの記事にたどり着きました。
FizzBuzz 問題どや顔で解くひとなんかよりも "KEN_ALL.csv" をうまく扱える人の方が社会的貢献度高い - Togetter
ざっと読んだ上で、面白そうなので郵便番号APIサーバの構築してみます。
郵便番号APIを提供している有名どころや新しいもの
郵便番号検索API - zipcloud
GitHub - madefor/postal-code-api: Postal Code API
郵便番号から住所を検索するAPIを作りました - Qiita
PostcodeJP API
不備があれば編集リスクエスト下さい。
目標
-
郵便局
が提供している郵便番号データを利用して郵便番号から住所を返却する
郵便番号APIサーバーを構築します。
開発環境
こちらはプロジェクト構築後に確認した開発環境になります。
$ rbenv exec bundle exec rake about
Running via Spring preloader in process 92302
About your application's environment
Rails version 5.2.0
Ruby version 2.5.0-p0 (x86_64-darwin16)
RubyGems version 2.7.3
Rack version 2.0.5
JavaScript Runtime Node.js (V8)
Middleware Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper
Application root /Users/zipcode-sandboc
Environment development
Database adapter mysql2
Database schema version 0
Github
プロジェクト作成
コマンドを打つ時に
rbenv exec
から始めるコマンドについては適宜自分の環境に合わせて読み替えて下さい。基本は省略可のはずです。
$ mkdir zipcode-sandbox
$ cd zipcode-sandbox
$ git init
$ gibo node MacOS rails >> .gitignore
$ rbenv local 2.5.0
$ ndenv local v8.9.3
$ rbenv exec gem install bundle
$ rbenv exec bundler init
Gemfileを開きRailsのバージョンを指定します。
# gem 'rails'
gem 'rails', '~> 5.2.0'
$ rbenv exec bundle install --path=vendor/bundle/
$ rbenv exec bundle exec rails new . --database=mysql -T -G
Railsのプロジェクトオプションの説明
オプション | 説明 |
---|---|
--database=mysql | mysqlを指定 |
-T | Test::Unitを組み込まない |
-G | .gitignoreを組み込まない |
dockerでmysqlコンテナーを立てます。
$ touch docker-compose.yml
version: '2'
services:
datastore:
image: busybox
volumes:
- mysql-data:/var/lib/mysql
db:
image: mysql:5.7.10
environment:
- MYSQL_ROOT_PASSWORD=abc123
- MYSQL_USER=root
- MYSQL_DATABASE=zipcode-sandboc_development
ports:
- "3306:3306"
volumes_from:
- datastore
volumes:
mysql-data:
driver: local
dockerを構築します。
$ docker-compose build
$ docker-compose up -d
mysqlが接続できるか確認します。
mysql>
と出れば接続成功です。
$ mysql -h127.0.0.1 -uroot --password=abc123 --port=3306
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.10 MySQL Community Server (GPL)
<省略>
mysql>
プロジェクトを起動してみます。
$ rbenv exec bundle exec rails s --port=3000 -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.0 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.11.4 (ruby 2.5.0-p0), codename: Love Song
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
0.0.0.0:3000
にアクセスして接続できればプロジェクト立ち上げ成功です。
02. 郵便局から郵便番号ZIPファイルを取得する。
郵便番号ZIPを取得します。
郵便番号取得バッチを作成するためにrunner
を利用します。
wget,curlあたりで取得するほうが簡単ですがRubyに拘ります。
名前空間にlib配下を追加します。
名前空間の設定追加
config.autoload_paths += Dir["#{config.root}/lib"]
runnerのサンプルを作成して動きを確認します。
普段はrakeタスクを利用しているのでrunner
の動作確認をします。
runnerの使い方がわかる人は、
郵便局から郵便番号ZIPファイルを取得します。
まで読み飛ばして下さい。
バッチクラスを作成
実行確認のために簡単なバッチを用意します。
# lib/tasks/batch.rb
class Tasks::Batch
def self.execute
# 実行したいコードを書く
p "Hello world"
end
end
バッチの実行確認
railsを起動して、runnerタスクを実行します。
タスクが実行できればOKです。
$ rbenv exec bundle exec rails runner Tasks::Batch.hello
Running via Spring preloader in process 95534
"Hello world"
郵便局から郵便番号ZIPファイルを取得します。
郵便局が提供している郵便番号データをダウンロードします。
ダウンロードには標準ライブラリのopen-uri
を利用します。
また、ダウンロードしたzipファイル解凍のにはgem rubyzip
を利用します。
読み仮名データの促音・拗音を小書きで表記しないもの - zip形式 日本郵便全国一括版を利用します。
事前にgem rubyzip
をinstallしておきます。
gem 'rubyzip', '~> 1.2', '>= 1.2.1'
$ rbenv exec bundle install --path=vendor/bundle/
郵便番号ZIPファイルの取得手順
- 前回取得したzipファイルと展開したKEN_ALL.CSVファイルを削除
- URLからzipファイルをダウンロード
- zipファイルを解凍
詳細の説明はコードに記載します。
require 'open-uri'
require 'zip'
class Tasks::JapanPost
class << self
def download_zipcodes_file
delete_files(['public/KEN_ALL.CSV', 'public/ken_all.zip'])
download_and_save("public/", "http://www.post.japanpost.jp/zipcode/dl/oogaki/zip/ken_all.zip")
unzip_file("public/", "ken_all.zip")
p 'public/ken_all.zipファイルの展開に成功しました。'
end
private
def delete_files(delete_files)
delete_files.each do |file|
File.delete file
end
end
def download_and_save(dir_path, url_path)
fileName = File.basename url_path
open(dir_path + "#{fileName}", 'wb') do |output|
open(url_path) do |data|
output.write(data.read)
end
end
end
def unzip_file(dir_path, file_name)
Zip::File.open(dir_path + file_name) do |zip|
zip.each do |entry|
# { true } は展開先に同名ファイルが存在する場合に上書きする指定
zip.extract(entry, dir_path + entry.name) { true }
end
end
end
end
end
シェルで実行
シェルで実行できるようにscripts/zipcode_download.sh
を作成します。
$ mkdir scripts
$ touch scripts/zipcode_download.sh
$ chmod 766 scripts/ # (一応)実行権限を付与
シェルはこんな感じです。
#!/usr/bin/env bash
# 前回取得したzipファイルを削除
# zipファイルをダウンロードして解凍まで実施。
$ rbenv exec bundle exec rails runner Tasks::JapanPost.download_zipcodes_file
実行します。
$ sh ./scripts/zipcode_download.sh
public/KEN_ALL.CSV
が生成されていればOKです。
参考
郵便番号検索 | ゆうびん.jp
郵便番号データのダウンロード - zipcloud
03. 郵便番号一覧CSVファイルの読み込み
取得したCSVファイルを読み込みます。
後からDBに登録するのですが、とりあえずCSVファイルを読み込みます。
標準ライブラリCSV
を利用します。
CSVファイルのエンコードは文字コードには、MS漢字コード(SHIFT JIS)です。
require 'csv'
class Tasks::JapanPost
class << self
def update_zipcodes
csv_file_read("public/", "KEN_ALL.CSV").each do |csv|
p csv # この情報をDBに登録予定
end
end
private
def self.csv_file_read(dir_path, file_name)
CSV.read(dir_path + file_name, encoding: "CP932:UTF-8");
end
end
end
シェルで実行できるようにscripts/insert_and_update_zipcodes.sh
を作成します。
$ touch scripts/insert_and_update_zipcodes.sh
シェルはこんな感じです。
#!/usr/bin/env bash
# CSVファイルを読み込んで、DBに保存します。
# レコードが存在しない場合は新規登録し、存在する場合は更新
rbenv exec bundle exec rails runner Tasks::JapanPost.update_zipcodes
実行
$ sh ./scripts/insert_and_update_zipcodes.sh
["37201", "760 ", "7600042", "カガワケン", "タカマツシ", "ダイクマチ", "香川県", "高松市", "大工町", "0", "0", "0", "0", "0", "0"]
["37201", "761 ", "7618041", "カガワケン", "タカマツシ", "ダンシチヨウ", "香川県", "高松市", "檀紙町", "0", "0", "0", "0", "0", "0"]
["37201", "760 ", "7600007", "カガワケン", "タカマツシ", "チユウオウチヨウ", "香川県", "高松市", "中央町", "0", "0", "0", "0", "0", "0"]
["37201", "761 ", "7618058", "カガワケン", "タカマツシ", "チヨクシチヨウ", "香川県", "高松市", "勅使町", "0", "0", "0", "0", "0", "0"]
["37201", "760 ", "7600061", "カガワケン", "タカマツシ", "ツキジチヨウ", "香川県", "高松市", "築地町", "0", "0", "0", "0", "0", "0"]
こんな感じでCSVファイルが読み込めればOKです。
04.郵便番号を格納するテーブルを作ります。
郵便番号データの説明に詳しい情報が掲載されています。
郵便番号データの説明 - 日本郵便
DB設計
郵便番号一覧を格納するためのテーブルを用意します。
返却対象
は郵便番号検索結果として利用するデータを指します。
郵便番号APIサーバが返却するデータと今のところは定義しておきます。
郵便番号のデータ形式
No | 用途 | 返却対象 |
---|---|---|
1 | 全国地方公共団体コード 半角数字 | - |
2 | (旧)郵便番号(5桁)半角数字 | - |
3 | 郵便番号(7桁)半角数字 | ◯ |
4 | 都道府県名 半角カタカナ(コード順に掲載)(注1) | - |
5 | 市区町村名 半角カタカナ(コード順に掲載)(注1) | - |
6 | 町域名 半角カタカナ(五十音順に掲載) (注1) | - |
7 | 都道府県名 漢字(コード順に掲載) (注1,2) | ◯ |
8 | 市区町村名 漢字(コード順に掲載) (注1,2) | ◯ |
9 | 町域名 漢字(五十音順に掲載) (注1,2) | ◯ |
10 | 一町域が二以上の郵便番号で表される場合の表示 (注3)(「1」は該当、「0」は該当せず) |
- |
11 | 小字毎に番地が起番されている町域の表示 (注4)(「1」は該当、「0」は該当せず) |
- |
12 | 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず) |
- |
13 | 一つの郵便番号で二以上の町域を表す場合の表示 (注5) (「1」は該当、「0」は該当せず) |
- |
14 | 更新の表示 (注6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用)) |
- |
15 | 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用)) |
- |
2018/05時点の資料抜粋
引用 郵便番号データの説明 - 日本郵便
十数万件あるデータは上記の仕様に準拠していますが、上記の条件に合わない特例が存在します。
その特例条件のデータについれは、10〜15のカラムでわかるようになっています。
ですが、いろんなサイトで公開されていない仕様があると言われているので郵便番号を正すにはそれなりの覚悟が入りそうです。
modelを作成します。
まずは、マイグレーションファイルを作成します。
class CreateYubins < ActiveRecord::Migration[5.2]
def change
create_table :yubins do |t|
t.string :local_governments_cd
t.string :past_zipcode
t.string :zipcode, null: false, index: true
t.string :region_kana, null: false
t.string :locality_kana, null: false
t.string :street_address_kana, null: false
t.string :region, null: false
t.string :locality, null: false
t.string :street_address, null: false
t.integer :flag_1, default: 0, null: false
t.integer :flag_2, default: 0, null: false
t.integer :flag_3, default: 0, null: false
t.integer :flag_4, default: 0, null: false
t.integer :view_update, default: 0, null: false
t.integer :reason, default: 0, null: false
t.timestamps
end
end
end
次にマイグレーションを実行
$ rbenv exec bundle exec rake db:migrate
== 20180511000000 CreateYubins: migrating =====================================
-- create_table(:yubins)
-> 0.0125s
== 20180511000000 CreateYubins: migrated (0.0126s) ============================
ついでにSEEDを作成してデータを入れます。
Yubin.create([{
local_governments_cd: '26109',
past_zipcode: '612 ',
zipcode: '6128035',
region_kana: 'キョウトフ',
locality_kana: 'キョウトシフシミク',
street_address_kana: 'トキワチョウ',
region: '京都府',
locality: '京都市伏見区',
street_address: '常盤町',
flag_1: 0,
flag_2: 0,
flag_3: 0,
flag_4: 0,
view_update: 1,
reason: 5
},
{
local_governments_cd: '28201',
past_zipcode: '670 ',
zipcode: '6700815',
region_kana: 'ヒョウゴケン',
locality_kana: 'ヒメジシ',
street_address_kana: 'ノザトヤマトチョウ',
region: '兵庫県',
locality: '姫路市',
street_address: '野里大和町',
flag_1: 0,
flag_2: 0,
flag_3: 0,
flag_4: 0,
view_update: 1,
reason: 5
}])
seeds.rbを流して、データの確認をしてみます。
$ rbenv exec bundle exec rake db:seed
$ rbenv exec bundle exec rails console -s
Running via Spring preloader in process 51625
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0> Yubin.find 1
Yubin Load (0.9ms) SELECT `yubins`.* FROM `yubins` WHERE `yubins`.`id` = 1 LIMIT 1
=> #<Yubin id: 1, local_governments_cd: "26109", past_zipcode: "612 ", zipcode: "6128035", region_kana: "キョウトフ", locality_kana: "キョウトシフシミク", street_address_kana: "トキワチョウ", region: "京都府", locality: "京都市伏見区", street_address: "常盤町", flag_1: 0, flag_2: 0, flag_3: 0, flag_4: 0, view_update: 1, reason: 5, created_at: "2018-05-12 15:17:17", updated_at: "2018-05-12 15:17:17">
irb(main):002:0> Yubin.find 2
Yubin Load (1.7ms) SELECT `yubins`.* FROM `yubins` WHERE `yubins`.`id` = 2 LIMIT 1
=> #<Yubin id: 2, local_governments_cd: "28201", past_zipcode: "670 ", zipcode: "6700815", region_kana: "ヒョウゴケン", locality_kana: "ヒメジシ", street_address_kana: "ノザトヤマトチョウ", region: "兵庫県", locality: "姫路市", street_address: "野里大和町", flag_1: 0, flag_2: 0, flag_3: 0, flag_4: 0, view_update: 1, reason: 5, created_at: "2018-05-12 15:17:17", updated_at: "2018-05-12 15:17:17">
irb(main):003:0>
登録できたデータを読み込むことができました。
実際に郵便番号一覧をDBに登録してみます。
一旦上記で試したDBデータは破棄します。
# DBをDropして作り直し
$ rbenv exec bundle exec rake db:drop db:create db:migrate
Dropped database 'zipcode-sandboc_development'
Dropped database 'zipcode-sandboc_test'
Created database 'zipcode-sandboc_development'
Created database 'zipcode-sandboc_test'
== 20180511144137 CreateYubins: migrating =====================================
-- create_table(:yubins)
-> 0.0135s
== 20180511144137 CreateYubins: migrated (0.0135s) ============================
CSVファイルのままだと、扱いが不便なので一旦、何も加工せずに郵便番号一覧をDBに格納します。
データを加工せずにDBに取り込むのならテーブルの名前をcsv_yubinにするなど工夫したほうがいいかも。
# CSVファイルを読み込みDBにinsertします。
def self.update_zipcodes
csv_file_read("public/", "KEN_ALL.CSV").each do |csv|
p csv
Yubin.create!({
local_governments_cd: csv[0],
past_zipcode: csv[1],
zipcode: csv[2],
region_kana: csv[3],
locality_kana: csv[4],
street_address_kana: csv[5],
region: csv[6],
locality: csv[7],
street_address: csv[8],
flag_1: csv[9],
flag_2: csv[10],
flag_3: csv[11],
flag_4: csv[12],
view_update: csv[13],
reason: csv[14]
})
end
p "#{Yubin.all.length}を登録しました。"
end
実行してDBに取り込みます。
$ rbenv exec bundle exec rails runner Tasks::JapanPost.update_zipcodes
・・・
["47381", "90715", "9071544", "オキナワケン", "ヤエヤマグンタケトミチヨウ", "ハトマ", "沖縄県", "八重山郡竹富町", "鳩間", "0", "0", "0", "0", "0", "0"]
["47382", "90718", "9071800", "オキナワケン", "ヤエヤマグンヨナグニチヨウ", "イカニケイサイガナイバアイ", "沖縄県", "八重山郡与那国町", "以下に掲載がない場合", "0", "0", "0", "0", "0", "0"]
ログにCSVの内容が流れればOKです。
実際にDBに登録しているログの確認するにはdocker-compose logs
で確認できます。
$ docker-compose logs -f db
(0.3ms) BEGIN
↳ lib/tasks/japan_post.rb:17
Yubin Create (0.5ms) INSERT INTO `yubins` (`local_governments_cd`, `past_zipcode`, `zipcode`, `region_kana`, `locality_kana`, `street_address_kana`, `region`, `locality`, `street_address`, `created_at`, `updated_at`) VALUES ('47348', '90113', '9011301', 'オキナワケン', 'シマジリグンヨナバルチヨウ', 'イタラシキ', '沖縄県', '島尻郡与那原町', '板良敷', '2018-05-12 15:37:52', '2018-05-12 15:37:52')
↳ lib/tasks/japan_post.rb:17
(1.6ms) COMMIT
↳ lib/tasks/japan_post.rb:17
これで、KEN_ALL.CSV
をDBに取り込むことに成功しました。
rails console
で確認してみます。
$ rbenv exec bundle exec rails console -s
Running via Spring preloader in process 54010
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
irb(main):002:0>
irb(main):003:0>
irb(main):004:0>
irb(main):005:0> Yubin.all.length
Yubin Load (319.0ms) SELECT `yubins`.* FROM `yubins`
=> 124184
irb(main):006:0>
OKですね。
05. 郵便番号APIを作成する。
郵便番号一覧をDBに登録することができましたので
郵便番号から住所を取得するAPIを実装します。
KEN_ALL.CSVが癖が強いデータだということは認識しています。
ここではAPIを作成に注力します。
API設計
ざっとな感じの設計方針
- サーバ側に状態を保持するような情報を持たない。
- リクエストは完全かつ孤立したものにする。
- 同じURLは同じリソースを返す。
APIイメージ
例 | URL |
---|---|
request | //zipcode-sandbox/api/v1/yubin/0660005 |
{
"message": null,
"results": [
{ "address1": "北海道",
"address2": "千歳市",
"address3": "協和",
"zipcode": "0660005"
}],
"status": 200
}
実装
リクエストを受けるためのControllerを作成します。
$ rbenv exec bundle exec rails generate controller api::v1::yubins search
create app/controllers/api/v1/yubins2_controller.rb
route namespace :api do
namespace :v1 do
get 'yubins/search'
end
end
invoke erb
create app/views/api/v1/yubins
create app/views/api/v1/yubins/search.html.erb
invoke helper
create app/helpers/api/v1/yubins_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/api/v1/yubins.coffee
invoke scss
create app/assets/stylesheets/api/v1/yubins.scss
コントローラには郵便番号を検索するためのsearch
メソッドを用意します。
class Api::V1::YubinController < ApplicationController
def search
@yubins = Yubin.where(zipcode: params[:id])
@message = "該当の郵便番号は見つかりませんでした。" if @yubins.blank?
end
end
viewsにはAPIで返却するjsonを生成する処理を記載します。
json.set! :results do
json.array! @yubins do |yubin|
json.zipcode yubin.zipcode
json.region yubin.region
json.locality yubin.locality
json.street_address yubin.street_address
end
end
json.car_count @yubins.length
json.message @message ||= ''
json.status 200
現時点では、この情報を返却するようにしました。
郵便番号からヒットする全ての郵便番号情報を返却します。
Key | Volue |
---|---|
count | ヒットした件数 |
message | エラーがある時に、該当の郵便番号は見つかりませんでした。 を表示 |
results | 郵便番号一覧 |
status | 200 (固定) |
3レコードで1つの郵便番号を表現しているレコードを呼び出してみます。
$ http http://0.0.0.0:3000/api/v1/yubins/0660005
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"40bf1b12dfa9886570852ac408753acd"
Referrer-Policy: strict-origin-when-cross-origin
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 6ded3dfa-fdbc-4e92-99ec-1dd5779dd4a1
X-Runtime: 0.042539
X-XSS-Protection: 1; mode=block
{
"count": 3,
"message": "",
"results": [
{
"locality": "千歳市",
"region": "北海道",
"street_address": "協和(88-2、271-10、343-2、404-1、427-",
"zipcode": "0660005"
},
{
"locality": "千歳市",
"region": "北海道",
"street_address": "3、431-12、443-6、608-2、641-8、814、842-",
"zipcode": "0660005"
},
{
"locality": "千歳市",
"region": "北海道",
"street_address": "5、1137-3、1392、1657、1752番地)",
"zipcode": "0660005"
}
],
"status": 200
}
存在しない郵便番号の場合
$ http http://0.0.0.0:3000/api/v1/yubins/9999999
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"28937b496d793376719c55fe009f52d9"
Referrer-Policy: strict-origin-when-cross-origin
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 12dcc5fb-66be-489d-afa1-5c76f167af6d
X-Runtime: 0.012420
X-XSS-Protection: 1; mode=block
{
"count": 0,
"message": "該当の郵便番号は見つかりませんでした。",
"results": [],
"status": 400
}
郵便番号が1つでも取得できればstatus
は200を返却し。
郵便番号が1つも取得できなければstatus
は400を返却するとします。
おわりに、
ここまでで、Railsアプリプロジェクトを構築し、郵便局が提供している郵便番号一覧を取り込み、郵便番号から住所を返却するAPIを構築することができました。自社で郵便番号を構築するために必要な作業手順は一旦完了です。
また、郵便番号一覧の情報について何も加工しない状態のデータを返却するAPIのバージョンをv1
とします。
次回は、郵便番号一覧のデータ補正を行いつつ郵便番号APIの品質を高めてV2
を作成していきます。
参考
KEN_ALL.CSV (郵便番号検索)の落とし穴 - Qiita
郵便番号や市区町村データを取り扱うときにはまったこと - Qiita
Rails4の郵便番号/住所変換のマスターデータ作成手順 - Qiita
日本郵便が公開する郵便番号データをそのまま利用するのがなぜ難しいか。そして、住所から郵便番号を求めるのがなぜ難しいか
郵便番号データは自分で加工しない - daily dayflower