7.1 郵便番号→住所検索,Wikipedia検索

(1/1)
クラウドコンピューティングのイラスト
WikipediaやAmazon、Googleといったクラウド・サービスは、ユーザーが作成したプログラムから利用できるようなAPI(Application Programming Interface)を用意している。Webで利用できることから、ここではとくに WebAPI と呼ぶことにする。
JavaScriptを使って、今回は、ZIP SEARCH API SERVICE を使って郵便番号を住所に変換するプログラムと、Wikipedia API を使ってWikipediaの見出し語検索を行うプログラムを作ってみよう。

目次

サンプル・プログラム

WebAPI利用イメージ

WebAPI利用イメージ
上図は、WebAPI の仕組みを表したものである。
クライアント(ブラウザ)からインターネットを経由し、WikipediaやAmazon、Googleといったクラウドの WebAPI を利用する。
クライアントから検索キーワードなどをGET/POSTなどのhttp通信で WebAPI へ送信すると、結果が XMLJSON の形で返ってくる。WebAPI の裏側にはデータベースなどのシステムがあるかもしれないが、クライアントからはそれらを意識しないで済む。

郵便番号→住所検索

郵便番号→住所検索
ZIP SEARCH API SERVICE 「JIS X0401」対応版」は、郵便番号を入力パラメータとしてURLで渡すと、住所(当道府県名、市町村名、町名など)ををJSONやJSONP形式で返す WebAPI である。
利用は無償だが、このWebAPIの 利用規約を遵守すること。
WebAPIのURL
URL
https://api.thni.net/jzip/X0401/JSONP/{1}/{2}.js

入力パラメータ
フィールド名 要否 内  容
1 必須 郵便番号(上3桁)
2 必須 郵便番号(下4桁)
応答データ構造(json) state 都道府県コード stateName 都道府県名 city 市区町村名 street 町域名

0071: /**
0072:  * ZIP SEARCH API SERVICE API:郵便番号→住所検索(JSONP)
0073:  * @param   String zip1 郵便番号(ハイフンの前 3桁)
0074:  * @param   String zip2 郵便番号(ハイフンの後 4桁)
0075:  * @return  String リクエストURL
0076: */
0077: function getURL_Zip2Address(zip1zip2) {
0078:     let url = 'https://api.thni.net/jzip/X0401/JSONP/' + zip1 + '/' + zip2 + '.js';
0079: 
0080:     return url;
0081: }

ユーザー関数 getURL_Zip2Address は、この WebAPI の呼び出し方法に従って、郵便番号からリクエストURLを生成する。

0083: /**
0084:  * 検索ボタン押下処理
0085:  * @param   なし
0086:  * @return  なし
0087: */
0088: function zip2address() {
0089:     //エラー・クリア
0090:     let errmsg = '';
0091:     document.getElementById('error').innerHTML = errmsg;
0092: 
0093:     //変換後テキスト・クリア
0094:     document.getElementById('state').value  = '';
0095:     document.getElementById('city').value   = '';
0096:     document.getElementById('street').value = '';
0097:     document.getElementById('dest').value   = '';
0098: 
0099:     //検索郵便番号
0100:     let zip = document.getElementById('zip').value;
0101:     //空白除去
0102:     zip = zip.trim();
0103:     //バリデーション
0104:     const regex = new RegExp('^[0-9]{3}\-[0-9]{4}$')
0105:     if (! regex.test(zip)) {
0106:         errmsg = '郵便番号が間違っています.'
0107:         console.error(errmsg);
0108:         document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0109:         return;
0110:     }
0111:     //郵便番号をハイフンで分離
0112:     let zips = zip.split('-');
0113: 
0114:     //XMLHttpRequestオブジェクト生成
0115:     let request = new XMLHttpRequest();
0116: 
0117:     //WebAPIリクエスト
0118:     let url = getURL_Zip2Address(zips[0], zips[1]);
0119:     console.log(url);
0120:     document.getElementById('WebAPI').innerHTML = '※WebAPI&nbsp;<a href="' + url + '">' + url + '</a>';
0121: 
0122:     //SOP回避
0123:     let target = document.createElement('script');
0124:     target.charset = 'utf-8';
0125:     target.src = url;
0126:     target.onerror = function() {
0127:         errmsg = '郵便番号が間違っているかWebAPIに接続できません.'
0128:         console.error(errmsg);
0129:         document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0130:     }
0131:     document.body.appendChild(target);
0132: 
0133:     //JSONP実行関数
0134:     target = document.createElement('script');
0135:     target.innerHTML = (
0136:         function ZipSearchValue(result) {
0137:             console.log(result);
0138:             document.getElementById('state').value = result.stateName;
0139:             document.getElementById('city').value = result.city;
0140:             document.getElementById('street').value = result.street;
0141:             document.getElementById('dest').value = result.stateName + result.city + result.street;
0142:         }
0143:     );
0144:     target.onerror = function() {
0145:         errmsg = '郵便番号が間違っているかWebAPIに接続できません.'
0146:         console.error(errmsg);
0147:         document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0148:     }
0149:     document.body.appendChild(target);
0150: }

