日本郵便のデータ(旧:KEN_ALL)を活用して、郵便番号マスタを作成してみた

こんにちは。GMOインターネットグループ株式会社 フロントエンドエンジニアの新垣です。

私は普段、社内の細々としたツールなどを作成しているのですが、
今回、どれかひとつでいいのでアウトプットしてくださいとお達しを受けまして、
みんな大好きKEN_ALL(日本郵便のデータ)から、自分だけの郵便番号マスタを作成する方法を紹介させていただこうと思います。

【はじめに】必要な環境・準備

  • MySQL
  • PHP(5.6↑)が動作する環境

予めテーブルも作成しておきます。

#郵便番号の本番テーブル
CREATE TABLE `zipcode_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `zipcode` varchar(7) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
  `prefecture` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `prefecture_kana` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `prefecture_rome` varchar(20) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `city` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `city_kana` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `city_rome` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `street1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `street1_kana` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `street1_rome` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `street2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `street2_kana` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `street2_rome` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY (`zipcode`)
);

#郵便番号の一時テーブル
CREATE TABLE `pending_zipcode_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `zipcode` varchar(7) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `prefecture` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `prefecture_kana` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `prefecture_rome` varchar(20) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `prefecture_key` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `city` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `city_kana` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `city_rome` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `city_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `street1` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `street1_kana` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `street1_rome` text CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `street1_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `street2` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `street2_kana` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `street2_rome` text CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `street2_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `is_corp` tinyint(1) DEFAULT 0,
  PRIMARY KEY (`id`)
);

#都道府県のローマ字・カタカナデータを一時的に保存するテーブル
CREATE TABLE `pending_yomi_prefecture_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` tinyint(1) NOT NULL DEFAULT 1,
  `key` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `value` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`type`,`key`)
);

#市区町村のローマ字・カタカナデータを一時的に保存するテーブル
CREATE TABLE `pending_yomi_city_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pref_id` int(11) NOT NULL,
  `key` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `value` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`pref_id`,`key`)
);

#町域のローマ字データを一時的に保存するテーブル
CREATE TABLE `pending_yomi_street1_rome_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city_id` int(11) NOT NULL,
  `key` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `value` varchar(200) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`city_id`,`key`)
);

#町域のカタカナデータを保存するテーブル
CREATE TABLE `pending_yomi_street1_kana_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city_id` int(11) NOT NULL,
  `key` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `value` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`city_id`,`key`)
);

共通処理のみをまとめた common.php を作成しておきましょう。
これは各処理ファイルの冒頭で読み込みます。

※漢数字→半角数字の処理はこちらを参考にさせていただきました。ありがとうございます。
https://www.pahoo.org/e-soul/webtech/php03/php03-08-01.shtm

<?php
// データベースへ接続
$pdo = new PDO('mysql:dbname=zipcode_db;host=localhost;charset=utf8', 'root', 'admin');

// クエリの最大パケット数を取得
$stmt = $pdo->prepare("SHOW VARIABLES LIKE 'max_allowed_packet'");
$stmt->execute();
if($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $max_allowed_packet = $result['Value'];
} else {
    $max_allowed_packet = 16 * 1024 * 1024;
}
$max_allowed_packet -= 1000;

$zip_dir = './'; //ファイルを保存するディレクトリ
$zip_files_dir = './zip_files'; //ファイルを展開するディレクトリ

// 漢数字→半角数字(参考:https://www.pahoo.org/e-soul/webtech/php03/php03-08-01.shtm)
$ConvertKanjiNum = function($str) {
    $kanji_num_list = ['〇'=>0,'壱'=>1,'一'=>1,'弐'=>2,'二'=>2,'参'=>3,'三'=>3,'四'=>4,'五'=>5,'六'=>6,'七'=>7,'八'=>8,'九'=>9];
    $digit_list1 = ['万'=>10000,'億'=>100000000,'兆'=>1000000000000,'京'=>10000000000000000];
    $digit_list2 = ['十'=>10,'百'=>100,'千'=>1000];

    $current_value = '';
    $str_list = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
    $digit1 = 1;
    $digit2 = 1;
    $partial_sum = 0;
    $total_sum = 0;
    
    foreach(array_reverse($str_list) as $c) {
        if(isset($kanji_num_list[$c])) {
            $current_value = $kanji_num_list[$c].$current_value;
            continue;
        }

        if(isset($digit_list1[$c])) {
            if($current_value != '') {
                $partial_sum = $partial_sum + $current_value * $digit2;
            } else
            if($digit2 != 1) {
                $partial_summ = $partial_sum + $digit2;
            }
            $total_sum = $partial_sum * $digit1 + $total_sum;
            $partial_sum = 0;
            $current_value = '';
            $digit2 = 1;
            $digit1 = $digit_list1[$c];
            continue;
        }
        
        if(isset($digit_list2[$c])) {
            if($current_value != '') {
                $partial_sum = $partial_sum + $current_value * $digit2;
            } else
            if($digit2 != 1) {
                $partial_sum = $partial_sum + $digit2;
            }
            $current_value = '';
            $digit2 = $digit_list2[$c];
            continue;
        }
    };

    $zero_value = '';
    if(preg_match("/^(0+)/", $current_value, $matches)) {
        $zero_value = $matches[1];
    }

    if($current_value != '') {
        $partial_sum = $partial_sum + $current_value * $digit2;
    } else
    if($digit2 != 1) {
        $partial_sum = $partial_sum + $digit2;
    }
    $total_sum = $partial_sum * $digit1 + $total_sum;

    if($total_sum == 0) {
        return $zero_value;
    }

    return $zero_value.$total_sum;
};

// 漢字データとローマ字データを結合するためのキーを生成する関数
$CreateAddKey = function($str) use ($ConvertKanjiNum) {
    $str = str_replace([' ', ' '], '', $str);
    $str = mb_convert_kana($str, 'n', 'utf-8');
    if(preg_match_all("/[一二三四五六七八九〇十百千]+/u", $str, $matches)) {
        $replace_datas = [];
        $sorter = [];
        foreach($matches[0] as $kan_num) {
            $num = $ConvertKanjiNum($kan_num);
            $replace_list[] = [$kan_num, $num];
            $sorter[] = mb_strlen($kan_num, 'utf-8');
        }
        array_multisort($sorter, SORT_NUMERIC, SORT_DESC, $replace_list);
        foreach($replace_list as $replace_data) {
            $str = str_replace($replace_data[0], $replace_data[1], $str);
        }
    }

    return $str;
};

// まとめてINSERTするための関数
$InsertData = function($insert_query, $insert_prefix = '', $add_data = []) use ($max_allowed_packet, $pdo) {
    $add_query = '';
    if(! empty($add_data)) {
        $add_data = array_map(function($value) use ($pdo) {
            if($value == NULL) {
                return 'NULL';
            } else {
                return $pdo->quote($value);
            }
        }, $add_data);
    
        $add_query = '('.implode(',', $add_data).'),';
    }
    
    if($add_query == '' || strlen($insert_query.$add_query) >= $max_allowed_packet) {
        if($insert_query || substr(trim($insert_query), -6) != 'VALUES') {
            $insert_query = rtrim($insert_query, ',').";";
            $pdo->query($insert_query);
            $insert_query = $insert_prefix;
        }
    }
    
    return $insert_query.$add_query;
};

# 時間のかかる処理もあるため制限は解除
set_time_limit(0);
ini_set('memory_limit', -1);

これで準備は整いました!いざ実践!

【phase1】ローマ字データを用意

1. 日本郵便のサイトから全国一括のZIPをダウンロード

住所の郵便番号ページ(https://www.post.japanpost.jp/zipcode/dl/roman-zip.html)から、
全国一括(zip形式)のアドレスを確認しておきます。

<?php
require 'common.php';

// ZIPのアドレスから直接ダウンロード
$zip_url = 'https://www.post.japanpost.jp/zipcode/dl/roman/KEN_ALL_ROME.zip';
$ch = curl_init($zip_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
curl_setopt($ch, CURLOPT_AUTOREFERER, true); // リダイレクトを自動で辿ります
$zip_source = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($http_code == '404') {
    // 404だったらファイル名を大文字・小文字に変換してリトライ
    $info = pathinfo($zip_url);

    $check_file_list = [
        strtoupper($info['filename']).'.'.strtoupper($info['extension']),
        strtoupper($info['filename']).'.'.strtolower($info['extension']),
        strtolower($info['filename']).'.'.strtoupper($info['extension']),
        strtolower($info['filename']).'.'.strtolower($info['extension']),
    ];

    // 元のファイル名は除外
    $default_name = $info['basename'];
    $check_file_list = array_diff($check_file_list, [$default_name]); 

    foreach($check_file_list as $file_name) {
        $zip_url = $info['dirname'].'/'.$file_name;
        curl_setopt($ch, CURLOPT_URL, $zip_url);
        $zip_source = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if(strpos($http_code, '2') === 0) {
            break;
        }
    }
}
curl_close($ch);
if(! $zip_source) {
    throw new Exception("全国一括データのダウンロードに失敗しました。");
}

2. 全国一括のZIPを解凍して CSV を取り出す

$csv_file_name = 'KEN_ALL_ROME.CSV';

$zip_filename = basename($zip_url);
$zip_filepath = $zip_dir.'/'.$zip_filename;
file_put_contents($zip_filepath, $zip_source);

$zip = new ZipArchive;
if($zip->open($zip_filepath)) {
   $zip->extractTo($zip_files_dir);
}

// ファイルが小文字の場合もあるので、あらかじめチェックするファイル名のリストを作成
$info = pathinfo($csv_file_name);
$check_file_list = [
    $csv_file_name,
    strtolower($info['filename']).'.'.strtolower($info['extension']),
    strtoupper($info['filename']).'.'.strtoupper($info['extension']),
    strtoupper($info['filename']).'.'.strtolower($info['extension']),
    strtolower($info['filename']).'.'.strtoupper($info['extension']),
];
$check_file_list = array_unique($check_file_list);

$is_found = false;
foreach($check_file_list as $file_name) {
    if(file_exists($zip_files_dir.'/'.$file_name)) {
        $csv_file_name = $file_name;
        $is_found = true;
        break;
    }
}
if(! $is_found) {
    throw new Exception('全国一括データのCSVが見つかりませんでした。');
}

3. 全国一括のCSVから連想配列を生成

全国一括データの説明(https://www.post.japanpost.jp/zipcode/dl/readme_ro.html)から、
1. 郵便番号(7桁)
2. 都道府県名
3. 市区町村名
4. 町域名
5. 都道府県名
6. 市区町村名
7. 町域名
の順に配列しているとのことがわかるので、キーのみの配列を作成して読み込み

$csv_key_list = [
    '郵便番号',
    '都道府県名 漢字',
    '市区町村名 漢字',
    '町域名 漢字',
    '都道府県名 ローマ字',
    '市区町村名 ローマ字',
    '町域名 ローマ字',
];

// 文字コードをUTF-8に変換して保存
$csv_file_path = $zip_files_dir.'/'.$csv_file_name;
$source = file_get_contents($csv_file_path);
$source = mb_convert_encoding($source, 'utf-8', 'sjis'); 
file_put_contents($csv_file_path, $source);

$ken_all_rome_data = [];
$fp =  fopen($csv_file_path, 'r');
if($fp) {
    $key_cnt = count($csv_key_list);

    while(($data = fgetcsv($fp)) !== false) {
        if(count($data) == $key_cnt) {
            // キーの数が一致する行のみ処理
            $ken_all_rome_data[] = array_combine($csv_key_list, $data);
        }
    }
    fclose($fp);
}

4. 各読みがなテーブルにデータを保存

// INSERT文の冒頭部分
$insert_query_prefix_yomi_prefecture = "INSERT IGNORE INTO `pending_yomi_prefecture_tbl`(`id`, `type`, `key`, `value`) VALUES ";
$insert_query_prefix_yomi_city = "INSERT IGNORE INTO `pending_yomi_city_tbl`(`id`, `pref_id`, `key`, `value`) VALUES ";
$insert_query_prefix_yomi_street1_rome = "INSERT IGNORE INTO `pending_yomi_street1_rome_tbl`(`id`, `city_id`, `key`, `value`) VALUES ";

$pref_id = 0; // 都道府県の id
$city_id = 0; // 市区町村の id
$street1_id = 0; // 町域の id
$cache_pref = null;
$cache_city = null;
$cache_street1 = null;
$insert_query_yomi_prefecture = $insert_query_prefix_yomi_prefecture;
$insert_query_yomi_city = $insert_query_prefix_yomi_city;
$insert_query_yomi_street1_rome = $insert_query_prefix_yomi_street1_rome;

reset($ken_all_rome_data);
do {
    $data = current($ken_all_rome_data);
    if(! $data) {
        // 最後のデータを処理
        $InsertData($insert_query_yomi_prefecture);
        $InsertData($insert_query_yomi_city);
        $InsertData($insert_query_yomi_street1_rome);
        break;
    }

    $next_data = next($ken_all_rome_data);
    
    $key = $CreateAddKey($data["都道府県名 漢字"]);
    if($cache_pref != $key) {
        ++$pref_id;
        $value = $data["都道府県名 ローマ字"];
        $value = strtolower($value);
        $value = ucfirst($value);
        $add_data = [
            $pref_id,
            1, // 1:ローマ字 2:カタカナ
            $key,
            $value,
        ];
        $insert_query_yomi_prefecture = $InsertData($insert_query_yomi_prefecture, $insert_query_prefix_yomi_prefecture, $add_data);
        $cache_pref = $key;
    }

    $key = $CreateAddKey($data["市区町村名 漢字"]);
    if($cache_city != $key) {
        ++$city_id;
        $value = $data["市区町村名 ローマ字"];
        $value = strtolower($value);
        $value = ucfirst($value);
        $add_data = [
            $city_id,
            $pref_id,
            $key,
            $value,
        ];
        $insert_query_yomi_city = $InsertData($insert_query_yomi_city, $insert_query_prefix_yomi_city, $add_data);
        $cache_city = $key;
    }

    $street1 = $data['町域名 漢字'];
    $street1_rome = $data['町域名 ローマ字'];
    $street1_key = '';
    if(in_array($street1, ['以下に記載がない場合'])) {
        // 何もしない
        $street1 = '';
        $street1_rome = '';
    } else
    if(strpos($street1, '(') !== false && strpos($street1, ')') !== false) {
        // 閉じ括弧が無い場合
        if(strpos($next_data['町域名 漢字'], '(') === false && strpos($next_data['町域名 漢字'], ')') !== false) {
            // 次のレコードを連結
            $street1 .= $next_data['町域名 漢字'];
            $street1_rome .= $next_data['町域名 ローマ字'];
            next($ken_all_rome_data);
        }
    }

    // 括弧以降は削除
    $street1 = explode('(', $street1)[0]; 
    $street1_rome = explode('(', $street1_rome)[0];
    $street1_key = $CreateAddKey($street1);

    if($street1_rome) {
        $value = $street1_rome;
        $value = strtolower($value);
        $value = ucfirst($value);
        $key = $street1_key;
        if($cache_street1 != $key) {
            ++$street1_id;
            $add_data = [
                $street1_id,
                $city_id,
                $key,
                $value,
            ];
            $insert_query_yomi_street1_rome = $InsertData($insert_query_yomi_street1_rome, $insert_query_prefix_yomi_street1_rome, $add_data);
            $cache_street1 = $key;
        }
    }
} while(1);

上記のコードを全て記述したphase1.phpを実行することで、ローマ字データは完成です。

【phase2】郵便番号の一時データを用意

1. 日本郵便のサイトから住所の郵便番号のZIPをダウンロード

住所の郵便番号(1レコード1行、UTF-8形式)(CSV形式)ページ(https://www.post.japanpost.jp/zipcode/dl/utf-zip.html)から、
最新データのダウンロード(zip形式)のアドレスを確認しておきます。

<?php
require 'common.php';

// ZIPのアドレスから直接ダウンロード
$zip_url = 'https://www.post.japanpost.jp/zipcode/dl/utf/zip/utf_ken_all.zip';
$ch = curl_init($zip_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
curl_setopt($ch, CURLOPT_AUTOREFERER, true); // リダイレクトを自動で辿ります
$zip_source = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($http_code == '404') {
    // 404だったらファイル名を大文字・小文字に変換してリトライ
    $info = pathinfo($zip_url);

    $check_file_list = [
        strtoupper($info['filename']).'.'.strtoupper($info['extension']),
        strtoupper($info['filename']).'.'.strtolower($info['extension']),
        strtolower($info['filename']).'.'.strtoupper($info['extension']),
        strtolower($info['filename']).'.'.strtolower($info['extension']),
    ];

    // 元のファイル名は除外
    $default_name = $info['basename'];
    $check_file_list = array_diff($check_file_list, [$default_name]); 

    foreach($check_file_list as $file_name) {
        $zip_url = $info['dirname'].'/'.$file_name;
        curl_setopt($ch, CURLOPT_URL, $zip_url);
        $zip_source = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if(strpos($http_code, '2') === 0) {
            break;
        }
    }
}
curl_close($ch);
if(! $zip_source) {
    throw new Exception("住所の郵便番号データのダウンロードに失敗しました。");
}

2. 住所の郵便番号のZIPを解凍して CSV を取り出す

$csv_file_name = 'utf_ken_all.csv';

$zip_filename = basename($zip_url);
$zip_filepath = $zip_dir.'/'.$zip_filename;
file_put_contents($zip_filepath, $zip_source);

$zip = new ZipArchive;
if($zip->open($zip_filepath)) {
   $zip->extractTo($zip_files_dir);
}

// ファイルが小文字の場合もあるので、あらかじめチェックするファイル名のリストを作成
$info = pathinfo($csv_file_name);
$check_file_list = [
    $csv_file_name,
    strtolower($info['filename']).'.'.strtolower($info['extension']),
    strtoupper($info['filename']).'.'.strtoupper($info['extension']),
    strtoupper($info['filename']).'.'.strtolower($info['extension']),
    strtolower($info['filename']).'.'.strtoupper($info['extension']),
];
$check_file_list = array_unique($check_file_list);

$is_found = false;
foreach($check_file_list as $file_name) {
    if(file_exists($zip_files_dir.'/'.$file_name)) {
        $csv_file_name = $file_name;
        $is_found = true;
        break;
    }
}
if(! $is_found) {
    throw new Exception('住所の郵便番号データのCSVが見つかりませんでした。');
}

3. 住所の郵便番号データのCSVから連想配列を生成

郵便番号データ(1レコード1行、UTF-8形式)の説明(https://www.post.japanpost.jp/zipcode/dl/utf-readme.html)を元に、キーのみの配列を作成して読み込み

$csv_key_list = [
    '全国地方公共団体コード',
    '旧郵便番号',
    '郵便番号',
    '都道府県名 カタカナ',
    '市区町村名 カタカナ',
    '町域名 カタカナ',
    '都道府県名 漢字',
    '市区町村名 漢字',
    '町域名 漢字',
    '一町域が二以上の郵便番号で表される',
    '小字毎に番地が起番されている町域',
    '丁目を有する町域',
    '一つの郵便番号で二以上の町域を表す',
    '更新の表示',
    '変更理由',
];

$csv_file_path = $zip_files_dir.'/'.$csv_file_name;

$ken_all_kana_data = [];
$fp =  fopen($csv_file_path, 'r');
if($fp) {
    $key_cnt = count($csv_key_list);

    while(($data = fgetcsv($fp)) !== false) {
        if(count($data) == $key_cnt) {
            // キーの数が一致する行のみ処理
            $ken_all_kana_data[] = array_combine($csv_key_list, $data);
        }
    }
    fclose($fp);
}

4. 一時テーブルへ保存

// テーブルを空にする
$pdo->query('TRUNCATE TABLE `pending_zipcode_tbl`');

// INSERT文の冒頭部分
$insert_query_prefix_ken_all = "INSERT INTO `pending_zipcode_tbl`(`zipcode`, `prefecture`, `prefecture_kana`, `prefecture_key`, `city`, `city_kana`, `city_key`, `street1`, `street1_kana`, `street1_key`) VALUES ";
$insert_query_ken_all = $insert_query_prefix_ken_all;

reset($ken_all_kana_data);
do {
    $data = current($ken_all_kana_data);
    if(! $data) {
        $InsertData($insert_query_ken_all);
        break;
    }

    next($ken_all_kana_data);

    $zipcode = trim($data['郵便番号']);
    
    $prefecture = $data["都道府県名 漢字"];
    $prefecture_kana = mb_convert_kana($data["都道府県名 カタカナ"], 'kas', 'utf-8');
    $prefecture_key = $CreateAddKey($prefecture);

    $city = $data["市区町村名 漢字"];
    $city_kana = mb_convert_kana($data["市区町村名 カタカナ"], 'kas', 'utf-8');
    $city_key = $CreateAddKey($city);

    $street1 = $data["町域名 漢字"];

    if(mb_substr($street1, -2, 2, 'utf-8') == '場合') {
        // 最後が「場合」の場合は空にする
        $street1 = null;
        $street1_kana = null;
        $street1_key = null;
    } else {
        $street1_kana = mb_convert_kana($data["町域名 カタカナ"], 'kas', 'utf-8');
		
		// 括弧以降の文字を削除
        $street1 = explode('(', $street1)[0];
		$street1_kana = explode('(', $street1_kana)[0];
		$street1_key = $CreateAddKey($street1);
    }

    $add_data = [
        $zipcode,
        $prefecture,
        $prefecture_kana,
        $prefecture_key,
        $city,
        $city_kana,
        $city_key,
        $street1 ? $street1 : NULL,
        $street1_kana ? $street1_kana : NULL,
        $street1_key ? $street1_key : NULL,
    ];
    $insert_query_ken_all = $InsertData($insert_query_ken_all, $insert_query_prefix_ken_all, $add_data);

} while(1);

上記のコードを全て記述したphase2.phpを実行することで、土台となるデータは完成しました!

【phase3】読みがなテーブルへカタカナデータをコピー

<?php
require 'common.php';

// 漢字・読み仮名データから都道府県のカタカナ読みデータを追加
$query = "INSERT IGNORE INTO `pending_yomi_prefecture_tbl`(`type`, `key`, `value`) 
    SELECT DISTINCT 2 AS `type`, `prefecture_key` AS `key`, `prefecture_kana` AS `value` 
        FROM `pending_zipcode_tbl`;";
$pdo->query($query);

// 漢字・読み仮名データから市区町村のカタカナ読みデータを追加
$query = "INSERT IGNORE INTO `pending_yomi_city_tbl`(`pref_id`, `key`, `value`) 
    SELECT DISTINCT P.`id` AS `pref_id`, MAIN.`city_key` AS `key`, MAIN.`city_kana` AS `value` 
    FROM (
        SELECT DISTINCT `prefecture_key`, `city_key`, `city_kana` 
        FROM `pending_zipcode_tbl` 
        WHERE `city_kana` IS NOT NULL AND `city_kana` != ''
    ) AS MAIN
    INNER JOIN `pending_yomi_prefecture_tbl` AS P 
    ON P.`type` = 2 AND P.`key` = MAIN.`prefecture_key`;";
$pdo->query($query);

// 漢字・読み仮名データから町域のカタカナ読みデータを追加
$query = "INSERT IGNORE INTO `pending_yomi_street1_kana_tbl`(`city_id`, `key`, `value`) 
    SELECT DISTINCT
    C.`id` AS `city_id`
    ,MAIN.`street1_key` AS `key`,
    MAIN.`street1_kana` AS `value`
    FROM (
        SELECT DISTINCT `prefecture_key`, `city_key`, `street1_key`, `street1_kana` 
        FROM `pending_zipcode_tbl` 
        WHERE `street1_kana` IS NOT NULL AND `street1_kana` != ''
    ) AS MAIN
    INNER JOIN `pending_yomi_prefecture_tbl` AS P
    ON P.`type` = 2 AND P.`key` = MAIN.`prefecture_key`
    INNER JOIN `pending_yomi_city_tbl` AS C
    ON C.`pref_id` = P.`id` AND C.`key` = MAIN.`city_key`;";
$pdo->query($query);

上記のコードを全て記述したphase3.phpを実行することで、読みがなデータも完成しました。

【phase4】事業所の個別郵便番号データを保存

1. 日本郵便のサイトから事業所の個別郵便番号データのZIPをダウンロード

事業所の個別郵便番号データダウンロード (zip形式) ページ(https://www.post.japanpost.jp/zipcode/dl/jigyosyo/index-zip.html)から、
最新データのダウンロード(zip形式)のアドレスを確認しておきます。

<?php
require 'common.php';

// ZIPのアドレスから直接ダウンロード
$zip_url = 'https://www.post.japanpost.jp/zipcode/dl/jigyosyo/zip/jigyosyo.zip';
$ch = curl_init($zip_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
curl_setopt($ch, CURLOPT_AUTOREFERER, true); // リダイレクトを自動で辿ります
$zip_source = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($http_code == '404') {
    // 404だったらファイル名を大文字・小文字に変換してリトライ
    $info = pathinfo($zip_url);

    $check_file_list = [
        strtoupper($info['filename']).'.'.strtoupper($info['extension']),
        strtoupper($info['filename']).'.'.strtolower($info['extension']),
        strtolower($info['filename']).'.'.strtoupper($info['extension']),
        strtolower($info['filename']).'.'.strtolower($info['extension']),
    ];

    // 元のファイル名は除外
    $default_name = $info['basename'];
    $check_file_list = array_diff($check_file_list, [$default_name]); 

    foreach($check_file_list as $file_name) {
        $zip_url = $info['dirname'].'/'.$file_name;
        curl_setopt($ch, CURLOPT_URL, $zip_url);
        $zip_source = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if(strpos($http_code, '2') === 0) {
            break;
        }
    }
}
curl_close($ch);
if(! $zip_source) {
    throw new Exception("事業所データのダウンロードに失敗しました。");
}

2. 事業所の個別郵便番号のZIPを解凍して CSV を取り出す

$csv_file_name = 'JIGYOSYO.CSV';

$zip_filename = basename($zip_url);
$zip_filepath = $zip_dir.'/'.$zip_filename;
file_put_contents($zip_filepath, $zip_source);

$zip = new ZipArchive;
if($zip->open($zip_filepath)) {
   $zip->extractTo($zip_files_dir);
}

// ファイルが小文字の場合もあるので、あらかじめチェックするファイル名のリストを作成
$info = pathinfo($csv_file_name);
$check_file_list = [
    $csv_file_name,
    strtolower($info['filename']).'.'.strtolower($info['extension']),
    strtoupper($info['filename']).'.'.strtoupper($info['extension']),
    strtoupper($info['filename']).'.'.strtolower($info['extension']),
    strtolower($info['filename']).'.'.strtoupper($info['extension']),
];
$check_file_list = array_unique($check_file_list);

$is_found = false;
foreach($check_file_list as $file_name) {
    if(file_exists($zip_files_dir.'/'.$file_name)) {
        $csv_file_name = $file_name;
        $is_found = true;
        break;
    }
}
if(! $is_found) {
    throw new Exception('事業所データのCSVが見つかりませんでした。');
}

3. 事業所の個別郵便番号のCSVから連想配列を生成

郵便番号データ(1レコード1行、UTF-8形式)の説明(https://www.post.japanpost.jp/zipcode/dl/jigyosyo/readme.html)を元に、キーのみの配列を作成して読み込み

$csv_key_list = [
	'大口事業所の所在地のJISコード',
	'大口事業所名(カナ)',
	'大口事業所名(漢字)',
	'都道府県名(漢字)',
	'市区町村名(漢字)',
	'町域名(漢字)',
	'小字名、丁目、番地等(漢字)',
	'大口事業所個別番号',
	'旧郵便番号',
	'取扱局(漢字)',
	'個別番号の種別の表示',
	'複数番号の有無',
	'修正コード',
];

// 文字コードをUTF-8に変換して保存
$csv_file_path = $zip_files_dir.'/'.$csv_file_name;
$source = file_get_contents($csv_file_path);
$source = mb_convert_encoding($source, 'utf-8', 'sjis'); 
file_put_contents($csv_file_path, $source);

$jigyosyo_data = [];
$fp =  fopen($csv_file_path, 'r');
if($fp) {
    $key_cnt = count($csv_key_list);

    while(($data = fgetcsv($fp)) !== false) {
        if(count($data) == $key_cnt) {
            // キーの数が一致する行のみ処理
            $jigyosyo_data[] = array_combine($csv_key_list, $data);
        }
    }
    fclose($fp);
}

4. 一時テーブル(pending_zipcode_tbl)へ追加

// INSERT文の冒頭部分
$insert_query_prefix_ken_all = "INSERT INTO `pending_zipcode_tbl`(`zipcode`, `prefecture`, `prefecture_key`, `city`, `city_key`, `street1`, `street1_key`, `street2`, `street2_kana`, `street2_key`, `is_corp`) VALUES ";
$insert_query_ken_all = $insert_query_prefix_ken_all;

reset($jigyosyo_data);
do {
    $data = current($jigyosyo_data);
    if(! $data) {
        $InsertData($insert_query_ken_all);
        break;
    }

    next($jigyosyo_data);

    $zipcode = trim($data['大口事業所個別番号']);

    $prefecture = $data["都道府県名(漢字)"];
    $prefecture_key = $CreateAddKey($prefecture);

    $city = $data["市区町村名(漢字)"];
    $city_key = $CreateAddKey($city);

    $street1 = $data["町域名(漢字)"];
    $street1_key = $CreateAddKey($street1);

    $street2 = implode(' ', array_filter([$data['小字名、丁目、番地等(漢字)'], $data['大口事業所名(漢字)']], 'strlen'));
    $street2_kana = null;
    $conv_street = mb_convert_kana($data['小字名、丁目、番地等(漢字)'], 'as', 'utf-8');
    if(is_numeric($conv_street)) {
        $street2_kana = implode(' ', array_filter([$conv_street, $data['大口事業所名(カナ)']], 'strlen'));
    }

    $street2_key = $CreateAddKey($data['小字名、丁目、番地等(漢字)']);

    $is_corp = 1;
    $add_data = [
        $zipcode,
        $prefecture,
        $prefecture_key,
        $city,
        $city_key,
        $street1,
        $street1_key,
        $street2,
        $street2_kana ? $street2_kana : NULL,
        $street2_key ? $street2_key : NULL,
        $is_corp,
    ];
    $insert_query_ken_all = $InsertData($insert_query_ken_all, $insert_query_prefix_ken_all, $add_data);

} while(1);

上記のコードを全て記述したphase4.phpを実行することで、事業所のデータの追加も完了しました。

【phase5】一時テーブルの読みがなを更新

<?php
require 'common.php';

// 都道府県(ローマ字)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key` FROM `pending_zipcode_tbl`
            WHERE `prefecture_key` IS NOT NULL AND `prefecture_rome` IS NULL
            LIMIT {$limit}
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 1 AND P.`key` = SUB.`prefecture_key`
        SET MAIN.`prefecture_rome` = IFNULL(P.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);

