[PHP][JS, Ajax]郵便番号を住所に変換する

会員登録時などで郵便番号を入力すると住所が自動入力される仕組みを作る場合、
郵便番号と実際の住所との対応表を利用します。
データは日本郵便が公開しており、CSV 形式で入手することができます。

郵便番号データダウンロード
http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

上記サイトから、「全国一括」というリンクをクリックし、ダウンロードした圧縮ファイルから「KEN_ALL.CSV」ファイルを取り出しておきます。

そのまま使用できればいいのですが、CSV の書き方に癖があり、長い住所を2行にまたいで書いてあったり、「以下に掲載がない場合」という文字や「大通西(1~19丁目)」のようになっているので一旦変換します。
また、全体で12万行以上あるため変換の際に郵便番号の1桁目をもとに10個のファイル(0.csv〜9.csv)に分割して保存します。あらかじめ「zipcode」というフォルダを用意し、書き込み可能なパーミッションに設定しておいて下さい。

convert.php

<?php
// メモリ上限を変更(必要に応じて)
ini_set('memory_limit', '256M');

// KEN_ALL.CSV の場所
$file = 'KEN_ALL.CSV';

// 変換後のファイル保存先
$dir = __DIR__ . '/zipcode';

setlocale(LC_ALL, 'ja_JP.UTF-8');

$data = file_get_contents($file);
$data = mb_convert_encoding($data, 'UTF-8', 'sjis-win');
$temp = tmpfile();

fwrite($temp, $data);
rewind($temp);

$groups = array();
$groupIndexes = array();
for($i=0;$i<10;$i++){
    $groupIndexes[$i] = 0;
}

$prev = null;
$index = 0;
while (($data = fgetcsv($temp, 0, ",")) !== FALSE) {
    $prefix = substr($data[2], 0, 1);
    if(!strlen($prefix)){
        $index++;
        continue;
    }
    $groupIndex = $groupIndexes[$prefix];

    $columns = array(
        $data[2],
        $data[6],
        $data[7],
        ($data[8] == '以下に掲載がない場合') ?
            '' : preg_replace('/(.+?)/u', '', $data[8])
    );

    if(!is_null($prev) && $prev[0] == $columns[0]){
        $groups[$prefix][$groupIndex - 1][3] .= $columns[3];
    } else {
        $groups[$prefix][$groupIndex] = $columns;
        $index++;
        $groupIndexes[$prefix]++;
    }
    $prev = $columns;
}
fclose($temp);

foreach($groups as $prefix => $group){
    $converted = fopen($dir . DIRECTORY_SEPARATOR . $prefix . '.csv', "w");
    if(flock($converted, LOCK_EX)){
        foreach($group as $columns){
            // ダブルクォート
            $columns = array_map(function($value){
                $value = str_replace('"', '""', $value);
                return '"' . $value . '"';
            }, $columns);
            fwrite($converted, implode(',', $columns) . "\n");
        }
        flock($converted, LOCK_UN);
    }
    fclose($converted);
}

echo "Completed.";

zipcode

変換が完了すると zipcode フォルダに 0.csv 〜 9.csv というファイルが出来ます。この変換では文字コードを UTF-8 にし、複数行にわかれた行を1行にまとめ、丸括弧の部分を取り除いた上で最低限必要な項目だけに絞り込んであります。


◆PHP のみで判別する場合

<?php
//郵便番号
$zipcode = '103-0027';

$dir = __DIR__ . '/zipcode';

$zipcode = mb_convert_kana($zipcode, 'a', 'utf-8');
$zipcode = str_replace(array('-','ー'),'', $zipcode);

$result = array();

$file = $dir . DIRECTORY_SEPARATOR . substr($zipcode, 0, 1) . '.csv';
if(file_exists($file)){
    $spl = new SplFileObject($file);
    while (!$spl->eof()) {
        $columns = $spl->fgetcsv();
        if(isset($columns[0]) && $columns[0] == $zipcode){
            $result = array($columns[1], $columns[2], $columns[3]);
            break;
        }
    }
}

if(!empty($result)){
    echo $result[0] . $result[1] . $result[2];
} else {
    echo 'Not Found';
}

出力結果:

東京都中央区日本橋

結果には3つのデータが配列として渡されます。それぞれ「県名」「市区」「町村」の順です。


◆HTML フォームから Javascript(jQuery) の Ajax を使って取得する場合

フォームから利用する場合は、jQuery の ajax() を用いて PHP の API に郵便番号を送り、得られた結果を住所欄に書き込みます。

form.html

<!DOCTYPE html>
<html>
<head>
<title>Zipcode Sample</title>
<meta charset="utf-8">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>

<script type="text/javascript">
$(document).ready(function(){
    $("#lookup").click(function(){
        var zip1 = $.trim($('#zip1').val());
        var zip2 = $.trim($('#zip2').val());
        var zipcode = zip1 + zip2;

        $.ajax({
            type: "post",
            url: "api.php",
            data: JSON.stringify(zipcode),
            crossDomain: false,
            dataType : "jsonp",
            scriptCharset: 'utf-8'
        }).done(function(data){
            if(data[0] == ""){
                alert('見つかりませんでした。');
            } else {
                $('#address').val(data[0] + data[1] + data[2]);
            }
        }).fail(function(XMLHttpRequest, textStatus, errorThrown){
            alert(errorThrown);
        });
     });
});
</script>
</head>
<body>
<form>
    <p><input type="text" name="zip1" id="zip1" size="6">-<input type="text" name="zip2" id="zip2" size="6">
    <input type="button" id="lookup" value="Lookup address"></p>
    <p><input size="50" type="text" name="address" id="address"></p>
