Rails4の郵便番号/住所変換のマスターデータ作成手順

入力フォームにて郵便番号から続きの住所を自動入力させる際、
gemでは一部の住所で柔軟に対応できなさそうなので自前でデータを作りました。
郵便局のCSVデータの実際の加工方法・考慮した点と、Railsで使用する準備まで細かめに載せてみます。

# 前提

* 「○丁目」などの詳細住所パターンは、郵便番号の重複に関係なく全て削除する。
* 郵便番号の重複を許す。
* 同じ郵便番号で違う県にまたがるものもそのまま残す。
* サービス開始時にseedからテーブルへの流し込みを行い通常更新はしない。
* 都道府県名はドロップダウン等でも使用しやすいよう別テーブルとする。

＜表1. 作成テーブルイメージ＞

|エリアテーブル|都道府県名テーブル|
|:---|:---|
|エリアID  |都道府県ID|
|郵便番号|都道府県名|
|都道府県名テーブル（参照）||
|市区町村||
|通り・番地||

# 開発環境

* MacOS X
* Ruby 2.0.0
* Ruby on Rails 4.0.5
* bash
* GNU sed コマンド（※GNUのsedは下記で入手可能）

```
$ brew install gnu-sed
```

# 1. Modelの用意

### 1-1. エリアモデルの用意

```bash
$ rails g model area postal_code:string prefectural:references city:string street:string
```

* 都道府県名はレコード自体を参照する為、"prefectural"カラムを"references"として作成する。

### 1-2. 作成したエリアマイグレーションファイルを編集

```rb:db/migrate/20140826160502_create_areas.rb
class CreateAreas < ActiveRecord::Migration
  def change
    create_table :areas do |t|
      t.string :postal_code,        null: false
      t.references :prefectural,    index: true
      t.string :city,               default: ""
      t.string :street,             default: ""

      #t.timestamps
    end
  end
end
```

* タイムスタンプ有無などはお好みで。


### 1-3. 都道府県名モデルの用意

```bash
$ rails g model prefectural name:string
```

* seedにて都道府県IDを指定する為、カラムは名前だけ追加しておく。

### 1-4. 作成した都道府県名マイグレーションファイルを編集

```rb:db/migrate/20140826162014_create_prefecturals.rb
class CreatePrefecturals < ActiveRecord::Migration
  def change
    create_table :prefecturals do |t|
      t.string :name

      #t.timestamps
    end
  end
end
```

### 1-5. migrate実行

```bash
$ bundle exec rake db:migrate
```

### 1-6. アソシエーションを設定

エリア--多:１--都道府県名

```rb:app/models/area.rb
class Area < ActiveRecord::Base
  belongs_to :prefectural
end
```

```rb:app/models/prefectural.rb
class Prefectural < ActiveRecord::Base
  has_many :areas
end
```

これでデータの受け皿は完成。

# 2. 初期データの用意

今回はdb/直下にエリア及び都道府県名の初期データを作ります。

### 2-1. 郵便局公式サイトから全県データを取得

下記は「読み仮名データの促音・拗音を小書きで表記するもの(zip形式)」になります。

```bash:db/
$ curl -# -LO http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip
######################################################################## 100.0%
$ ls
development.sqlite3 ken_all.zip         migrate             schema.rb           seeds.rb            test.sqlite3
```

### 2-2. 解凍

```
$ unzip ken_all.zip
Archive:  ken_all.zip
  inflating: KEN_ALL.CSV
```

### 2-3. CSVの加工

db/で次の１行のコマンド群を実行します。（※詳細は下記参照）

```bash
$ iconv -f SJIS -t UTF8 KEN_ALL.CSV | gsed -e 's/\r//g' -e 's/"//g' -e 's/以下に掲載がない場合//g' | awk 'BEGIN{FS=",";OFS=",";rtn=0}{if(rtn==0){print($3, substr($1,1,2), $7, $8, $9)}} /（/{rtn=1} /）/{rtn=0}' | sed  -e 's/（.*$//g' -e 's/[０１２３４５６７８９]\+.*[、〜].*//g' | sort | uniq | tee postal_code_seed.csv | awk 'BEGIN{FS=",";OFS=","}{print $2,$3}' | sort | uniq > prefecturals_name_seed.csv
$ 
```

### 2-4. 加工で実施・考慮した内容

上記データ加工でのポイントです。
必要な物を拾って適宜編集して実施して下さい。

＜参考＞郵便局：郵便番号データの説明
http://www.post.japanpost.jp/zipcode/dl/readme.html


#### （１）Shift-JISをUTF-8へ変換

```bash
iconv -f SJIS -t UTF8 KEN_ALL.CSV
```


#### （２）改行コードの調整（ ※bashの場合のコマンド ）

```bash
| gsed -e 's/\r//g' 
```

* Linux上で使用することを想定して、念の為Windows標準改行コードの"CR+LF"のうち"CR"を削っておく。


#### （３）CSV各要素のダブルクオーテーションを削除

```bash
(gsed) -e 's/"//g'
```

#### （４）「その他住所」の記載内容削除。

```bash
(gsed)  -e 's/以下に掲載がない場合//g'
```

