Google Feed APIの代わりに自前のPHPでJSONPを扱う

2015年12月3日にGoogle Feed APIが突然使えなくなりました。しかし、翌日頃には復活した模様です。
Google Feed APIは公式に非推奨扱いであり、今回の停止騒ぎで如何に軽視されているのかが分かりました。
ここでは、次のサーバーダウンや廃止に備え、自前のPHPでJSONPを処理する方法を紹介します。
ここで説明する内容は、初心者の開発者や、プログラムを専門としないウェブ担当者向けです。

JSONとJSONPの違い

おさらい

JSONとは?

JSONは異なるプログラム言語間でデータをやり取りし易いように用意されたデータの記述ルールです。
配列は[角括弧]、連想配列は{中括弧}に含め、文字列はダブルクォーテーションで囲む、エンコーディングはUTF-8、
というようなルールがあります。
それぞれのプログラム内では、JSONへのエンコード、JSONからのデコードが行われます。
例えば、
PHPの連想配列→エンコード(PHP)→JSON→デコード(Javascript)→Javascriptのオブジェクト、
Javascriptのオブジェクト→エンコード(Javascript)→JSON→デコード(PHP)→PHPの連想配列、
というようにデータをやり取りできます。

JSONPとは?

まず、JSONを使ったPHP・Javascript間のやり取りを例示します。
Javascriptで、{"id":123}というID情報をtest.phpに渡し、PHPでこのIDに合ったデータを返して、Javascriptでこれを処理します(「成功時の処理」の部分)。

$.ajax( {
    "url": "./test.php",
    "dataType" : "json",
    "data": {"id": 123},
    "success": function(result) {
        成功時の処理
    }
} ); 

jQueryを使わなければ、

var ajax = new XMLHttpRequest();
ajax.open("GET", "./test.php?id=123", true);
ajax.setRequestHeader("X-Requested-With", "XMLHttpRequest");
ajax.onreadystatechange = function () {
    if(ajax.readyState != 4 || ajax.status != 200) {
        return;
    }

    成功時の処理
};
ajax.send(""); 

上記の
./test.php
の部分を、別ドメインの
http://b.jp/test.php
にすると、エラーが出て処理ができません。これは、セキュリティ上の制約によるもので解決方法は基本的にありません。
このような、クロスドメイン(異なったドメイン間)で処理を行う際に利用するのがJSONPです。
JSONPでは同じ処理を以下のように記述します。

$.ajax( {
    "url": "http://b.jp/test.php",
    "dataType" : "jsonp",
    "data": {"id": "123"},
    "success": function(result) {
        成功時の処理
    }
} ); 

jQueryを使わなければ、

function my_callback(result) {
    成功時の処理
}
var script = document.createElement("script");
script.setAttribute("src", "http://b.jp/test.php?id=123&callback=my_callback");
script.type = "text/javascript";
document.getElementsByTagName("head").item(0).appendChild(script); 

便利なjQueryを使うとdataTypeを変えるだけなので違いがあまり分かりませんが、jQueryを使わない場合を考えると結構違うことをやっています。
同一ドメインではXMLHttpRequestオブジェクトによる非同期通信をしているのに対し、クロスドメインではscriptタグを設置し、callbackを実行しています。
JSONPがやっていることはとてもシンプルです。では、PHP側でJSONPを度のように扱っているかを次に見てみます。

PHPでJSONPを扱う

上のjQueryを使わない例は単に、

<script type="text/javascript" src="http://b.jp/test.php?id=123&callback=my_callback"></script> 

というscriptタグを設置しているだけです。
通常は拡張子が「js」のJavascriptを呼ぶところに「.php」を入れ、PHPでサーバーサイドの処理を行います。例えば「$_GET["id"]で取得したIDに合ったレコードをデータベースから取得する」といった処理です。
拡張子は「.php」でも、呼び出したHTMLではtext/javascriptの扱いなので、結果は
my_callback( {"result": {"name": "aiueo"} } );
のようにJavascript形式のテキストで出力します。
JSONPではcallback関数名も引数としてPHP側に渡されているので、関数名を変数として以下のように出力します。