</form>
</body>
</html>

「Lookup address」ボタンを押すと郵便番号が api.php に伝えられ、見つかった場合は配列の JSON データを、見つからなかった場合は空文字の配列データを返します。

api.php

<?php
$dir = __DIR__ . '/zipcode';

// Ajax以外からのアクセスを遮断
$request = (string)filter_input(INPUT_SERVER, 'HTTP_X_REQUESTED_WITH');
if(strtolower($request) !== 'xmlhttprequest') exit;

$json = file_get_contents('php://input');
$data = json_decode($json, true);
file_put_contents('test.log', print_r($data, true));
$zipcode = !empty($data) ? $data : '';
$zipcode = mb_convert_kana($zipcode, 'a', 'utf-8');
$zipcode = preg_replace('/[\sー-]/', '', $zipcode);

$callback  = (string)filter_input(INPUT_GET, 'callback');
$callback  = htmlspecialchars(strip_tags($callback));

$param = array('', '', '');

$file = $dir . DIRECTORY_SEPARATOR . substr($zipcode, 0, 1) . '.csv';
if(file_exists($file)){
    $spl = new SplFileObject($file);
    while (!$spl->eof()) {
        $columns = $spl->fgetcsv();
        if(isset($columns[0]) && $columns[0] == $zipcode){
            $param = array($columns[1], $columns[2], $columns[3]);
            break;
        }
    }
}

header('Content-type: application/javascript; charset=utf-8');
printf("{$callback}(%s)", json_encode( $param ));

[PHP]ある曜日の第n週は何日か調べる(国民の祝日)

ハッピーマンデー制度により、一部の日本の祝日は月日固定ではなく、曜日で固定して連休になるようにつくられています。

2012年12月現在、月曜日に移動した国民の祝日は以下の4つです

・成人の日(1月の第2月曜日) ・海の日(7月の第3月曜日) ・敬老の日(9月の第3月曜日) ・体育の日(10月の第2月曜日)

たとえば、成人の日は、1月の第2月曜日で、それが何日にあたるかはその年によって異なります。

PHP でこれを調べるには、ある曜日の第1週が何日かを計算し、何週目かによって 7 を足していきます。

<?php
//年
$year = 2038;
 
//名前, 月, 週, 曜日(0~6)の順
$holiday = array(
    array("成人の日", 1, 2, 1),
    array("海の日", 7, 3, 1),
    array("敬老の日", 9, 3, 1),
    array("体育の日", 10, 2, 1)
);

$datetime = new DateTime();
$datetime->setTimezone( new DateTimeZone('Asia/Tokyo') );
 
foreach($holiday as $value){
	list($name, $month, $week, $wday) = $value;
 
    //その月の始まりは何曜日か
	$datetime->setDate($year, $month, 1);
    $w = (int)$datetime->format('w');
 
    //指定された曜日の最初の日
    $first = ($wday - $w >= 0) ? 1 + $wday - $w : 1 + $wday - $w + 7;
 
    //日にちを算出
    $day  = $first + ( 7 * ($week - 1) );
	$datetime->setDate($year, $month, $day);
 
    echo $name . ': ' . $datetime->format('Y-m-d') . "\n";
}

出力結果

成人の日: 2038-01-11
海の日: 2038-07-19
敬老の日: 2038-09-20
体育の日: 2038-10-11

$holiday には「名前, 月, 週, 曜日」の順で指定します。曜日は日曜日が 0 で土曜が 6 です。

[PHP]ある曜日に該当する日だけを表示する

一年の中で、土日のみ抽出して表示したい時などは DateInterval を一日間隔にセットし、曜日をチェックすれば簡単です。
閏年を考慮した日数分ループさせるか、年が変わったらループを抜けるように設計するのがいいと思います。

こちらのサンプルでは配列として日曜(0)と土曜(6)を指定し、該当する日にちを表示します。

<?php
$year   = 2038;
$target = array(0, 6);		// 日曜と土曜

$datetime = new DateTime();
$datetime->setTimezone( new DateTimeZone('Asia/Tokyo') );
$datetime->setDate($year, 1, 1);

// 間隔
$interval =  new DateInterval('P1D');

// 閏年をチェック
$days = $datetime->format('L') == '1' ? 366 : 365;

$result = array();

for($i=0;$i<$days;$i++){
	if( in_array( (int)$datetime->format('w'), $target) ){
		$result[] = clone $datetime;
	}
	$datetime->add($interval);
}

foreach($result as $value){
	echo $value->format("Y-m-d l") . "\n";
}

実行結果

2038-01-02 Saturday
2038-01-03 Sunday
2038-01-09 Saturday
2038-01-10 Sunday
2038-01-16 Saturday
2038-01-17 Sunday
2038-01-23 Saturday
2038-01-24 Sunday
2038-01-30 Saturday
2038-01-31 Sunday
2038-02-06 Saturday
2038-02-07 Sunday
2038-02-13 Saturday
2038-02-14 Sunday
(以下略)