検索ボタンをクリックしたときに実行するのが zip2address である。
まず、入力した郵便番号をハイフンで3桁と4桁に分離する。これには split メソッドを利用した。

次に、WebAPI を非同期呼び出しするために XMLHttpRequest オブジェクトを生成する。これは、WebAPI のような非同期通信を行う Ajax(Asynchronous JavaScript + XML)通信を行うときに使うオブジェクトだ。XMLの名前が付いているが、JSONやJSONPでも利用できる。

ここで、JavaScriptには、同一オリジンのリソースにしかアクセスできないという制限 SOP(Same-Origin Policy;同一生成元ポリシー)が課されている。同一オリジンというのは、同じドメイン、同じポート番号であることを指す。
SOPはセキュリティ対策の一環で、JavaScriptが悪意のあるスクリプトを実行したり、異常なデータを参照することを回避するための仕組みである。

しかし、WebAPI にアクセスするには、この SOP が邪魔になる。そこで、サーバ側で特殊なHTTPヘッダ項目を追加することで、送り出したWebページ上のスクリプトがWebブラウザから別のサーバへアクセスできるようにする CORS(Cross-Origin Resource Sharing;クロスオリジンリソース共有)が施されていることが望ましい。
だが、今回利用する ZIP SEARCH API SERVICE を含め、CORS に対応している WebAPI は少ない。
ここでは次善の策として、JSONPを使って SOP を回避する。

具体的には、2つのscriptを動的に追加する。
1つは、WebAPI そのものを srcとするscriptだ。これにより、scriptが WebAPI と同一オリジンで実行されると見せかけることになる。
2つめは、JSONPで実行されるscriptを追加する。JSONP は、JSONデータを解析する関数を追加したデータ形式である。ZIP SEARCH API SERVICE ではJSOP実行関数は ZipSearchValue 固定だ。
scriptの追加には appendChild メソッドを用いた。エラー発生時には、console.logとHTMLにエラーを出力する。
JSONP実行関数は、JSON形式の応答データをそのままHTMLの要素に代入するだけである。
最後に、都道府県名、市町村名、町名などを結合し、クリップボードにコピーできるようにした。

このプログラムは、よくある会員登録で郵便番号ボタンをクリックすると自動的に住所が入力されるGUIに応用できる。

Wikipedia検索

Wikipedia検索
フリーの百科事典として有名な「Wikipedia」にも Wikipedia API と呼ぶ WebAPI が用意されている。検索キーワードにヒットする見出し語のサマリなどを返してくれるWebサービスで、利用料はかからない。
WebAPIのURL
URL
https://ja.wikipedia.org/w/api.php

入力パラメータ
フィールド名 要否 内  容
format 省略可 xml, json, yaml等
callback 省略可 コールバック関数名
action 必須 操作:ここではquery
prop 省略可 action固有のパラメータ。記事の各構成要素を取得する。ここではextractsを指定し、サマリを抽出する。
explaintext 省略可 出力をHTMLではなくプレーンテキストにする。
redirects 省略可 リダイレクト記事を含める。
titles 必須 見出し検索語。
応答データ(json) query pages (pageID) extract サマリー pageid ページID

0068: /**
0069:  * Wikipedia API:サマリのリクエストURLを取得する
0070:  * @param   String query 検索キーワード
0071:  * @return  String リクエストURL
0072: */
0073: function getURL_WikipediaAPI_summary(query) {
0074:     let url = 'https://ja.wikipedia.org/w/api.php?format=json&callback=callback&&action=query&prop=extracts&exintro&explaintext&redirects=1&titles=' + encodeURI(query);
0075: 
0076:     return url;
0077: }

