##はじめに
全国の郵便番号から住所テーブルに一括で保存し、月毎に更新するタスクを作ってみました。
しかし、郵便番号は約12万行ほどあり、更新する際にこれを総舐めするのは負荷がでかくて無駄が多いです。
これを解決すべく、月ごとの差分から更新するタスクも作ってみたので参考までに公開してみようと思います。
##やること
1. 郵便番号一覧のDBを作成
2. 郵便番号をダウンロード
3. CSVからDBに保存
4. CSVの差分から更新
##1. 郵便番号一覧のDBを作成
- 郵便番号(zip_code
)
- 都道府県(state
)
- 市区町村(city
)
- 番地(street
)
class CreateAddresses < ActiveRecord::Migration[5.0]
def change
create_table :addresses do |t|
t.column :zip_code, "CHAR(7)", null: false
t.string :state, null: false
t.string :city, null: false
t.string :street
t.timestamps
end
add_index :addresses, :zip_code, unique: true, name: "add_unique_index_zip_code"
end
end
##2. 郵便番号をダウンロード
郵便番号をダウンロードするタスクを作成します。
郵便番号はこちらからダウンロードが可能。
保存と更新の両方で使うのでモジュール化しておくと楽でしょう。
module JapanPost
module AddressDownload
def download_zipcode_file
@download_url = "https://www.post.japanpost.jp/zipcode/dl/roman/ken_all_rome.zip"
@download_path = Rails.root.join("lib", "tasks", "data", "tmp")
download_zip_file
unzip
end
def download_zip_file
zip_file = open(@download_url)
@zip_name = File.basename @download_url
File.open(@download_path + @zip_name, "w") do |file|
file.write zip_file.read.force_encoding("UTF-8")
end
end
def unzip
Zip::File.open(@download_path + @zip_name) do |zip|
zip.each do |entry|
@csv_name = "KEN_ALL_#{entry.time.strftime('%Y%m')}.CSV"
zip.extract(entry, @download_path + @csv_name) { true }
end
end
File.delete @download_path + @zip_name
end
end
end
ざっくりと処理の流れ↓
- zipファイルをダウンロード
- 解凍して
lib/tasks/data/tmp/
配下に今月分のKEN_ALL.CSV
を作る - zipファイルだけ削除
解凍の参考記事: https://qiita.com/ogontaro/items/e11d10a460e127ad29d0
##3. 郵便番号をDBに保存
namespace :tmp do
desc "郵便番号をインポート"
task import_address: :environment do
include JapanPost::AddressDownload
download_zipcode_file
import_zipcode
end
private
def import_zipcode
addresses = []
CSV.open(@download_path + @csv_name, encoding: "Shift_JIS:UTF-8").each do |row|
addresses << Address.new(
zip_code: row[0],
state: row[1],
city: row[2],
street: row[3].sub(/(.*|以下に掲載がない場合/, ""),
)
end
Address.import addresses
end
end
CSVを読み込んでデータを流し込みます。
大通西(1~19丁目) → 大通西
以下に掲載がない場合 → ""
となるように、(
より後ろ、以下に掲載がない場合は削除するようにしてます。
このタスクを走らせると、lib/tasks/data/tmp/
配下に今月分のKEN_ALL.CSV
が残ってるはずです。
$ ls lib/tasks/data/tmp/
KEN_ALL_201906.CSV
月毎の更新で、先月分のKEN_ALL.CSV
から差分を抽出する際必要になるので消さないようにしましょう。
あとは更新するだけなので、このタスクは不要です。
tmp
にしたのはこれが理由で、走らせたらは削除してもOK。
4. CSVの差分からDBを更新する
require "diff-lcs"
namespace :address do
desc "郵便番号を更新"
task update: :environment do
include JapanPost::AddressDownload
download_zipcode_file
update_zipcodes
end
private
def update_zipcodes
last_month_csv = CSV.read(@download_path + "KEN_ALL_#{Time.current.ago(1.month).strftime('%Y%m')}.CSV", encoding: "Shift_JIS:UTF-8")
this_month_csv = CSV.read(@download_path + @csv_name, encoding: "Shift_JIS:UTF-8")
diffs = Diff::LCS.diff(last_month_csv, this_month_csv)
File.delete @download_path + "KEN_ALL_#{Time.current.ago(1.month).strftime('%Y%m')}.CSV"
return if diffs.blank?
addresses = []
diffs.flatten.select(&:adding?).each do |diff|
row = diff.element
address = Address.find_or_initialize_by(zip_code: row[0])
address.state = row[1]
address.city = row[2]
address.street = row[3].sub(/(.*|以下に掲載がない場合/, "")
addresses << address
end
Address.import addresses, on_duplicate_key_update: [:state, :city, :street]
end
end
先月分のCSV(last_month_csv
)と, 今月分のCSV(this_month_csv
の差分を抽出して更新しています。
CSV
の差分はこんな感じで求めることが可能。
diffs = Diff::LCS.diff(変更前のCSV, 変更後のCSV)
差分の中身はこんな感じです。
> diffs.flatten
=> [
["-", 0, ["0600000", "hoge北海道", "札幌市 中央区", "以下に掲載がない場合", "HOKKAIDO", "SAPPORO SHI CHUO KU", "IKANIKEISAIGANAIBAAI"]],
["-", 1, ["0640941", "fuga北海道", "札幌市 中央区", "旭ケ丘", "HOKKAIDO", "SAPPORO SHI CHUO KU", "ASAHIGAOKA"]],
["+", 0, ["0600000", "北海道", "札幌市 中央区", "以下に掲載がない場合", "HOKKAIDO", "SAPPORO SHI CHUO KU", "IKANIKEISAIGANAIBAAI"]],
["+", 1, ["0640941", "北海道", "札幌市 中央区", "旭ケ丘", "HOKKAIDO", "SAPPORO SHI CHUO KU", "ASAHIGAOKA"]]
]
そこからadding?
を使い、変更後のものに絞ればいいわけです。
> diffs.flatten.select(&:adding?)
=> [
["+", 0, ["0600000", "北海道", "札幌市 中央区", "以下に掲載がない場合", "HOKKAIDO", "SAPPORO SHI CHUO KU", "IKANIKEISAIGANAIBAAI"]],
["+", 1, ["0640941", "北海道", "札幌市 中央区", "旭ケ丘", "HOKKAIDO", "SAPPORO SHI CHUO KU", "ASAHIGAOKA"]]
]
diff
の中身はelement
で取り出します。
> diff
=> ["+", 0, ["0600000", "北海道", "札幌市 中央区", "以下に掲載がない場合", "HOKKAIDO", "SAPPORO SHI CHUO KU", "IKANIKEISAIGANAIBAAI"]]
> diff.element
=> ["0600000", "北海道", "札幌市 中央区", "以下に掲載がない場合", "HOKKAIDO", "SAPPORO SHI CHUO KU", "IKANIKEISAIGANAIBAAI"]
adding?・・・変更後を取り出す
element・・・変更のシーケンスを取り出す
参考: https://www.rubydoc.info/gems/diff-lcs/1.2.5/Diff/LCS/Change#adding%3F-instance_method
※[補足]
ここではじめて先月分のCSVを削除しますが、
File.delete @download_path + "KEN_ALL_#{Time.current.ago(1.month).strftime('%Y%m')}.CSV"
今月分のCSVは残すことを忘れてはいけません。
これはimport
したときと同様、来月更新するときに必要になるからです。
$ ls lib/tasks/data/tmp/
KEN_ALL_201906.CSV ← 削除
KEN_ALL_201907.CSV ← 残す
##月毎走らせる
あとは更新タスクを毎月走らせてあげればOK。
# 郵便番号に変更があれば更新
# DL先: https://www.post.japanpost.jp/zipcode/dl/roman/ken_all_rome.zip
every 1.month, at: "3:00pm" do
rake "address:update"
end
注意点としては、先にimport
を済ませてから走らせることです。そうでないと、先月分のCSVがなくて差分を抽出できないためです。
##文字化け対策とか(おまけ)
###Encoding::UndefinedConversionError: "\xEC" from ASCII-8BIT to UTF-8
Zipファイルを読み込む際にこのようなエラーが出た場合、force_encoding("UTF-8")
をしてすれば解決しました。
# 変更前
zip_file.read
# 変更後
zip_file.read.force_encoding("UTF-8")
参考: https://blog.tanebox.com/archives/452/
###ArgumentError: invalid byte sequence in UTF-8
CSVを開くときにこのエラーが出た場合、以下のように修正しましょう。
#変更前
CSV.open("KEN_ALL.CSV")
#変更後
CSV.open("KEN_ALL.CSV", encoding: 'Shift_JIS:UTF-8')
CSVはJIS
で読みこもうとするため、UTF-8
に変換してあげる必要があります。