// 都道府県(カタカナ)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key` FROM `pending_zipcode_tbl`
            WHERE `prefecture_key` IS NOT NULL AND `prefecture_kana` IS NULL
            LIMIT {$limit}
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 2 AND P.`key` = SUB.`prefecture_key`
        SET MAIN.`prefecture_kana` = IFNULL(P.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);

// 市区町村(ローマ字)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key`, `city_key` FROM `pending_zipcode_tbl`
            WHERE `city_key` IS NOT NULL AND `city_rome` IS NULL
            LIMIT {$limit}
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 1 AND P.`key` = SUB.`prefecture_key`
        LEFT JOIN `pending_yomi_city_tbl` AS C
        ON C.`pref_id` = P.`id` AND C.`key` = SUB.`city_key`
        SET MAIN.`city_rome` = IFNULL(C.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);
    
// 市区町村(カタカナ)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key`, `city_key` FROM `pending_zipcode_tbl`
            WHERE `city_key` IS NOT NULL AND `city_kana` IS NULL
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 2 AND P.`key` = SUB.`prefecture_key`
        LEFT JOIN `pending_yomi_city_tbl` AS C
        ON C.`pref_id` = P.`id` AND C.`key` = SUB.`city_key`
        SET MAIN.`city_kana` = IFNULL(C.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);


