WordPress:jQueryなしのAjaxでウェブスクレイピング結果を表示する

WordPress:jQueryなしのAjaxでウェブスクレイピング結果を表示する

Heroku の無料プランが11月28日で終了することを受けて、その代替となる機能を WordPress 内で完結するよう実装しました。あらためて WordPress の Ajax を理解することが出来ました。

01Ajax でウェブスクレイピング結果を表示する

関連記事は、

目的としていることはやや特殊ですが、Ajax やウェブスクレイピングの考え方自体は応用のきくものですので参考にしていただけることもあるかと思います。やろうとしていることは、私自身が運用している映画のレビューサイトと、同時にレビューを投稿している Filmarks のマイページの記事をリンクさせようということです。

もともとは、このサイトも含めすべてはてなブログで運用していましたので Javascript 以外に機能追加の方法がなく、Heroku に Node.js サーバーを立ち上げていました。たとえば、この Filmarks ボタンをクリックするとその映画のレビューが Filmarks にあればそのマイページに飛ぶということです。

それを今回 WordPress に移行しましたのですべて PHP で完了させました。

02WordPress の Ajax

WordPress には Ajax で通信するための機能が /wp-admin/admin-ajax.php として用意されています。ですので、ブラウザからこの admin-ajax.php にリクエストを送ればいいのですが、そのためにはブラウザにここに送ってくださいよということを知らせる必要があります。

wp_localize_script

一般的に WordPress では、wp_enqueue_scripts アクションフックを使ってスクリプトを読み込みます。その際に同時に PHP からブラウザに変数を渡す方法が用意されています。wp_localize_script です。

基本的な構文は、

wp_localize_script( 'スクリプトのハンドル名', 'データを格納する変数名', '配列データ' );

で、今回の場合は、

function sample_enqueue_scripts_styles() {
	if(is_single()){ // この条件分岐は今回の内容のため
		wp_enqueue_script( 'mi-filmarks', get_stylesheet_directory_uri() . '/lib/js/get-filmarks.min.js', '', '', true );
		wp_localize_script( 'mi-filmarks', 'wp_ajax', array(
			'ajax_url' => admin_url( 'admin-ajax.php' ),
			'nonce' => wp_create_nonce( 'filmarks-mypage' ),
		) );
	}
}
add_action( 'wp_enqueue_scripts', 'sample_enqueue_scripts_styles' );

としています。

wp_localize_script に wp_enqueue_script を使って読み込むスクリプトのハンドル名(mi-filmarks)、変数(wp_ajax)とデータ(配列)を与えておきます。そうしますと HTML に次のコードが出力されます。URL はローカルの開発環境になっています。

<script type="text/javascript" id="mi-filmarks-js-extra">
/* <![CDATA[ */
var wp_ajax = {"ajax_url":"http:\/\/localhost:8000\/wp-admin\/admin-ajax.php","nonce":"bf000952a7"};
/* ]]> */
</script>

Fetch による Ajax スクリプト

ブラウザからサーバーへリクエストを送る Ajax スクリプトには fetch を使います。

基本的な構文は、

fetch(URL, {オプション})
.then(response => response.json()) // response.text(), response.blob()
.then(data => console.log(data))
.catch(error => console.log(error))

で、今回の場合は、映画タイトルと記事 ID が必要になりますのでそれらをオプションとしてサーバーに送ります。サーバー側の php では Filmarks にレビューがあればその URL を、なければ ‘no review’ のテキストを返しますので一旦グローバル変数に保存するようにしています。このあたりは特殊なケースですので後述します。

window.imz = window.imz || {}; // 特殊,グローバル変数を使用するため

const params = new URLSearchParams();
const matches = /postid-(\d+)/.exec(document.body.classList); // bodyのクラス名から記事IDを取得

params.append('action', 'get_filmarks'); // 送ったデータを処理するWordPressのアクションフック名
params.append('nonce', wp_ajax.nonce );
params.append('title', document.title);
params.append('postid', matches[1]);
const opt = {
	method: 'post',
	body: params
}

fetch(wp_ajax.ajax_url, opt)
	.then(response => response.text())
	.then(text => {
		imz.filmarks = text; // FilmarksのURLやエラーテキストをグローバル変数に保存する
	})
	.catch(error => {
		imz.filmarks = 'error';
	});