<?php echo $_GET["callback"]."(".json_encode($result).")"; ?> 

これにより、Javascript側で用意したmy_callbackがJSON化した結果を引数として実行されます。

処理のフローまとめ

以上をまとめてフロー図にしてみます。

HTML・Javascript側)
src="http://b.jp/test.php?id=123&callback=my_callback"としたscriptタグを設置

PHP側)
idを使ってDB照会などサーバーサイドの処理を実行

PHP側)
処理で取得した結果をJSON化して、my_callback(JSON);として出力

HMTL・Javascript側)
予め要していたmy_callbackが、PHPの処理結果であるJSONを引数として呼び出し

xmlをjsonに変換するたった3行のPHPスクリプト

JSONPは様々な用途に利用できます。しかし、今回はGoogle Feed APIが停止したことをうけて書き始めた記事なので、ブログFeedをJSON形式で出力することに焦点を当てて説明したいと思います。
ブログのRSSはXMLで記述されています。ブログFeedからこれらのXMLを取得し、JSONにデコードして出力するという処理を、PHPではネイティブ関数だけを使い、たって3行で実現可能です。

$arr = simplexml_load_file($_GET["feed"]);
$json = json_encode($arr);
echo $_GET["callback"]."(".$json.");";

XMLの取得にはsimplexml_load_file関数を利用します。
simplexml_load_fileで取得した内容は連想配列なので、これをそのままjson_encode関数でJSON化します。
最後は上で説明したように、callback変数を使いJavascriptの文字列として出力します。

実際には、この処理の前に、feedやcallbackの有無と形式の正誤をチェックした方が良いでしょう。例えば下記のような処理です。

//callbackチェック
if(!$_GET["callback"] || !preg_match("/^\w+$/", $_GET["callback"]) ) {
    exit();
}
//feedチェック
if(!preg_match("/^https?:\/\/[\x21-\x7e]+$/", $_GET["feed"]) ) {
    die($_GET["callback"]."();");
} 

もしこのPHPファイルを自分用に使いたい場合は、リファラーチェックを下記のように加えると良いかも知れません。

//refererチェック
if(!preg_match("/^https?:\/\/a\.jp/", $_SERVER["HTTP_REFERER"]) ) {
    die($_GET["callback"]."();");
} 

問題点

とても手軽な方法ですが、大きな問題があります。
それは、一部のブログサービス(RSSのフォーマット)によっては、simplexmlでXMLの内容が正しく取得できないことです。
例えば、アメーバブログのdescriptionは取得できません(しかし、少なくともテストした全てのフォーマットでタイトルと記事へのリンクは取得できました)。
導入前に表示したいブログFeedを入力してお試しの上、取得できない部分はないか、また取得できない部分があっても構わないかどうかの判断をしてください。
尚、simplexml_load_fileを使用しないでXMLを取得する方法は、ネイティブ関数としては存在しないようです。
誰かが公開しているライブラリを使用するか、file_get_contentsなどで取得したXMLから正規表現で1つずつ値を取り出すプログラムを作る必要があります。

JSON化されたRSSをJavascriptで表示する

jQueryを使ってdataType=JSONPとしてリクエストし、上のPHPで処理した結果をdiv.testタグに設置する処理は、例えば以下のようになります。

$.ajax( {
     "url": "http://b.jp/test.php",
    "dataType" : "jsonp",
    "data": {"feed": feed},
    "success": function(res) {
        if(typeof(res) != "object") {
            return "";
        }

        var html = "";
        var items;
        if(typeof(res.channel) == "object" && typeof(res.channel.item) == "object") {
            items = res.channel.item;
        } else if(typeof(res.item) == "object") {
            items = res.item;
        } else if(typeof(res.entry) == "object") {
            items = res.entry;
        }

        for(var i = 0 ; i < items.length; i++) {
            var pub = items[i].pubDate ? items[i].pubDate : (items[i].published ? items[i].published : (items[i].issued ? items[i].issued : "") );

            html += "<h3><a href=\"" + items[i].link + "\" target=\"_blank\">" + items[i].title + "</a></h3>"
                + "<p>" + (typeof(items[i].description) == "string" ? items[i].description : (typeof(items[i].summary) == "string" ? items[i].summary : "") ) + "</p>"
                + (pub ? "<div class=\"date\">" + date_format(pub) + "</div>" : "");
        }

        $("div.test").append(html);
    },
    "error": function(res, status) {
    }
} ); 

タイトルをh3タグ、概要をpタグ、日付をdiv.dateタグに出力しています。
処理のほとんどは、RSS・RDF・ATOMのフォーマットの違いを吸収するためのものです。
date_format関数は例えば下記のようになります。

var zerofill = function(time) {
    return time < 10 ? "0" + time : time;
};
var date_format = function(str) {
    if(!str) {
        return "";
    }

    if(str.match(/^(\d{4})(?:-|\/)(\d{2})(?:-|\/)(\d{2})\D+(\d{2}):(\d{2}):(\d{2})(?:\+\d{2}:\d{2})?$/) ) {
        var t = new Date(RegExp.$1, RegExp.$2 - 1, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6);
    } else {
        var t = new Date(str);
    }

    //IE用(「Fri Oct 21 10:17:42 +0000 2011」を「Fri Oct 21 2011 10:17:42 +0000」にする)
    if(isNaN(t) ) {
        var t_arr = str.split(" ");
        str = [t_arr[0], t_arr[1], t_arr[5], t_arr[2], t_arr[3], t_arr[4]].join(" ");
        t = new Date(str);
    }

    return t.getFullYear() + "/" + (t.getMonth() + 1) + "/" + t.getDate() + " " + zerofill(t.getHours() ) + ":" + zerofill(t.getMinutes() ) + ":" + zerofill(t.getSeconds() );
}; 

RSS表示の用途でGoogle Feed APIの代わりになるのか

Google Feed APIを同じことをやると下記のようになります。

var g_feed = new google.feeds.Feed(feed);
g_feed.load(function(res) {
    if(typeof(res) != "object" || typeof(res.feed) != "object" || typeof(res.feed.entries) != "object") {
        return "";
    }

    var html = "";
    var items = res.feed.entries;
    for(var i = 0 ; i < items.length; i++) {
        html += "<h3><a href=\"" + items[i].link + "\" target=\"_blank\">" + items[i].title + "</a></h3>"
            + "<p>" + items[i].contentSnippet + "</p>"
            + "<div class=\"date\">" + date_format(items[i].publishedDate) + "</div>";
    }

    $("div.test").append(html);
} ); 

Google Feed APIの優れた点は、RSS・RDF・ATOMのフォーマット間の差異を予め吸収してくれる点です。
また、上記したsimplexmlでXMLの一部が取得できない問題もクリアできます。とても使い勝手が良いAPIです。
自前で用意したプログラムがこの代わりになるのかは、simplexmlの働きと、皆さんのスキルによって異なります。
simplexmlがFeed APIと変わらず全てのデータを拾ってくれるならば問題ありません。全てを取得できない場合は、自らそれを解決しなければなりません。問題が解決できなければ、Google Feed APIの穴は埋められないことになります。
いずれにせよ、将来空くであろう「Google Feed APIの穴」を如何に埋めるかは、今からでも準備しておきましょう。

自作を断念したら

まめわざで提供しているツールにRSS表示ブログパーツがあります。これは開発者向けではないため、設定して設置するだけのものですが、simplexmlを利用していないので多くのRSSに対応しています。困ったら使用をご検討ください。

2015/12/11