ユーザー関数 getURL_WikipediaAPI_summary は、この WebAPI の呼び出し方法に従って、検索キーワードからリクエストURLを生成する。
前述の getURL_Zip2Address と異なり、パラメータを GETで渡す。
Wikipedia API には明示的にJSONP呼び出しはないが、コールバック関数を指定することによって同様の処理ができる。

0079: /**
0080:  * 検索ボタン押下処理
0081:  * @param   なし
0082:  * @return  なし
0083: */
0084: function wikisearch() {
0085:     //エラー・クリア
0086:     let errmsg = '';
0087:     document.getElementById('error').innerHTML = errmsg;
0088: 
0089:     //検索結果クリア
0090:     document.getElementById('dest').value  = '';
0091: 
0092:     //検索キーワード
0093:     let query = document.getElementById('query').value;
0094:     //空白除去
0095:     query = query.trim();
0096:     //入力文字のエスケープ
0097:     query = htmlspecialchars(query);
0098: 
0099:     //XMLHttpRequestオブジェクト生成
0100:     let request = new XMLHttpRequest();
0101: 
0102:     //WebAPIリクエスト
0103:     let url = getURL_WikipediaAPI_summary(query);
0104:     console.log(url);
0105:     document.getElementById('WebAPI').innerHTML = '※WebAPI&nbsp;<a href="' + url + '">' + url + '</a>';
0106: 
0107:     //SOP回避
0108:     let target = document.createElement('script');
0109:     target.charset = 'utf-8';
0110:     target.src = url;
0111:     target.onerror = function() {
0112:         errmsg = 'WebAPIに接続できません.'
0113:         console.error(errmsg);
0114:         document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0115:     }
0116:     document.body.appendChild(target);
0117: 
0118:     //JSONP実行関数
0119:     target = document.createElement('script');
0120:     target.innerHTML = (
0121:         function callback(result) {
0122:             console.log(result);
0123:             let key = Object.keys(result.query.pages)[0];
0124:             let summary = result.query.pages[key].extract;
0125:             //応答結果あり
0126:             if (typeof summary != 'undefined') {
0127:                 document.getElementById('dest').value = summary;
0128:             //応答結果なし
0129:             } else {
0130:                 errmsg = '検索キーワードが見つかりません.'
0131:                 console.error(errmsg);
0132:                 document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0133:             }
0134:         }
0135:     );
0136:     target.onerror = function() {
0137:         errmsg = 'WebAPIに接続できません.'
0138:         console.error(errmsg);
0139:         document.getElementById('error').innerHTML = 'エラー:' + errmsg;
0140:     }
0141:     document.body.appendChild(target);
0142: }

検索ボタンをクリックしたときに実行するのが wikisearch である。前述の zip2address と流れは同じだ。

留意点すべきは、応答で得られるJSONオブジェクトのページIDが可変である点。
IEでも動作するよう、Object.keys を利用し、応答で得られるJSONオブジェクト result.query.pages の最初の要素を取得する。これがページIDである。
ページIDから目的のサマリーが存在するかどうかを typedef 演算子で調べ、無ければHTMLとconsole.logにエラー出力する。

コラム:CORS問題

ファイアウォールのイラスト
CORS は、不正なスクリプトが実行されてしまうXSS(Cross Site Scripting)脆弱性と、意図しない処理を実行されるCSRF (Cross-Site Request Forgeries)脆弱性を防ぐ目的で導入されている。

あるクライアントまたはすべてのクライアントに CORS を許可するには、サーバ側で Access-Control-Allow-Origin ヘッダを送信する必要がある。
自サーバやレンタルサーバであれば、この設定ができるだろうが、Wikipediaのような既存クラウドサービスで設定することはできない。そこで今回は、JSONPを使って回避をしている。
ブラウザによっては設定によって CORS を回避することができる。たとえばChromeは、"--disable-web-security --user-data-dir="C:\Users\ユーザ名\Local\Google\Chrome\User Data"" というオプションを付けて起動することでCORS を回避できる。

ただし、CORS 導入の目的から明らかなように、JSONPを使ったり CORS を回避することは、プログラムが脆弱性リスクを抱えることになる。

参考サイト

(この項おわり)
header