// 町域名(ローマ字)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key`, `city_key`, `street1_key` FROM `pending_zipcode_tbl`
            WHERE `street1_key` IS NOT NULL AND `street1_rome` IS NULL
            LIMIT {$limit}
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 1 AND P.`key` = SUB.`prefecture_key`
        LEFT JOIN `pending_yomi_city_tbl` AS C
        ON C.`pref_id` = P.`id` AND C.`key` = SUB.`city_key`
        LEFT JOIN `pending_yomi_street1_rome_tbl` AS STREET1
        ON STREET1.`city_id` = C.`id` AND STREET1.`key` = SUB.`street1_key`
        SET MAIN.`street1_rome` = IFNULL(STREET1.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);

// 町域名(カタカナ)を更新
$limit = 10000;
do {
    $query = "UPDATE `pending_zipcode_tbl` AS MAIN
        INNER JOIN (
            SELECT `id`, `prefecture_key`, `city_key`, `street1_key` FROM `pending_zipcode_tbl`
            WHERE `street1_key` IS NOT NULL AND `street1_kana` IS NULL
            LIMIT {$limit}
        ) AS SUB
        ON MAIN.`id` = SUB.`id`
        LEFT JOIN `pending_yomi_prefecture_tbl` AS P
        ON P.`type` = 2 AND P.`key` = SUB.`prefecture_key`
        LEFT JOIN `pending_yomi_city_tbl` AS C
        ON C.`pref_id` = P.`id` AND C.`key` = SUB.`city_key`
        LEFT JOIN `pending_yomi_street1_kana_tbl` AS STREET1
        ON STREET1.`city_id` = C.`id` AND STREET1.`key` = SUB.`street1_key`
        SET MAIN.`street1_kana` = IFNULL(STREET1.`value`, '');";
    $result = $pdo->query($query);
    
    $changed_count = $result->rowCount();
    if($changed_count == 0) {
        break;
    }
} while(1);