サーバーから返されたデータ filmarks をグローバル変数に保存している理由は後述します。いずれにしても Ajax でなにを取得してどう処理するかはそもそもの目的ですので text であれ、json であれ、画像などのバイナリデータであれ、必要な処理方法をとるということになります。

サーバー側 wp_ajax アクションフック

ブラウザが admin-ajax.php あてに送ったデータは wp_ajax_[action名] や wp_ajax_nopriv_[action名] のアクションフックで受け取り処理します。前者はログインユーザー用、後者はログインしていない一般ユーザー用ですので、処理作業によってはログインユーザーのみにすることが出来ます。

一般的な構文は次のとおりです

function get_filmarks_func(){
	echo '成功';
	die;
}
add_action('wp_ajax_get_filmarks', 'get_filmarks_func');
add_action('wp_ajax_nopriv_get_filmarks', 'get_filmarks_func');

die で処理を終了しておかないと admin-ajax.php が「0」を返しますので要注意です。

で、今回のケースはかなり特殊ですが一応コードを載せておきます。ウェブスクレイピングに正規表現を使っているのは、python よりも速かったからです。

function get_filmarks_func(){
	// nonceが一致しない場合は'fatal error'を返す
	if(!check_ajax_referer( 'filmarks-mypage', 'nonce', false )){
		echo 'fatal error';
		die;
	}

	preg_match('/^.*?「(.+?)」.*$/', $_POST['title'], $matches); // 特殊,記事タイトルの中から映画タイトルを取り出している
	$title = $matches[1];
	$postid = $_POST['postid'];
	// カスタムフィールドにURLが保存されていれば返して終了
	$meta_value = get_post_meta($postid, 'filmarks', true);
	if ($meta_value !== ''){
		echo $meta_value;
		die;
	}else{
		$meta_value = 'no review';
	}
	// 以下はFilmarksのマイページをスクレイピングして該当レビューURLを探している
	$url = 'https://filmarks.com/users/ausnichts';
	$mypage = file_get_contents($url);
	preg_match('/"c-pagination__last".*href="\/users\/ausnichts\?page=(\d+)/s', $mypage, $matches);
	$page = $matches[1];
	
	for($i=1; $i<=$page; $i++){
		$nowurl = $url . '?page=' . $i;
		$nowpage = file_get_contents($nowurl);
		$pattern = '/<a href="(\/movies\/\d+\?mark_id=\d+)">' . $title . '/s';
		if(preg_match($pattern, $nowpage, $matches)){
			$meta_value = 'https://filmarks.com' . str_replace('?mark_id=', '/reviews/', $matches[1]);
			break;
		}
	}
	// $meta_valueには、レビューがあればURL,なければ'no review'が入っている
	add_post_meta($postid, 'filmarks', $meta_value, true);
	echo $meta_value;
	die;
}
add_action('wp_ajax_get_filmarks', 'get_filmarks_func');
add_action('wp_ajax_nopriv_get_filmarks', 'get_filmarks_func');

03結果をグローバル変数に保存している理由

ということで、ブラウザに目的の Filmarks のレビュー URL が渡りグローバル変数に保存されるわけですが、この後なにをやろうとしているかを整理しますと、映画レビューサイトはそれぞれの映画について記事タイトルに映画タイトルを入れて1記事ずつ書いています。その記事の下に SNS のシェアボタンと一緒に Filmarks のボタンをおいて Filmarks サイトに自分のレビューがあればそのページに飛ぶようにしているということです。

ただ問題は、

  • Filmarks にレビュー記事があるかどうかわからない
  • レビュー記事がある場合には別ウィンドウ(タブ)を立ち上げる必要がある
  • レビュー記事がない場合にはダイアログを表示する必要がある

ということになりますので、一旦結果をグローバル変数に保存し、ボタンがクリックされた場合に、その値を読んで、値が URL であれば別ウィンドウで Filmarks を表示し、値が ‘no review’ であればダイアログを表示してレビューがない旨を表示するよう Javascript で書いているということです。

また、クリックされたときにまだグローバル変数が存在しないケースがありますので、その場合はローディング画像を出し setInterval() を使ってグローバル変数を読み込めるまで待つようにしています。

というかなり特殊なことですのでコードは省略です。

以上です。