CodeIgniterの学習 30 - 住所←→郵便番号 の相互検索機能ヘルパを作る(その2 CodeIgniter側で郵便番号データ取り込み機能を作ったがボツにしたソース)
(08/12/19追記)
以下の記述で使っているcurlライブラリは、
現在CodeIgniterのwiki上にある最新のcurlライブラリと異なります。
このブログの記述時点のリビジョンは
http://codeigniter.com/wiki/Curl_library/revision/7530/
だったのですが、
現在の最新は
http://codeigniter.com/wiki/Curl_library/revision/8261/
となっています。
ソースの中身が全く異なるので、別の人が同名のライブラリを作って、
wikiを丸ごと変更したのかな?
メソッド名も違うので最新のリビジョンのソースだとこのエントリーの記述では動きません。
中身は
- 最新の方が高機能(ベーシック認証、CURLOPT_COOKIE対応、CURLOPT_PROXY対応などいろいろ。ただし初歩的なバグがあるのとPHP5限定)
- 古い方は自動リダイレクトが実装されている(ようにみえる)、デフォルトでユーザーエージェントの設定がある。等々
という違いがあるようです。
新しいリビジョンのソースになって間もないのですが、
機能の多い新しいソースの方を改造して使うのがいいかも。
(なお、このブログではスクレイピング周りはあまり詳しくは記述しません。)
スクレイピング周りはいろいろな方法があり、さわりだけこのブログでもいくつか紹介しているけど、
自分はcurlを呼びだすこのような方法が好きです。興味のある人はソースを覗いてみるといいかもしれません。
新しいリビジョンのソースで動かしたい場合は、
新しいリビジョンのソースの、9行目付近の
private $CI; // CodeIgniter instance
を
public $CI; // CodeIgniter instance
に変更した後で、
このエントリーの記述内の、
<?php //上略 $this -> load -> library( 'curl' ); // curlでファイルを取得 $this -> curl -> open(); $content = $this -> curl -> http_get( $this -> file_url ); $this -> curl -> close(); //下略 ?>
等でデータ取得している部分を、
<?php //上略 $this -> load -> library( 'curl' ); // curlでファイルを取得 $content = $this -> curl -> get( $this -> file_url ); //下略 ?>
に呼びだすメソッド名を変更すれば一応動きます。(open(),close()不要)
(追記終わり)
ここから元のエントリー
今日は昨日のエントリで、半べそかきながらお蔵入りにした、
郵便番号データのダウンロード〜DB内への取り込みソースを貼っておく。
ソースが長い+取り込みが遅い+system関数を呼んでいてもろに環境依存。
これだったら昨日のシェルスクリプトで取り込んだ方が賢いよね。(ここまで作っている俺もあほだ。)
ちなみに12万件取り込むと時間が掛かりすぎるので、全件取り込みを試すのはやめた方がいいと思う。
たぶん30分位かかる。csv読み込みなら30秒なのに。(apache-php用のmysqlユーザには、FILE権限は与えない。)
念のため、没ソースは100件で取り込みをbreakするようにしている。
画面
途中で投げたのでかなり適当没ソースにしたけど悔しくないもんね
めちゃめちゃ環境依存+真剣に作っていないので出来が悪いが貼っておく。一応動くけど。
良かった点としては、これを作っているうちに、
- ファイルヘルパ(system/helpers/file_helper.php)の使い方
- get_file_info(パス)でファイル情報取得、戻り値の配列にファイルサイズとか日時とかが入っている。
- write_file(パス,データ)でファイル書き込み
- read_file(パス)でファイル内容の読み込み
- 言語ヘルパ(system/helpers/language_helper.php)の使い方
- 多言語用に$lang['zipcode_pid_err2'] = '秒待って再度実行してください。';みたいなメッセージを含むファイルを、application/language/japanese/hoge_lang.phpとして作る。
- $this -> lang -> load( 'hoge' ); で、1のメッセージが呼び出せるようになる。
- $this -> lang -> line( 'zipcode_pid_err2' ) ;で、文言を取得出来る。
- application/language/english/hoge_lang.phpとして英語版を作っておけば、言語毎のメッセージの切り替えも簡単。
といった方法を覚えたこと。
ソースの解説は特にしないが、
CodeIgnite側では、
- セッション:db_session (http://d.hatena.ne.jp/dix3/20080921/1221946495 で導入)
- ワンタイムトークンライブラリ:csrf (俺俺改造版 http://d.hatena.ne.jp/dix3/20081017/1224196292 で作成)
- curlライブラリ(日本郵便からlzh圧縮済データken_all.lzhをダウンロードする用):http://codeigniter.com/wiki/Curl_library/ から辿る
を使っている。
cURLが使えないと3は使えないので、fopenとかでlzhデータを取ってくるべきだろうが、
curlライブラリを使うと、
<?php //上略 $this -> load -> library( 'curl' ); // curlでファイルを取得 $this -> curl -> open(); $content = $this -> curl -> http_get( $this -> file_url ); $this -> curl -> close(); write_file( $this -> file_path, $content ); //lzhファイルの保存 //下略 ?>
みたいにコードが簡潔に書ける上、
httpヘッダ内Location:の自動追随とか、データのpostとかが楽になるから入れてみた。
(Snoopyを使うのが楽でいいけど、CodeIgniterのライブラリになっているこれをベースにスクレイピング周りを俺俺改造したいな。なんて思ってたりする。)
また、作業ディレクトリとして、
application/data/tmp/ ディレクトリをあらかじめ手動で作って、そこに 郵便番号データ(ken_all.csv)を解凍するようにしている。
system関数側では、
解凍にlha、整形にsedを使っている。
解凍は、lzhファイルをphp上だけで解凍する方法が無い(俺が知らない)ので仕方がない。
整形は、preg_replace系でもいいのだが、sedの方が圧倒的に速いから。
結局この辺でぐだぐだになって、しかも遅いからphpで取り込む方式をやめたのだ。
以下没ソース
1)モデル: application/models/zipcode.php
郵便番号周りの処理をモデルに寄せてみたかったので、寄せてみた。
<?php if (!defined('BASEPATH')) exit('No direct script access allowed'); class Zipcode extends Model { var $errmsg = ""; var $pid_limit_time; var $file_max_size; var $csv_max_size; var $pid_path; var $file_path; var $csv_path; var $file_url; function Zipcode() { parent :: Model(); $this -> lang -> load('zipcode');//アラートメッセージは言語ヘルパを使って外に出す $this -> load -> helper( 'file' ); //ファイルヘルパのロード } function _download_init() { set_time_limit(0); // ファイルパスとかのセット $this -> pid_limit_time = 600 ; //10分以内のpidファイルがあるとエラー $this -> file_max_size = 5000000; //解凍する圧縮ファイルの上限は約5MBとしておく $this -> csv_max_size = 30000000; //解凍後のCSVファイルの上限は約30MBとしておく $this -> pid_path = APPPATH . "data/tmp/zipcodeimport.pid"; //2重で取り込み実行されないようにpidファイルを作っておく $this -> file_path = APPPATH . "data/tmp/ken_all.lzh"; //ダウンロード先フルパス $this -> csv_path = APPPATH . "data/tmp/ken_all.csv"; //解凍したcsvのパス $this -> file_url = 'http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/ken_all.lzh' ; } // lzhファイル(中身CSV)のダウンロードと解凍 function _download_csv() { $this -> load -> library( 'curl' ); //curlライブラリのロード $ret = false; //ダウンロード成否判定フラグ //$download_flg = false; //本当にダウンロードするかどうかフラグ $now = time(); //現在時刻 // pidファイルの存在チェック、10分以内のpidファイルがあるとエラー、これより前のpidファイルは削除して続行 $pid_info = get_file_info( $this -> pid_path ); $time_diff = isset( $pid_info["date"] ) ? $now - $pid_info["date"] : NULL; if ( $time_diff && ( $time_diff < $this -> pid_limit_time ) ) { // 600秒以内のpidファイルが存在 $this -> errmsg = $time_diff . $this -> lang -> line( 'zipcode_pid_err1' ); $this -> errmsg .= ( $this -> pid_limit_time - $time_diff ) . $this -> lang -> line( 'zipcode_pid_err2' ) ; }else { // 古いpidファイルが有れば消去 if(realpath($this -> pid_path)){ @unlink( $this -> pid_path ); } // 古いファイルが有れば消去 if(realpath($this -> file_path)){ @unlink( $this -> file_path ); } if(realpath($this -> csv_path)){ @unlink( $this -> csv_path ); } // 現在時刻のみ記述されているpidファイルを作る、ファイル内の時刻は特に使っていないけど。 write_file( $this -> pid_path, $now ); // ファイルをダウンロードして結果を表示する // curlでファイルを取得 $this -> curl -> open(); $content = $this -> curl -> http_get( $this -> file_url ); $this -> curl -> close(); // lzhファイルの保存と解凍 if ( $content ) { write_file( $this -> file_path, $content ); //lzhファイルの保存 $content_info = get_file_info( $this -> file_path ); if ( $content_info['size'] > $this -> file_max_size ) { // ファイルサイズチェック $this -> errmsg = $this -> lang -> line( 'zipcode_lzh_size_max' ); }else { if ( realpath( $this -> file_path ) ) { // lzhてシステム関数呼ばないと解凍できないっけ? system( 'lha -eqw=' . realpath( dirname( $this -> file_path ) ) . ' ' . realpath( $this -> file_path ) . ' >/dev/null ' ); $csv_file_info = get_file_info( $this -> csv_path ); if ( !$csv_file_info ) { $this -> errmsg = $this -> lang -> line( 'zipcode_lzh_err1' ) ; }elseif ( $csv_file_info['size'] > $this -> csv_max_size ) { $this -> errmsg = $this -> lang -> line( 'zipcode_csv_max_size' ) ; }elseif ( $csv_file_info['size'] < 1 ) { $this -> errmsg = $this -> lang -> line( 'zipcode_csv_min_size' ) ; }else { //CSVファイルの文字エンコーディング変換 $str = mb_convert_encoding(read_file( $this -> csv_path ),"UTF-8","Shift-JIS"); $str = mb_convert_kana( $str ,"KV"); write_file( $this -> csv_path, $str ); //CSVファイルの保存 //変換が遅いのでsedに任せる $cmd = 'sed -i -e \'s/"\\([0-9]*\\)\\s*"/\\1/g\' -e \'s/"//g\' ' . realpath($this -> csv_path) ; system($cmd); $ret = true; //ここだけtrue } }else { $this -> errmsg = $this -> lang -> line( 'zipcode_lzh_err2' ); } } }else { $this -> errmsg = $this -> lang -> line( 'zipcode_lzh_download_err' ); } // 異常時のpidファイルの削除 // 正常にダウンロード出来たときはpidファイルはまだ削除しない if ( !$ret && realpath($this -> pid_path)) { @unlink( realpath($this -> pid_path) ); } } return $ret; } //データのインポート function _import_csv(){ //トランザクションは入れない。 //メンテナンス中に限って使うことを想定しているので、いきなりtruncate/insertしてる $ret = true; $str = read_file( realpath($this -> csv_path) ); $arr = explode("\r\n",$str); $this->db->truncate('zipcode'); $i=0; foreach($arr as $k=>$v){ $i++; //動作確認用100件で取り込みをやめる。 if($i>100){ $ret = true; break; } $cols = explode(",",$v); $data = array( 'code' => $cols[0] , 'old_zipcode' => $cols[1], 'zipcode' => $cols[2], 'kana_pref' => $cols[3], 'kana_city' => $cols[4], 'kana_street' => $cols[5], 'pref' => $cols[6], 'city' => $cols[7], 'street' => $cols[8], 'f1' => $cols[9] , 'f2' => $cols[10] , 'f3' => $cols[11] , 'f4' => $cols[12] , 'f5' => $cols[13] , ); if(! $this->db->insert('zipcode', $data)){ $this -> errmsg = $this -> lang -> line( 'zipcode_db_ins_err' ) ; $ret = false; break; } } //成功、失敗にかかわらずpidファイルは除去する if(realpath($this -> pid_path)){ @unlink( realpath($this -> pid_path) ); } return $ret; } function get_errmsg() { return $this -> errmsg; } } /** * //元データのCSVファイルは http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/ken_all.lzh から * //列定義はhttp://www.post.japanpost.jp/zipcode/dl/readme.htmlから * //CSVのエンコードはShift-JIS,CR+LF * //格納テーブルはエンコードはUTF-8,半角カタカナは全角カタカナに変換 CREATE TABLE zipcode ( id INTEGER(10) UNSIGNED NOT NULL auto_increment, code VARCHAR(6) NOT NULL, # 1. 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字 old_zipcode VARCHAR(5) NOT NULL, # 2. (旧)郵便番号(5桁)…… 半角数字、空白トリム zipcode VARCHAR(7) NOT NULL, # 3. 郵便番号(7桁)………… 半角数字 kana_pref VARCHAR(64) NOT NULL, # 4. 都道府県名 ………… 全角カタカナ(コード順に掲載) kana_city VARCHAR(127) NOT NULL, # 5. 市区町村名 ………… 全角カタカナ(コード順に掲載) kana_street VARCHAR(127) NOT NULL, # 6. 町域名 ……………… 全角カタカナ(五十音順に掲載) pref VARCHAR(64) NOT NULL, # 7. 都道府県名 ………… 漢字(コード順に掲載) city VARCHAR(127) NOT NULL, # 8. 市区町村名 ………… 漢字(コード順に掲載) street VARCHAR(127) NOT NULL, # 9. 町域名 ……………… 漢字(五十音順に掲載) f1 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 10. 一町域が二以上の郵便番号で表される場合の表示 (「1」は該当、「0」は該当せず) f2 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 11. 小字毎に番地が起番されている町域の表示(「1」は該当、「0」は該当せず) f3 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 12. 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず) f4 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 13. 一つの郵便番号で二以上の町域を表す場合の表示(「1」は該当、「0」は該当せず) f5 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 14. 更新の表示(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用)) f6 INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, # 15. 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用)) del_flg INTEGER(1) UNSIGNED NOT NULL DEFAULT 0, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified TIMESTAMP NULL , PRIMARY KEY(id) ); CREATE INDEX idx1_zipcode on zipcode (zipcode,del_flg); CREATE INDEX idx2_zipcode on zipcode (pref,del_flg); CREATE INDEX idx3_zipcode on zipcode (pref,city,del_flg); CREATE INDEX idx4_zipcode on zipcode (pref,city,street,del_flg); CREATE INDEX idx5_zipcode on zipcode (city,del_flg); CREATE INDEX idx6_zipcode on zipcode (city,street,del_flg); CREATE INDEX idx7_zipcode on zipcode (street,del_flg); CREATE INDEX idx8_zipcode on zipcode (f6,del_flg); CREATE INDEX idx9_zipcode on zipcode (old_zipcode,del_flg); */ /** * End of file zipcode.php */ /** * Location: ./application/models/zipcode.php */ ?>
2)メッセージ: application/language/japanese/zipcode_lang.php
複数言語に対応させる実験として言語ヘルパを使ってみた。
<?php if (!defined('BASEPATH')) exit('No direct script access allowed'); $lang['zipcode_pid_err1'] = '秒前に取り込みボタンが押されてまだ処理が終了していません。'; $lang['zipcode_pid_err2'] = '秒待って再度実行してください。'; $lang['zipcode_lzh_size_max'] = 'なぜかファイルがでかすぎだから中止。'; $lang['zipcode_lzh_err1'] = '解凍できてねーよ。'; $lang['zipcode_lzh_err2'] = '解凍したファイルが見つからないっす?'; $lang['zipcode_lzh_download_err'] = '元ネタファイルがダウンロード出来ないっす'; $lang['zipcode_csv_max_size'] = '解凍したファイルがでかすぎだから中止。'; $lang['zipcode_csv_min_size'] = 'ファイルの中が空だよ。ちゃんと解凍出来てるの?'; $lang['zipcode_db_ins_err'] = 'DBに突っ込むときになんかしくった'; /* End of file zipcode_lang.php */ /* Location: ./application/language/japanese/zipcode_lang.php */ ?>
3)コントローラ: application/controllers/zipcodeimport.php
ボタンをプチプチ押して、データ取り込みを実行する系。
lzhファイルを解凍して、csvファイルを直接アップロードさせる方法はファイルサイズが大きすぎるので却下。
<?php if (!defined('BASEPATH')) exit('No direct script access allowed'); // 郵便番号の取り込み実験 class Zipcodeimport extends Controller { // コンストラクタ function Zipcodeimport() { parent :: Controller(); // ログイン認証、ログインしてなければログイン画面に飛ばされる // $this -> freakauth_light -> check('superadmin');//superadmin権限でチェックする // urlヘルパ $this -> load -> helper( 'url' ); // フォームヘルパ $this -> load -> helper( 'form' ); // ワンタイムトークンライブラリのロード $this -> load -> library( 'csrf' ); //zipcodeモデルのロード $this -> load -> model( 'zipcode','', true ); $this -> zipcode -> _download_init(); } // デフォルトインデックス function index() { // ビューの生成 $data["title"] = "郵便番号の取り込みテスト"; $data["step"] = "STEP1 - ファイルのダウンロード"; // 中身テンプレートにデータをセット $tpl["sub_title"] = "郵便番号でいろいろ"; $tpl["main_content"] = $this -> load -> view( "zipcodeimport_index", $data , true ); // 外枠のテンプレートに、中身をはめ込む $this -> load -> view( 'base_view', $tpl ); } function submit() { if ( !$this -> input -> post( "submit" ) ) { // URL直呼びはindexへリダイレクト redirect( "/zipcodeimport/" ); } if ( !$this -> csrf -> check( "zipcodeimport" ) ) { // トークン不正は有無を言わさずindexへリダイレクト redirect( "/zipcodeimport/" ); } $this -> csrf ->clean( "zipcodeimport"); // バリデーションチェックOKの時の処理 if ( $this -> zipcode -> _download_csv() ) { //CSVファイルのダウンロードをおこなう // ビューの生成 $data["title"] = "郵便番号の取り込みテスト"; $data["step"] = "STEP2 - ダウンロード完了〜取り込み実行"; // 中身テンプレートにデータをセット $tpl["sub_title"] = "郵便番号でいろいろ"; $tpl["main_content"] = $this -> load -> view( "zipcodeimport_submit", $data , true ); }else { // ダウンロード失敗なら // ビューの生成 $data["title"] = "郵便番号の取り込みテスト 大失敗"; $data["step"] = "STEP2 - ああーなんかエラーだって"; $data["errmsg"] = $this -> zipcode ->get_errmsg(); // 中身テンプレートにデータをセット $tpl["sub_title"] = "郵便番号でいろいろ"; $tpl["main_content"] = $this -> load -> view( "zipcodeimport_err", $data , true ); } // 外枠のテンプレートに、中身をはめ込む $this -> load -> view( 'base_view', $tpl ); } function doimport(){ if ( !$this -> input -> post( "submit" ) ) { // URL直呼びはindexへリダイレクト redirect( "/zipcodeimport/" ); } if ( !$this -> csrf -> check( "zipcodedoimport" ) ) { // トークン不正は有無を言わさずindexへリダイレクト redirect( "/zipcodeimport/" ); } $this -> csrf ->clean( "zipcodedoimport"); if($this -> zipcode -> _import_csv()){ $data["title"] = "郵便番号の取り込みテスト 取り込みOK"; $data["step"] = "STEP3 - 取り込み完了"; $data["errmsg"] = $this -> zipcode ->get_errmsg(); // 中身テンプレートにデータをセット $tpl["sub_title"] = "郵便番号でいろいろ"; $tpl["main_content"] = $this -> load -> view( "zipcodeimport_success", $data , true ); }else{ // ビューの生成 $data["title"] = "郵便番号の取り込みテスト 取り込み実行大失敗"; $data["step"] = "STEP3 - ああーなんかエラーだって"; $data["errmsg"] = $this -> zipcode ->get_errmsg(); // 中身テンプレートにデータをセット $tpl["sub_title"] = "郵便番号でいろいろ"; $tpl["main_content"] = $this -> load -> view( "zipcodeimport_err", $data , true ); } // 外枠のテンプレートに、中身をはめ込む $this -> load -> view( 'base_view', $tpl ); } } //Endofclass /** * End of file zipcodeimport.php */ /** * Location: ./application/controllers/zipcodeimport.php */ ?>
4)中身ビュー1:application/views/zipcodeimport_index.php
最初のページ
<h2><?= $title ?></h2> <h3><?= $step ?></h3> <?= form_open('zipcodeimport/submit/') ?> <?= form_csrf('zipcodeimport') ?> <p class="info yellow"> ボタンを押すと元データをサーバー側にダウンロードするよ。<br> 元データのCSVファイルは <?= anchor( "http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/ken_all.lzh" , "日本郵便のサイト") ?> から取得するっす。<br> ちなみにファイルはlzhで圧縮されている。<br> あとダウンロード先は、application/data/tmp/というディレクトリを作っている。 </p> <input type="submit" name="submit" value="ぽちっと押してダウンロードしてみる" /> <?= form_close(); ?>
5)中身ビュー2:application/views/zipcodeimport_submit.php
ダウンロード後のページ、ここでDB取り込み実行
<h2><?=$title;?></h2> <h3><?=$step;?></h3> <?= form_open('zipcodeimport/doimport/'); ?> <?=form_csrf('zipcodedoimport');?> <p class="info green"> ファイルがダウンロードされたよん。 取り込み実行ボタンを押してくれ。 </p> <input type="submit" name="submit" value="取り込み実行!" /> <?= form_close(); ?>
6)中身ビュー3:application/views/zipcodeimport_success.php
取り込み成功時のページ
<h2><?=$title;?></h2> <h3><?=$step;?></h3> 取り込みが完了したなりよ。よかったよかった。
7)中身ビュー4:application/views/zipcodeimport_err.php
エラーページ
<h2><?=$title;?></h2> <h3><?=$step;?></h3> なんかエラーが出たって、残念。<?= anchor( "/zipcodeimport/" , "もう1回試す") ?>か、あきらめて管理者に教えれ <p class="info red"> <?=$errmsg?> </p>
8)外枠ビュー:application/views/base_view.php
以前と変わらないので省略
9)cssファイル:html/css/style.css
以前と変わらないので省略
10)ddl文
1)モデル の末尾にある。
このような回り道をしながら、次回に続く。
次回は、貯め込んだ郵便番号データをxmlrpcクライアント・サーバを使って検索結果を取得してみようかな。
(08/11/12 記述内 datas → dataに綴り誤りを修正)