上記のコードを全て記述したphase5.phpを実行することで、
一時テーブルの読み情報が不十分なレコードが更新されます。

【phase6】本番テーブルへデータをコピー

<?php
require 'common.php';

// テーブルを空にする
$pdo->query('TRUNCATE TABLE `zipcode_tbl`');

$query = "INSERT INTO `zipcode_tbl`(`zipcode`,`prefecture`,`prefecture_kana`,`prefecture_rome`,`city`,`city_kana`,`city_rome`,`street1`,`street1_kana`,`street1_rome`,`street2`,`street2_kana`,`street2_rome`)
  SELECT `zipcode`,`prefecture`,`prefecture_kana`,`prefecture_rome`,`city`,`city_kana`,`city_rome`,`street1`,`street1_kana`,`street1_rome`,`street2`,`street2_kana`,`street2_rome` 
  FROM `pending_zipcode_tbl`;";
$pdo->query($query);

上記のコードを全て記述したphase6.phpを実行することで、
必要な情報のみ本番テーブルへコピーされます。

終わりに

以上で自分だけの郵便番号マスタが完成しました!
かなり負荷のかかる処理で、場合よらなくても怒られる可能性があるので、
ローカル環境で実行して、本番テーブル(zipcode_tbl)のみコピーするといいかもしれないですね。

ブログの著者欄

新垣 万葉

GMOインターネットグループ株式会社

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック