Skip to content

Instantly share code, notes, and snippets.

@hirayama-evolni
Last active August 29, 2015 14:23
Show Gist options
  • Save hirayama-evolni/aeba5ade0a502c7110df to your computer and use it in GitHub Desktop.
Save hirayama-evolni/aeba5ade0a502c7110df to your computer and use it in GitHub Desktop.
郵便番号検索ライブラリ
<?php
/*
* 郵便番号検索ライブラリ
* (C) EVOLNI
* (C) Wataru Hirayama
* License: MIT
*
* Version: 1.0.0
*
* 日本郵便の郵便番号データ以外のものに依存しない
* セルフホスティングの郵便番号検索を実現するライブラリです。
*
* Pros:
* ・データをローカルに持つので外部に依存しない
* ・cross domainフリー
* ・データ形式が単純なので(たぶん)比較的早い
*
* Cons:
* ・データをローカルに持つので十数MBほどディスクが必要
*
* (1) データのインストール
*
* 書き込み権限のあるディレクトリにこのファイルをコピーし、
* コマンドラインから実行してください。
* 或いはブラウザから"zipsearch.php?mode=setup"にアクセスしてください。
*
* 同じ階層に"zipdata"ディレクトリが作成され、その下に郵便番号データが
* 一番号一ファイルで格納されます。
*
* ディレクトリ構造は下記のようになります。
*
* zipdata/XXX/XXXYYYY.json
*
* 容量が十数メガほど必要です。
*
* (2) 検索
*
* "zipsearch.php?q=XXXYYYY"にアクセスしてください。
* 次のようなJSONが返ります。
*
* [
* {
* "zip": "1000001",
* "pref": "東京都",
* "city": "千代田区",
* "addr": "千代田"
* }
* ]
*
* ひとつの番号に複数の地名がある場合は次のようになります。
*
* [
* {
* "zip": "0680546",
* "pref": "北海道",
* "city": "夕張市",
* "addr": "南部青葉町"
* },
* {
* "zip": "0680546",
* "pref": "北海道",
* "city": "夕張市",
* "addr": "南部菊水町"
* }
* ]
*
* 番号に対応するデータが存在しない場合などエラーの場合は404が返ります。
*
* あるいは、JSONデータはファイルになっているので、直接ファイルを
* 取っていっても構いません。
*
* (3) 注意など
*
* ・CSVを一旦全部メモリに読み込むので、メモリ容量にご注意ください。
* ・12万ファイルほど作成するので、ディスクの書き込み速度が遅いと時間がかかります。
*
*/
/***** config ******/
// データ作成時にgzipする。CPUをたくさん使うので重くなります。
// 元々小さいファイルなので全部で数メガ程度しか節約できないかと。
$create_gzipped_data = false;
/***** initialize ******/
setlocale('LC_ALL', 'ja_JP.UTF-8');
$lang = 'UTF-8';
ini_set('memory_limit', '512M');
ini_set('max_execution_time', 60 * 60);
$is_cli = (php_sapi_name() === 'cli');
$topdir = __DIR__;
$datadir = $topdir . '/zipdata';
$logs = array();
$logfile = sys_get_temp_dir() . '/zipcode.log';
$logfile_tmp = sys_get_temp_dir() . '/zipcode.log.tmp';
/***** functions ******/
// 通常のログ出力
function zipcode_put_logfile()
{
global $logs;
global $logfile;
global $logfile_tmp;
file_put_contents($logfile_tmp, implode("\n", $logs)."\n");
rename($logfile_tmp, $logfile);
}
function zipcode_log($message = '', $no_newline = false)
{
global $is_cli;
global $logs;
if ($is_cli) {
echo $message;
if (!$no_newline) {
echo "\n";
}
} else {
$logs[] = $message;
zipcode_put_logfile();
}
}
// パーセント表示など、同じ行で書き換わるもの
function zipcode_log_replace($message = '')
{
global $is_cli;
global $logs;
if ($is_cli) {
echo "\r",$message;
} else {
$logs[count($logs)-1] = $message;
zipcode_put_logfile();
}
}
function zip_code_tmplog($message)
{
error_log($message."\n", 3, '/tmp/aaa');
}
// show setup page
function zipcode_setup()
{
$pathinfo = pathinfo(__FILE__);
$filename = $pathinfo['filename'];
if (isset($pathinfo['extension']) && !empty($pathinfo['extension'])) {
$filename .= ('.' . $pathinfo['extension']);
}
?>
<!doctype html>
<html>
<head>
<title>郵便番号データのインストール</title>
<meta charset="UTF-8" />
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script>
$(function(){
"use strict";
var filename = "<?php echo $filename; ?>";
$("#start").on("click", function(e){
$(this).prop("disabled", true);
function get_log(){
$.ajax({
url: filename+"?mode=progress",
dataType: "text"
}).done(function(data, textStatus, jqXHR){
$("#logbox").val(data);
if(!/EOF/.test(data)){
window.setTimeout(get_log, 3 * 1000);
}
});
}
$.ajax({
url: filename+"?mode=install",
timeout: 60 * 60 * 1000,
dataType: "text"
});
window.setTimeout(get_log, 3 * 1000);
});
});
</script>
</head>
<body>
<div id="button">
<button id="start">スタート!</button>
</div>
<div id="log">
<textarea cols="100" rows="30" id="logbox"></textarea>
</div>
</body>
</html>
<?php
}
// send progress information
function zipcode_progress()
{
global $logfile;
global $logfile_tmp;
if (file_exists($logfile)) {
$logdata = file_get_contents($logfile);
} else {
$logdata = '???';
}
if (strstr($logdata, 'EOF') !== false) {
unlink($logfile);
unlink($logfile_tmp);
}
header('Content-Length: ' . strlen($logdata));
header('Content-Type: text/plain');
echo $logdata;
}
// install
function zipcode_install()
{
global $is_cli;
global $lang;
global $topdir;
global $datadir;
global $logfile;
global $logfile_tmp;
global $create_gzipped_data;
if (file_exists($logfile)) {
unlink($logfile);
}
if (file_exists($logfile_tmp)) {
unlink($logfile_tmp);
}
// 必須チェック
zipcode_log('必須チェック');
if (!ini_get('allow_url_fopen')) {
zipcode_log('allow_url_fopenオプションが無効です。');
zipcode_log('EOF');
exit;
} else {
zipcode_log('allow_url_fopenオプション … OK');
}
if (!in_array('http', stream_get_wrappers())) {
zipcode_log('allow_url_fopenオプションが無効です。');
zipcode_log('EOF');
exit;
} else {
zipcode_log('http stream wrapper … OK');
}
if (!class_exists('ZipArchive')) {
zipcode_log('ZipArchiveクラスがありません。');
zipcode_log('EOF');
exit;
} else {
zipcode_log('ZipArchiveクラス … OK');
}
if (!function_exists('json_encode')) {
zipcode_log('json_encode()がありません。');
zipcode_log('EOF');
exit;
} else {
zipcode_log('json_encode() … OK');
}
if (!is_writable($topdir)) {
zipcode_log('書き込み権限がありません。');
zipcode_log('EOF');
exit;
} else {
zipcode_log('書き込み権限 … OK');
}
// download
$url = 'http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip';
zipcode_log('データダウンロード中');
$data = file_get_contents($url);
if ($data === false) {
zipcode_log('失敗');
exit;
}
$zipfilename = tempnam($topdir, 'tempzipfile');
file_put_contents($zipfilename, $data);
zipcode_log('完了');
// extrace zip file.
$csvfilename = 'KEN_ALL.CSV';
zipcode_log('データ解凍中');
$zip = new ZipArchive;
if ($zip->open($zipfilename) === true) {
$zip->extractTo($topdir, $csvfilename);
$zip->close();
unlink($zipfilename);
zipcode_log('完了');
} else {
zipcode_log('失敗');
}
$zip = null;
// parse csv
$fh = fopen($csvfilename, 'r');
$rec = array();
$datasize = filesize($csvfilename);
zipcode_log('CSV読み込み中');
zipcode_log();
$tmp_count = 0;
while (($line = fgets($fh)) !== false) {
$per = intval(ftell($fh) * 100 / $datasize);
if (++$tmp_count % 100 === 0) {
zipcode_log_replace($per.'%');
}
$line = mb_convert_encoding($line, $lang, 'Shift_JIS');
$line = mb_convert_kana($line, 'KV', 'UTF-8');
$ary = str_getcsv($line);
$num = $ary[2];
if (!isset($rec[$num])) {
$rec[$num] = array();
}
$rec[$num][] = $ary;
}
zipcode_log_replace('100%');
zipcode_log();
fclose($fh);
// データ結合
$datanum = count($rec);
$cnt = 1;
zipcode_log('ファイル出力中');
zipcode_log();
$tmp_count = 0;
foreach ($rec as $num => $ary) {
$per = intval($cnt * 100 / $datanum);
if (++$tmp_count % 100 === 0) {
zipcode_log_replace(sprintf('%06d', $cnt) . ' / ' . $datanum . ' ('.sprintf('%02d', $per).'%)');
}
$cnt++;
$tmp = array();
if (count($ary) > 1) {
$prev = null;
foreach ($ary as $line) {
// 2015/6/18現在のデータを見たところ、
// ・長ければ分割と書いてあるが長さは当てにならない
// ・通常は「一つの郵便番号で二以上の町域を表す場合の表示」が
//  ゼロ(該当せず)の場合を見ればいい。
// ・例外がひとつだけあり、9218046を特別扱いする
if ($line[12] == 0 || $line[2] == '9218046') {
if ($line[12] == 0) {
if (isset($prev)) {
$prev[5] = $prev[5] . $line[5];
$prev[8] = $prev[8] . $line[8];
} else {
$prev = $line;
}
} elseif ($line[2] == '9218046') {
// この番号には結合が必要なものとそうでないものの
// 2つの番号が含まれる。
// 判別は、開きカッコと閉じカッコが両方あるものとそうでないもの、
// とするしかない様子。
if (mb_strpos($line[8], '(', 0, $lang) !== false &&
mb_strpos($line[8], ')', 0, $lang) !== false) {
if (isset($prev)) {
$tmp[] = $prev;
$prev = null;
}
$tmp[] = $line;
} else {
if (isset($prev)) {
$prev[5] = $prev[5] . $line[5];
$prev[8] = $prev[8] . $line[8];
} else {
$prev = $line;
}
}
}
} else {
if (isset($prev)) {
$prev[5] = $prev[5] . $line[5];
$prev[8] = $prev[8] . $line[8];
$tmp[] = $prev;
$prev = null;
} else {
$tmp[] = $line;
}
}
}
if (isset($prev)) {
$tmp[] = $prev;
$prev = null;
}
} else {
$tmp[] = $ary[0];
}
// $topdir/zipdata/頭三桁/XXXYYYY.json
// ディレクトリ作成
$prefix = substr($num, 0, 3);
$dir = $datadir . '/' . $prefix;
if (!file_exists($dir)) {
if (!mkdir($dir, 0755, true)) {
zipcode_log('ディレクトリ作成に失敗しました '.$dir);
zipcode_log('EOF');
exit;
}
}
$filename = $dir . '/' . $num . '.json';
$data = array();
foreach ($tmp as $t) {
$data[] = array(
'zip' => $num.'',
'pref' => $t[6],
'city' => $t[7],
'addr' => $t[8]
);
}
if ($create_gzipped_data && function_exists('gzencode')) {
$json_data = gzencode(json_encode($data));
$filename .= '.gz';
} else {
$json_data = json_encode($data);
}
if (file_put_contents($filename, $json_data) === false) {
zipcode_log('ファイルの書き込みに失敗しました。 '.$filename);
zipcode_log('EOF');
exit;
}
}
zipcode_log_replace($datanum . ' / ' . $datanum . ' (100%)');
zipcode_log();
zipcode_log('終了');
zipcode_log('EOF');
unlink($csvfilename);
if (!$is_cli) {
header('Content-Type: text/plain');
echo 'OK';
}
}
function zipcode_return_404($message = '')
{
http_response_code(404);
header('Content-Type: text/plain');
header('Content-Length: '.strlen($message));
echo $message;
exit;
}
function zipcode_search()
{
global $datadir;
// qパラメータがなければ404
if (!isset($_REQUEST['q'])) {
zipcode_return_404('No parameter.');
}
$q = $_REQUEST['q'];
// 数字7桁必須
if (!preg_match('/\d{7}/', $q)) {
zipcode_return_404('Invalid parameter format.');
}
$prefix = substr($q, 0, 3);
$filename_base = $datadir . '/' . $prefix . '/' . $q . '.json';
$filename_gz = $filename_base . '.gz';
// 該当するデータなし
if (!file_exists($filename_base) && !file_exists($filename_gz)) {
zipcode_return_404('No requested data.');
}
// 正当性チェック終了
// お返事する
if (file_exists($filename_gz)) { // .gzがある
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) &&
preg_match('/gzip/i', $_SERVER['HTTP_ACCEPT_ENCODING'])) {
// gzipしたものを使用する
header('Content-Encoding: gzip');
$data = file_get_contents($filename_gz);
} else {
$data = gzdecode(file_get_contents($filename_gz));
}
} else {
$data = file_get_contents($filename_base);
}
header('Content-Type: application/json; charset=UTF-8');
header('Content-Length: ' . strlen($data));
echo $data;
}
/***** main *****/
if ($is_cli) {
zipcode_install();
} else {
if (isset($_REQUEST['mode'])) {
switch ($_REQUEST['mode']) {
case 'install':
zipcode_install();
break;
case 'setup':
zipcode_setup();
break;
case 'progress':
zipcode_progress();
break;
default:
zipcode_return_404('no such mode.');
}
} else {
zipcode_search();
}
}
exit;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment