--- title: 図書館を検索するGreasemonkeyの作り方 author: kazu634 date: 2008-02-09 wordtwit_post_info: - 'O:8:"stdClass":13:{s:6:"manual";b:0;s:11:"tweet_times";i:1;s:5:"delay";i:0;s:7:"enabled";i:1;s:10:"separation";s:2:"60";s:7:"version";s:3:"3.7";s:14:"tweet_template";b:0;s:6:"status";i:2;s:6:"result";a:0:{}s:13:"tweet_counter";i:2;s:13:"tweet_log_ids";a:1:{i:0;i:3707;}s:9:"hash_tags";a:0:{}s:8:"accounts";a:1:{i:0;s:7:"kazu634";}}' categories: - つれづれ ---

 つい最近宮城県立図書館がリニュアールされた。その結果、Amazonの個別ページを開いた際に図書館の蔵書チェックを行うGreasemonkeyスクリプトがうまく動かなくなってしまった。大学で借りれるものならば借りたい!宮城県立図書館に行けば借りられるのなら借りたいのだ!

 何を言っているかわからない人は下の画像を見てほしい。

f:id:sirocco634:20080209105045j:image

こんな形で左上に東北大学附属図書館に蔵書があればリンクが表示される。右下には実験用に作った神奈川県立図書館の蔵書検索スクリプトの結果*1

 今回、宮城県立図書館の蔵書検索Greasemonkeyを作り直そうとしたのだが、自分の作業過程をかなり忘れていたことに気づいたので、自分用の備忘録としてここに書き散らかしておきます。ちなみに宮城県立図書館用のスクリプトはどうやらGreasemonkeyの仕様のためにうまく動かないことが判明。。。refererを指定してpostできないみたいなんです。ぐすん。

 ちなみに私はGreasemonkeyやJavascriptについてあまり詳しく知らないので、おかしなことを書いている可能性は高いです。そこら辺を割り引いて読んでください!

先人の肩に乗っていこう!

 Googleなんかで検索すると色々と提供している人が多いことに気づきます。「GreasemonkeyでAmazonから探せる図書館まとめ – Myrmecoleon in Paradoxical Library. はてな新館」でまとめられています。それらのGreasmeonkeyスクリプトを眺めていると、二つの種類があることに気づきます。

  1. 検索結果のページに飛ばすリンクを作成するタイプ
  2. 検索結果のページを解析して、蔵書があれば個別の蔵書ページへのリンクを作成するタイプ

こんな感じです。今回は二つめのタイプに取りかかることにします。

 私は素人さんなので、既存のスクリプトを変更することで自分のやりたいことを実現することにしました。こういうときはじゃんじゃん先人の肩に乗っていけばいいと思います。

参考にしたスクリプト

 自分が過去に作ったのを見よう見まねで色々と修正をしてみることにした。とりあえず見つけたのはこいつだった。

// ==UserScript==
// @name          Amazon2
// @namespace     
// @description	  Temporary Script.
// @include       http://*.amazon.*
// ==/UserScript==
// HTMLでなければ終了
if(document.contentType != 'text/html') return;
// ASINを見つけるよ
document.body.parentNode.innerHTML.match(/name=\"ASIN\" value=\"([0-9A-Z]{10})([\/\-_a-zA-Z0-9]*)/i);
// ASINが見つからなければ終了
if (RegExp.$1 == '')	return;
// asinを変数に代入
var asin = RegExp.$1;
// パネルの設定
var GM_infoPanel = document.createElement('div')
with(GM_infoPanel.style) {
bottom = 0;
right = 0;
padding = '2px';
opacity = 0.8;
fontsize = 'x-small';
color = '#000000';
backgroundColor = '#EEEEEE';
border = '1px solid #C0C0C0';
zIndex = 100;
position = 'fixed';
}
GM_infoPanel.innerHTML = "Library Searching...";
GM_xmlhttpRequest({
method : "POST",
data   : "TERM=INDEX-1&WORD=" + asin + "&SP_SEARCH=1&MENUNO=0",
headers: {'User-Agent': 'Mozilla/4.0 (compatible)',
'Content-type': 'application/x-www-form-urlencoded',
'Host': 'www.klnet.pref.kanagawa.jp'},
onload : function(response){
if (response.responseText.match(/OPP1500[^"]*/i)) {
var result=response.responseText.match(/OPP1500[^"]*/g);
comment = 'http://www.klnet.pref.kanagawa.jp/opac/' + result[];
GM_infoPanel.innerHTML += '<br><a href="' + comment + '" target="blank">Kanagawa Pref</a>';
}
else{
GM_infoPanel.innerHTML += '<br>Kanagawa Pref: Not Found';
}
}
});
// パネルの表示
document.body.appendChild(GM_infoPanel);

こいつの読み方を見ていく。

スクリプトの読み方

 順を追って見ていくことにします。まず冒頭の

// ==UserScript==
// @name          Amazon2
// @namespace     
// @description	  Temporary Script.
// @include       http://*.amazon.*
// ==/UserScript==

はお約束なので、とりあえず書いておくべき部分。「@include」の部分は「http://*.amazon.co.jp」になっていることが多いのだけれど、自分の場合Amazon.comから海外の古本屋サイトに行くなどの用途も考慮しているのでjpだけに決めうちしていません。ここらへんは使い回しているので、各自で適当に書き換えればいいかと。

 次の

// HTMLでなければ終了
if(document.contentType != 'text/html') return;
// ASINを見つけるよ
document.body.parentNode.innerHTML.match(/name=\"ASIN\" value=\"([0-9A-Z]{10})([\/\-_a-zA-Z0-9]*)/i);
// ASINが見つからなければ終了
if (RegExp.$1 == '')	return;
// asinを変数に代入
var asin = RegExp.$1;

という部分はAmazonの個別ページに現在いるのかどうかをチェックしている部分。個別ページでなければ、処理を進めずに終了させている。これはAmazonの個別ページにはかならず

<input type="hidden" name="ASIN" value="4797336803" />

という一行があること利用しています。ちなみにasinというのはisbn10と同じ。すべての本は固有な10桁の番号が割り振られていて、その番号のことをisbn10といいます。isbn10は必ず「10桁の数字」、もしくは「9桁の数字 + X」になります。isbn10がわかればどんな図書館でも蔵書検索できるので、「asinが存在するか調べる」ことは、「蔵書検索ができるか」をチェックしていることになります。*2asinは後で必要になるので、変数asinに代入しています。

 次はパネルの設定。

// パネルの設定
var GM_infoPanel = document.createElement('div')
with(GM_infoPanel.style) {
bottom = 0;
right = 0;
padding = '2px';
opacity = 0.8;
fontsize = 'x-small';
color = '#000000';
backgroundColor = '#EEEEEE';
border = '1px solid #C0C0C0';
zIndex = 100;
position = 'fixed';
}
GM_infoPanel.innerHTML = "Library Searching...";
// <=== 途中省略 ===>
// パネルの表示
document.body.appendChild(GM_infoPanel);

ここは検索した結果を表示する部分を担当している。お呪いだと思っておいて、後で必要になれば調べると良いのではないかと思う。*3

検索結果を表示するURLにはサイトによって二つのパターンがある…ようだ

 こいつが一番の山場。自分もうまく理解していないけど、オンライン書店などで検索した結果を表示する時に検索結果を表示するページのURLには二つのパターンがある。

  1. 検索結果のページに、おのおの検索条件に対応する固有のURLが与えられているパターン
  2. 検索結果のページが常に同じURLになる。つまり、内部的に検索条件を処理しているので、決まり切ったURLに移動するパターン

です。この二つがどのように違うのかはわからないorzのですが、とにかく二つのパターンがあります。例えばオンライン書店 本やタウンでisbnが「4798015350」で調べると、検索結果のページのURLはこんなURLになります。だから、一番目のパターンの時はこのURLの「isbn_cd=4798015350」の部分を任意のisbnにすれば、検索結果がわかるのです。

 それに対して二つめのパターンだと、例えば神奈川県立図書館で同じ条件で検索すると、検索結果のURLは常に「http://www.klnet.pref.kanagawa.jp/opac/index.jsp」です。内部的にゴニョゴニョしているので、Greasemonkeyを使ってそのゴニョゴニョをしてくれるように図書館のページにお願いしてあげないといけません。そうするための命令が、「GM_xmlhttpRequest」なのです。図書館の蔵書検索の場合だと、二つめのパターンしかこれまで見たことがないです。

GM_xmlhttpRequest

 参考にすべきは「GM_xmlhttpRequest」です。今回、こんな風に用いています。

GM_xmlhttpRequest({
method : リクエストの種類(大半はpost),
data   : "送信するデータ"
headers: {'User-Agent': 'Mozilla/4.0 (compatible)',
'Content-type': 'application/x-www-form-urlencoded'},
onload : function(response){成功したときの処理}

リクエストの種類は図書館の蔵書検索のページ(例は「神奈川県立図書館 」)の検索用フォームの設定を見ればわかります。この場合、

<FORM NAME="SIMPLE" ACTION="OPP1400" METHOD="POST">

とあるのでリクエストの種類は「POST」になります。

 送信するデータは「value = “***”」となっている部分を逐一拾っていけば大丈夫なのですが、いちいちそんなことやってられないのでずるをします。Live HTTP Headersを用います(解説)。こいつを使うとどんなデータを送っているのかが一発でわかるので便利です。WindowsならFiddlerというのがあります。こいつを使うと、任意のURLに任意のデータをPOSTできちゃったりして、結果を確認できます。こいつも便利です。

 こうした一連の過程を経ると、

GM_xmlhttpRequest({
method : "POST",
data   : "TERM=INDEX-1&WORD=" + asin + "&SP_SEARCH=1&MENUNO=0",
headers: {'User-Agent': 'Mozilla/4.0 (compatible)',
'Content-type': 'application/x-www-form-urlencoded',
'Host': 'www.klnet.pref.kanagawa.jp'},

となります。

onloadに続く部分

 以後の部分は、POSTした結果手に入れたものをいじくっています。こいつに検索結果が表示されているので、仮に蔵書があればそこへのリンクを表示させます。もしなければ、見つからなかった旨を表示します。

POSTした結果を眺めてみると、蔵書があれば

<A HREF="OPP1500?ID=1&SELDATA=TOSHO&SEARCHID=0&START=1&ORDER=ASC&ORDER_ITEM=SORT1-F&LISTCNT=20&MAXCNT=100&SEARCHMETHOD=SP_SEARCH&MENUNO=0">1</A>

といったように、かならず「OPP1500」ではじまるリンクが与えられているようだと推測できました。なので、

if (response.responseText.match(/OPP1500[^"]*/i)) {
var result=response.responseText.match(/OPP1500[^"]*/g);
comment = 'http://www.klnet.pref.kanagawa.jp/opac/' + result[];
GM_infoPanel.innerHTML += '<br><a href="' + comment + '" target="blank">Kanagawa Pref</a>';
}
else{
GM_infoPanel.innerHTML += '<br>Kanagawa Pref: Not Found';
}

では、まずOPP1500ではじまるリンクの部分が存在するかどうかをチェックしています。もし存在すれば、リンクの部分を抜き出して、そのリンクに基となる「http://www.klnet.pref.kanagawa.jp/opac/」をくっつけて、結果を表示させています。

おわりに

 なんかまずいこと書いているかもしれないので、そのときは教えていただけるとありがたいです(__)

Greasemonkeyスクリプティング TIPS&SAMPLES

Greasemonkeyスクリプティング TIPS&SAMPLES

*1:今回は関係ないけど、左下には各種オンラインストアへのリンクが表示されるようになっている

*2:ハードカバーと文庫版は異なるisbnが振られていることに注意。また、雑誌や雑貨などにもasinは割り振られているのですが、それらにはisbn10が割り振られていないのでAmazon側で適当な値を割り振っています。一応、そいつも認識するようにしています。こいつはアフィリエート作成のために取っておいたんだけど、はてなに移ってからは使ってないな(..;)

*3:→google:GM_infoPanel Greasemonkeyしても自分のブログしか出てこないorz。『Greasemonkeyスクリプティング TIPS&SAMPLES』を参照するのが良いかも。