特定の郵便番号の対応にない場合、その他としてまとめられているようです。
住所内容として「以下に掲載がない場合」と書かれています。
そのまま出力されるのはおかしいので削除しておきます。

#### （５）必要行の必要な要素のみを抽出

実行しているawkコマンド

```awk
BEGIN{FS=",";OFS=",";rtn=0}
{
  if(rtn==0){
    print($3, substr($1,1,2), $7, $8, $9)
  }
}
/（/{rtn=1} #開始括弧があれば次の行から取得しない
/）/{rtn=0} #閉じ括弧があれば次の行から取得再開
```

BEGIN{FS=",";OFS="}で、カンマ区切りにデータを取得出力もカンマ区切り。
substr(\$1,1,2)で県コード（１列目の先頭２桁）のみ取得。
郵便番号（３列目）、県名（７列目）、市区町村名（８列目）、番地等（９列目）を取得。
長い住所記載の場合途中で内容が切れて次の行に続きが書かれているものがある為、
一旦取得してから閉じ括弧がなければ閉じ括弧がある行まではrtnというフラグ取得を抑止しています。
※"（"以降の文字はあとからsedで削除


#### （６）住所パターン部分の削除１

```bash
(gsed) -e 's/[０１２３４５６７８９]\+.*[、〜].*//g' 
```

「地割」など、特殊な住所の場合括弧ではなく数字パターンが並べられています。
全角の数字以降を削除して最低限の文字だけ取り出します。

【例】

* 野々宿６０地割〜野々宿６２地割 => 野々宿
* 細内６８地割、細内６９地割 => 細内

#### （７）住所パターン部分の削除２

```bash
(gsed) -e 's/（.*$//g'
```

郵便番号確定時に入力フォームに紐付いた「住所範囲」などがそのまま記入されてしまうと、
ユーザーは一旦その部分を消さなくてはいけないというダサいことになります。
その為今回は全角の括弧の**開始以降は全て削除**しています。

【例】

* 真駒内（その他） => 真駒内
* 北二条西（１〜１９丁目） => 北二条西

【注意】

* 対応する閉じ括弧がない行は（５）にて削除している為、今回は「（」以降を全て削除しています。
* 同CSV内の後続に書かれたフラグ等も使用する場合この書き方はしないで下さい。 

#### （８）エリアデータは都道府県名データとは分けて保存

```bash
sort | uniq | tee postal_code_seed.csv
```

#### （９）必要な要素を抽出
都道府県コード（ID）と都道府県名だけ取り出す。

```bash
 | awk 'BEGIN{FS=",";OFS=","}{print $2,$3}' | uniq > prefecturals_name_seed.csv
```


### 2-5. 加工後のデータイメージ

##### エリアテーブル用データ

お好みで都道府県名IDでソートして下さい。

```bash:db/postal_code_seed.csv
$ head -3 postal_code_seed.csv && tail -3 postal_code_seed.csv
0010000,01,北海道,札幌市北区,
0010010,01,北海道,札幌市北区,北十条西
0010011,01,北海道,札幌市北区,北十一条西
9998524,06,山形県,飽海郡遊佐町,富岡
9998525,06,山形県,飽海郡遊佐町,直世
9998531,06,山形県,飽海郡遊佐町,菅里
$ 
```

##### 都道府県名テーブル用データ

```bash:db/prefecturals_name_seed.csv
$ head -3 prefecturals_name_seed.csv && tail -3 prefecturals_name_seed.csv
01,北海道
02,青森県
03,岩手県
45,宮崎県
46,鹿児島県
47,沖縄県
$ 
```

# 3. 初期テーブル状態の作成

### 3-1. seedファイルの作成

seedファイルに以下を追記して、作成したモデルと流し込むデータを紐付けます。

```rb:db/seeds.rb
# coding: utf-8
require "csv"

Prefectural.delete_all # 重複するとERRORになる為、seedの度に再作成している。
namelist = Array.new
CSV.foreach('db/prefecturals_name_seed.csv') do |row|
  record = {
    :id              => row[0].to_i,
    :name            => row[1],
  }
  p record
  name = Prefectural.create!(record)
  namelist[name.id] = name # 配列でオブジェクトを退避
end

Area.delete_all
CSV.foreach('db/postal_code_seed.csv') do |row|
  record = {
    :postal_code     => row[0],
    :prefectural     => namelist[row[1].to_i], # オブジェクトを紐付け
    :city            => row[3],
    :street          => row[4],
  }
  p record
  Area.create!(record)
end
```

Prefectural.delete_allで"rake db:seed"を行う度に、該当テーブルの内容を破棄して入れ直しています。
Prefecturalモデルのレコード作成時に配列を作成して、Areaの:prefecturalに参照させています。

### 3-2. seedの取り込み

```bash
$ bundle exec rake db:seed
```

### 3-3. 反映確認

```bash
$ rails console
irb(main):016:0> Prefectural.all
irb(main):017:0> Area.all
```



投入データが参照出来れば問題有りません。
テーブル反映が確認出来たら、KEN_ALL.CSV等は相当重いので消しておきましょう。
あとはviewにインスタンス変数として渡すなりjsonで渡すなりして使えるはずです。