日夜きめいるとIRCマクロを科学し、隙あらば、その成果を発表する部署です。
スクリプト部の方は、惜しみなく貢献してあげてください。

小粒でぴりりと辛い資料達、だといいな。

LimeChat マクロ Tips

基礎的な事(特定のメッセージに対して反応する)

動作条件
ユーザ %mel*
コマンド PRIVMSG
チャンネル *(#任意のチャンネル名  と書くと、そのチャンネルにのみ適用される。複数指定したい場合はLimeChatのヘルプを参照してください(レイアウト崩れる関係で対象記号を記載できませんorz)
メッセージ 任意のメッセージ
動作
動作 SEND
送信先 %f
動作の情報 任意のメッセージ

長いメッセージも入力し、物語マクロなんてものを作成する事もできます。

基礎的な事(ユーザの指定 - 基礎編)

マクロでは、マクロを発動させてもいいユーザを指定することが出来ます。例えば、発動条件のユーザ欄に"frofile1P"というnickを入れると、frofile1Pさんにマクロの発動を許可したことになります。自分自身を許可する場合は、自分のnickではなく、%meというフレーズを使います。
複数の人に許可を与えたいときは|(半角パイプ記号)でnickを繋げます。例えば、frofile1Pさんと、zi-noさんに許可するときは、"frofile1P|zi-no"と言った具合です。
みんなに許可を与えたい場合は、*(半角アスタリスク記号)を使います。これは自分自身には許可が下りないので気をつけてください。自分を含めたみんな、と言うときには、半角パイプ記号で"%me|*"と繋げて表現します。

基礎的な事(ユーザの指定 - 応用編[書きかけ])

半角アスタリスク記号はみんなに当てはまる、と言いましたが、正確には、半角アスタリスク記号は、そこに0文字以上の全ての文字列が当てはまる、と言うことを意味しています。
例えば、先頭に"Haku"が含まれていることを表現したいときは、"Haku*"、末尾に"Reimu"が含まれていることを表現したいときは、"*Reimu"、nick中に"Miko"が含まれていることを表したいときは、"*Miko*"となります。
また、自分が書き表したnick以外に当てはまる、と言うことを表したいときは、そのnickの前に"$NOT "というフレーズを付けます。例えば、"PSK"以外に当てはまることを表したいときは、"$NOT PSK"となります。

これらの記号やフレーズは、チャンネルの指定や、メッセージの指定にも使えますので、覚えておいて損はありません。と言うか是非とも覚えましょう。

$EncodeUrlの利用例(Google検索のURLを表示する)

google [検索ワード]でGoogle検索のURLを表示します。
検索ワードのエンコーディングに$EncodeUrl(文字列)を使います。
検索ワードの取得には発言を空白で区切った文字列%0~%9の%1(2番目)を使います。
動作条件
ユーザ %me|*
コマンド PRIVMSG
チャンネル *
メッセージ google *
動作
動作 SEND
送信先 %f
動作の情報 http://www.google.co.jp/search?ie=utf-8&q=$EncodeUrl(%1)

$Fileと%tの応用(きまぐれ時報-ときどき小町る)

時報と発言すると時間を返します。ときどき小町ってくれます。
動作条件
ユーザ %me|*
コマンド PRIVMSG
チャンネル *
メッセージ 時報
動作
動作 SEND
送信先 %f
動作の情報 $File(TimeSignal.txt)

TimeSignal.txtを(ユーザ名)\macros\filesに予め入れておきます。
TimeSignal.txtの内容は例えば以下のようにします。
%t
%t
%t
%t
%t
ξ・ヮ・)?
$Fileがランダムに行を選ぶことを利用して、%tでない行を用意することで、サボることを実現しています。

AddTailLineアクションと$FileOTの応用(何でも箱マクロ)

"[何か] を箱に入れる"で、箱に何かを入れて、"箱から出す"で箱から何かを出します。
動作条件(1つめ)
ユーザ %me|*
コマンド PRIVMSG
チャンネル *
メッセージ * を箱に入れる
動作(1つめ)
動作 AddTailLine
送信先 MyBox.txt
動作の情報 %0
ここで実行を止める 止めない
動作条件(2つめ)
ユーザ %me|*
コマンド PRIVMSG
チャンネル *
メッセージ * を箱に入れる
動作(2つめ)
動作 Send
送信先 %f
動作の情報 「%0」を箱に入れました
ここで実行を止める 止める
動作条件(3つめ)
ユーザ %me|*
コマンド PRIVMSG
チャンネル *
メッセージ 箱から出す
動作(3つめ)
動作 Send
送信先 %f
動作の情報 「$FileOT(MyBox.txt)」が出てきました。
ここで実行を止める 止める

箱に入れるときは、1つめで発言の頭を取ってAddTailLineにより、MyBox.txtの末尾に書き込みます。2つめで箱に入れたことをSendで知らせます。
箱から出すときは、3つめで$FileOTによりランダムに1行読み込み、選ばれた行を消去して、箱から出したものをSendで知らせます。

このマクロの問題点は、箱が空の時にそれを知らせることが出来ないことです。条件分岐の類はありませんので、「」が出てくることが気になるのであれば、スクリプトの使用を検討して下さい。

LimeChat Script Tips

以下のTipsはonChannelTextやonCommandの表記は省いています。textや、channelと言う変数がある場合は、それらの中で使うものだと考えて下さい。
スクリプトの基本文法については、とほほのJavaScriptリファレンスを参照して学習してください。

基本的なこと(誰かが発言したときに反応する・前編)

誰かの発言に対して何かしたいとき、スクリプトにevent::onChannelTextメソッドを定義します。
コードは大体次のような感じです。
function event::onChannelText(prefix, channel, text) {
  /* ここになにかを書く */
} 
prefixは発言した人の情報、channelは発言が行われたチャンネル名、textは発言内容が入ります。
prefixはいくつかのプロパティを持っています。代表的なものには、nickプロパティ(発言者のNick)、addressプロパティ(発言者のホスト名)があります。
試しに、何か発言したとき、それぞれの引数の値を見るコードを書いてみましょう。
function event::onChannelText(prefix, channel, text) {
  log(prefix.nick);
  log(prefix.address);
  log(channel);
  log(text);
} 
log関数はスクリプトコンソールに指定した文字列を印字する一引数の関数です。
スクリプトを有効にした後、スクリプトコンソールを開きながら、何か発言してみてください。
発言する度に、発言に関する情報が4行ずつコンソールに印字されると思います。

基本的なこと(誰かが発言したときに反応する・後編)

前編で、発言時に何かすることと、event::onChannelTextの引数の内容について確認しました。
それでは、誰かが特定の言葉を発言したときに、特定の言葉を返すようにしてみましょう。
たとえば、「死んだ」と発言したとき、「惜しいひとを・・・」と発言し返すスクリプトを考えます。
「死んだ」と言う発言だけに反応するにはif文で、textが「死んだ」であるかどうかを確認すればよいですね。
function event::onChannelText(prefix, channel, text) {
  if (text == "死んだ") {
     /* ここで「惜しいひとを・・・」と発言したい */
  }
} 
次は、「惜しいひとを・・・」と発言し返すコードを書きます。特定のチャンネルでNOTICE発言するには、send関数を使います。
send(発言したいチャンネル, 発言内容); 
ここでは反応したチャンネルに対して発言したいわけですから、一つめの引数はchannel変数となります。二つめは言わずもがなですね。
function event::onChannelText(prefix, channel, text) {
  if (text == "死んだ") {
     /* 「惜しいひとを・・・」と発言する */
     send(channel, "惜しいひとを・・・");
  }
} 
試しにスクリプトをonにしたサーバ内で発言してみましょう。ちゃんと発言し返されると思います。

さて、ここまで出来れば、後は努力賞です。頑張って楽しいスクリプトを作ってください。

発言に特定の文字列が含まれているときだけ反応する

マクロの発動条件で言う *ほげ* みたいなことを実現します。
正規表現にマッチした最初の位置を返すString#searchメソッドを使います。
String#searchメソッドは、マッチしなかった場合、-1を返すので、それを条件判断に利用します。
if (text.search(/ほげ/) != -1) {
  /* 以下"ほげ"が含まれていたときの反応 */
  ...
} 
また、正規表現にマッチした文字列を配列で返すString#matchメソッドでも実現できます。
こちらは、マッチしなかった場合、nullを返すので、それを条件判断に利用します。
if (text.match(/ほげ/)) {
  /* 以下"ほげ"が含まれていたときの反応 */
  ...
} 
正規表現の書き方については、正規表現を解説しているサイトを当たって下さい。

文字列をランダムに選んで返事をする

[0, 1)の乱数を返すMath#randomメソッドと、床関数のMath#floorを使って、文字列配列の要素をランダムに選択します。
var phrases = [
  "きめいる",
  "れあきめいる",
  "おお、こいるこいる"
];
send(channel, phrases[Math.floor(Math.random() * phrases.length)]); 

なるとがある時だけ反応する

LimeChat マクロではリストボックス選択で終わりな機能ですが、スクリプトではそうはい神崎です。
var ch = findChannel(channel);
var me = ch.findMember(myNick);
/* オペレータ権が無ければメソッドを抜ける */
if(!me || !me.op) return;
/* 以下、反応コード */
... 
組み込み関数findChannelでチャンネル名から、Channelオブジェクトを取得、次にfindMemberメソッドで自身のChannelMemberを取得してオペレータ権があるかどうかを確認しています。
onChannelTextのchannel引数はChannelオブジェクトにしてくれればいいのに。

Fileオブジェクトの補完資料

公式ヘルプでは足りないFile周りの説明を補完してみました。

ファクトリメソッド

File openFile(fileName : string [,readOnly : boolean [,codePage : int = CP_UTF8]]) 
指定したファイルを開き、対応するFileオブジェクトを得ます。
引数
fileName
開きたいファイル名を指定します。ファイル名は絶対パスないし、相対パスを使用することができます。相対パスを指定した場合、ルートはuserScriptFilePathが示すパスとなります。
readOnly
この引数は省略可能です。
ファイルを読み込み専用で開くかどうかを指定します。trueで読み込み専用、falseで読み書き可能となります。既定値はtrue(読み込み専用)です。
codePage
この引数は省略可能です。
ファイルの文字コードを指定します。既定値はCP_UTF8(65001)です。
Shift_JIS(CP932)の場合は932、EUC-JP(CP20932, CP51932)の場合は20932ないし、51932となります。

戻り値
ファイルを開けたとき、対応するFileオブジェクトを返します。
ファイルを開けなかったときはnullを返します。

メソッド

string readLine() 
ファイルから一行読み込みます。
ファイルの終端以降を読み込もうとしたときempty(等号比較でnullにマッチ)を返します。

テキストファイルを行区切りで配列に読み込む

マクロの$Fileみたいな真似をスクリプトでやろうと思っても、1関数でぽんっと済ます方法はありません。
一つめの方法としては、openFile関数、File#readLineメソッド、Array#pushメソッドの3つを使って実現させます。
var fileName; /* なにかファイル名を入れる */
var file = openFile(fileName);
var lines = [];
var line;
while((line = file.readLine()) != null) {
  lines.push(line);
}
file.close(); 
これで、lines変数に行区切りの文字列配列を得ます。配列と行数の対応は、lines[n-1]=n行目となります。例えば、lines[0]が1行目、lines[1]が2行目と言った具合です。
二つめの方法は、File#readAllメソッド、String#splitメソッドを使って実現させます。
var fileName; /* なにかファイル名を入れる */
var file = openFile(fileName);
var lines = file.readAll().split("\r\n");
file.close(); 
lines変数の内容は一つめの方法と同じです。
"\r\n"の部分はテキストファイルの改行コードに合わせる必要があります。

ファイルに何か書き込む

openFile関数の第二引数readOnlyにfalseを指定することで、書き込みを行うことができます。
一行書き込むときはFile#writeLineメソッド、行について気にせず書き込むときはFile#writeメソッドを使います。
書き込んだ位置以降の内容を切り捨てるには、File#truncateメソッドを使います。
var fileName; /* なにかファイル名を入れる */
var file = openFile(fileName, false); /* 書き込み可能にしてファイルを開く */
file.writeLine("Hello, world!"); /* "Hello, world![改行]"と書き込まれる */
file.write("きゃー所長さーん"); /* "きゃー所長さーん"と書き込まれる */
file.truncate(); /* 後に付いてる古い内容を消す */
file.close(); 

イベント発動者のIPアドレスを得る

多くのイベントではprefix : Prefix引数を通して、Prefix.addressプロパティから、イベント発動者のホスト名ないしIPアドレスを得ることが出来ます。
しかし、イベント発動者のIPアドレスだけを得ることが出来るプロパティや、ホスト名からIPアドレスを得る組み込み関数などは、LimeChat 2.30時点では実装されていません。
一方、外部コマンドを実行してその結果文字列を得る組み込み関数executeCommandが存在するので、ホスト名からIPアドレスを得るコマンドと組み合わせれば、イベント発動者のIPアドレスを得ることが可能になります。
ホスト名からIPアドレスを得るコマンドと言えば、よく知られているnslookupコマンドですね。Linuxな方はdigコマンド!と答えるかも知れませんが、そんなものはWindowsに標準搭載されていませんので、nslookupを使うことにします。
まず、nslookupがどのような結果文字列を返すか見てみましょう。
始めに、単純な成功例から。
>nslookup www3.atwiki.jp
Server:  xxx.xxx (*1)
Address:  xxx.xxx.xxx.xxx (*1)

Non-authoritative answer: (*2)
Name:    www3.atwiki.jp
Address:  219.117.252.116
大体このような結果が得られると思います。
(*1)には、IPアドレスを得るのに利用したDNSサーバのホスト名およびIPアドレスが入ります。
(*2)の行は、環境によってあったりなかったりします。
あと、表示にはいくつかのパターンがあります。
>nslookup www.google.com
Server:  xxx.xxx
Address:  xxx.xxx.xxx.xxx

Non-authoritative answer:
Name:    lb1.www.ms.akadns.net
Addresses:  65.55.12.249, 65.55.21.250, 207.46.19.190, 207.46.19.254
          207.46.192.254, 207.46.193.254
Aliases:  www.microsoft.com, toggle.www.ms.akadns.net
          g.www.ms.akadns.net
アドレスが複数になったことで、AddressがAddressesになっていたり、Aliasesと言う項目が増えていたりしますね。
次は、失敗例です。
>nslookup www.kimeiru.info
Server:  xxx.xxx
Address:  xxx.xxx.xxx.xxx


*** local.gateway can't find www.kimeiru.info: Non-existent domain
後半のNameやAddressと言った項目が全て消えて、エラーメッセージのみになっていますね。

さて、いよいよここからが本題です。
今まで見てきた表示から、空白行を隔てて前半がDNSサーバの情報、後半がホスト名を問い合わせた結果と言うことが分かります。私たちが今必要な情報は、問い合わせたホスト名に対応したIPアドレスですから、後半部分のうち、Address: ないし、 Addresses: の行を見ればいいと言うことですね。失敗例を見返すと後半にこの行は存在しないので、恐らく誤検出することはないでしょう。この項目は Name: の行の後に出て来ているので、それを条件にして抽出を行えば良さそうです。
Addressesの場合は、複数のIPアドレスが存在しますが、ここでは先頭のIPアドレスを代表として抽出することにしましょう。
var hostName = prefix.address;
var result = executeCommand("nslookup " + hostName);
result.match(/Name:.*\nAddress(?:es)?:\s+(\d+\.\d+\.\d+\.\d+)/);
var ip = RegExp.$1; 

追記(Vistaを考慮する)

nslookupの表示は統一されていると筆者は考えていましたが、どうやらVistaでは以下のような表示になるそうです。
サーバー: ********
Address:  *.*.*.*

名前:    ********
Address: *.*.*.*
いくつかの項目について日本語化されています。これに対応するには、対応する英語とorを取ればいいですね。
var hostName = prefix.address;
var result = executeCommand("nslookup " + hostName);
result.match(/(?:Name|名前):.*\nAddress(?:es)?:\s+(\d+\.\d+\.\d+\.\d+)/);
var ip = RegExp.$1; 
これでもやっていることはスクレイピングですので、将来書式が変わると動かなくなる可能性があります。
バージョンアップ等で動かなくなったときは書式の変更も考慮に入れてください。

非同期的に外部コマンドを実行する

LimeChatでは組み込み関数であるexecuteCommand関数を使って外部コマンドを実行し、その標準出力を得ることが出来ます。しかし、この関数は外部コマンドが終了するまで制御を戻さないので、実行終了までに時間の掛かるコマンドを実行すると、その間クライアントがフリーズしてしまいます。チャットと兼用で使っているなら、不快感をもたらしますし、スクリプト専用で使っているなら、フリーズしている間スクリプトの反応が停止してしまい、運用に支障が出るかも知れません。
そこで紹介するのが、WScript.ShellオブジェクトのExecメソッドです。
実行したいコマンドを引数にして呼び出すと、コマンドの実行を開始させた後、即座に制御を返し、コマンドの状態監視や操作を行うことが出来るWshExecオブジェクトを返します。
var command; /* 実行したいコマンド */
// WScript.Shellオブジェクトを得る
var WScriptShell = new ActiveXObject("WScript.Shell");
// 非同期実行を開始してWshExecオブジェクトを得る
var wshExec = WScriptShell.Exec(command); 
WshExecオブジェクトは次のようなメンバを持っています。
Status [WshExecStatus型]
コマンドの状態です。0(WshRunning)は実行中、1(WshFinished)は実行完了、2(WshFailed)は実行失敗を表します。
StdIn [ITextStream型]
コマンドの標準入力に繋がるストリームです。
StdOut [ITextStream型]
コマンドの標準出力に繋がるストリームです。
StdErr [ITextStream型]
コマンドの標準エラーに繋がるストリームです。
ProcessID [long型]
コマンドのプロセスIDです。
ExitCode [long型]
コマンドの終了コードです。実行終了時にこのプロパティから得ることが出来ます。
Terminate()
コマンドを強制終了します。

終わるまで待って、何か呼び出してくれるとかそんな便利なメソッドはないようなので、自らコマンドの終了を見届ける必要があります。
ではどうするかというと、Statusプロパティを監視して終了ステートになるまで待てば良いわけです。非同期的に監視するには、setTimeout関数を使えばよいですね。
(function commandWaitLoop() {
  //
  switch(wshExec.Status) {
  case 0: // WshRunning - 未だ実行中
    // 100ミリ秒後にもう一度statusを見ることにする
    setTimeout(commandWaitLoop, 100);
    break;
  case 1: { // WshFinished - コマンドの実行が完了した
      // 標準出力の内容を全て読み取る
      var result = wshExec.StdOut.ReadAll();
      // それを使って何かする
      ...
    }
  case 2: // WshFailed - コマンドの実行に失敗した
    // 失敗したのでなんかする
    ...
  }
})(); 
実際に使うには、可読性や再利用性を考えて、次のように関数化してしまうのがいいでしょう。
// asyncExecuteCommand - 非同期にコマンドを実行する
// command  : 実行するコマンド
// callback : 標準出力の内容を受け取る関数
function asyncExecuteCommand(command, callback) {
var wshExec = (new ActiveXObject("WScript.Shell")).Exec(command);
  (function commandWaitLoop() {
    switch(wshExec.Status) {
    case 0: // WshRunning - 未だ実行中
      // 100ミリ秒後にもう一度statusを見ることにする
      setTimeout(commandWaitLoop, 100);
      break;
    case 1: // WshFinished - コマンドの実行が完了した
      // 標準出力の内容を全て読み取ってコールバック関数に渡す
      callback(process.StdOut.ReadAll());
      break;
    case 2: // WshFailed - コマンドの実行に失敗した
      // 例外を投げる
      throw new Error("asyncExecuteCommand failed");
    }
  })();
}
// 使用例
asyncExecuteCommand("nslookup www.example.com", function(result) {
  // nslookup www.example.com の標準出力をコンソールに表示
  log(result);
}); 

続・非同期的に外部コマンドを実行する(コンソールウザイ編)

「非同期的に外部コマンドを実行する」の項でnslookupを使用例に挙げましたが、実行時にコンソールが出てきませんでしたでしょうか。そう、WScript.Shell#Execメソッドはコンソールアプリケーションを起動するとき、コンソールを表示させてしまうのです。これでは、スクリプトが実行される度にコンソールが付いたり消えたりするので、チャット兼用で使っていると鬱陶しくて仕方がありません。
そこで、Execでコンソールを表示させずにコンソールアプリケーションを実行する方法を2つ紹介します。

HideExec.vbs を使う

この問題を解決するために、吉岡 照雄氏HideExec.vbsというVBScriptを開発公開されています。
これを次のように、WScript.Shell#Execと組み合わせることで、コンソールを表示せずに実行することが可能になります。
var hideExecPath; // HideExec.vbs のパス
var command; // 実行したいコマンド
 
// 実行コマンドを生成
var hideCommand = 'WScript //B "' + hideExecPath + '" ' + command;
// WScript.Shellオブジェクトを得る
var WScriptShell = new ActiveXObject("WScript.Shell");
// 非同期実行を開始してWshExecオブジェクトを得る
var wshExec = WScriptShell.Exec(hideCommand);
// ここから標準入力受付期間
// 標準入力に対して何かする
wshExec.StdIn.WriteLine("きゃーじーのさーん"); // とか
wshExec.StdIn.Write("きめいるあいしてる"); // とか。
// 標準入力を閉じて実行を再開する
wshExec.StdIn.Close();
// 以下監視ループ
... 
ただ、HideExec.vbsは起動~終了までに若干時間が掛かるので、この方法は、即時性を求めるタスクには不向きかも知れません。

コンソールの表示を抑制してコマンドを実行するツールを作る

HideExec.vbsと違い、こちらはWin32APIプログラミングが出来るプログラミング言語を知っている必要があるので、少し玄人向けかも知れません。しかし、一から作るのは大変なので目的にあったコードを用意しておきました。
#include <windows.h>
extern "C"
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPCSTR, int) {
  // コマンドライン引数から実行するコマンドを引き出す
  PTSTR cmdLine = GetCommandLine();
  TCHAR eoc = (*cmdLine == '"') ? '"' : ' ';
  do {
    cmdLine = CharNext(cmdLine);
  } while(*cmdLine != '\0' && *cmdLine != eoc);
  do {
    cmdLine = CharNext(cmdLine);
  } while(*cmdLine == ' ' || *cmdLine == '\t');
  // コンソールの表示を抑制してコマンドを実行する
  STARTUPINFO si;
  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof(si);
  si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
  si.wShowWindow = SW_HIDE;
  si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
  si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
  PROCESS_INFORMATION pi;
  if (CreateProcess(NULL, cmdLine, NULL,
                    NULL, TRUE, CREATE_NO_WINDOW | DETACHED_PROCESS,
                    NULL, NULL, &si, &pi)) {
    // コマンドの終了まで待つ
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
  }
  // 最後に起きたエラーの番号を返してプログラムを終了する
  ExitProcess(GetLastError());
} 
このコードをC++言語によるWin32 GUIアプリケーションとしてコンパイルして出来るツールは、以下のように使います。
var hideConPath; // ツールのパス
var command; // 実行したいコマンド
 
// 実行コマンドを生成
var hideCommand = '"' + hideConPath + '" ' + command;
// WScript.Shellオブジェクトを得る
var WScriptShell = new ActiveXObject("WScript.Shell");
// 非同期実行を開始してWshExecオブジェクトを得る
var wshExec = WScriptShell.Exec(hideCommand);
// HideExec.vbsと違って、いつものように標準入力を扱えます。
// 以下監視ループ
... 
開発環境をお持ちでない方のためにコンパイル済みバイナリを用意しました。ご自由にお使いください。
ツールのパッケージをダウンロード

外部スクリプトをロードする

jQueryやPrototype等の補助ライブラリを使うには必須のイディオムです。
var file =openFile('スクリプトの相対/絶対パス');
eval(file.readAll());
file.close(); 
ファイルを読み込んでevalすることにより、あたかもその位置で読み込んだコードが実行されたように見せかけます。
ただし、グローバルでevalしないと、実行したコードにある定義が残らないことに注意して下さい。

Prototype.jsを使う

Prototype.js は、Javascript の利用をさらに効率的にする拡張を行ってくれる Javascript ライブラリです。
Prototype.js はブラウザ上で実行されることを想定しているため、そのままではローカル実行できません。
ブラウザ上で実行しているように見せかけるため、次のようなスタブを用意する必要があります。
// Web Library Hack
var document = {
	getElementsByTagName : function() { return [ { src : '' } ]; }, 
	createElement : function() { return { appendChild : function() {} }; },
	createTextNode : function() {},
	createEvent : function() { return { __proto__ : {} }; },
	getElementById : function() { return {}; },
	write : function() {},
	domain : "localhost"
};
var window = {
	document : document,
	attachEvent: function() {},
	setTimeout : function(a, b) { setTimeout(a, b); }
};
var navigator = { userAgent : 'LimeChat' };
var location = { protocol : "http:", port : "" };
function Element() {} 
Prototype.jsをロードする前に実行することで、Prototype.jsを利用できるようになります。
あくまでスタブなので、Prototype.js のいくつかの機能については利用することが出来ません(例:DOM要素の取得)。ブラウザと関係なさそうな意外な機能にも、ブラウザ依存が潜んでいることがあるので、十分気をつけて利用して下さい。
実際にこのスタブを利用するときは、外部ファイルに追い出して、外部スクリプトのロードを利用するといいでしょう。

Webサーバからリソースを取得する (Prototype.js利用編)

いわゆるAjaxですが、一からイディオムを記述すると超めんどいことになります。
一方、Prototype.jsのAjax.Requestクラスを利用すれば、いとも簡単にAjaxAjaxできます。
Ajax.Requestのコンストラクタ引数を設定してnewすることで通信が開始されます。
コンストラクタ引数は以下のようになっています。
Ajax.Request(
  取得したいリソースのURL,
  通信パラメータ
); 
前者は、ただ単純にURLを指定します。後者は、オブジェクトのリテラル表現 { ... } を用いてパラメータの設定を行います。
指定できるパラメータは以下の通りです。
asynchronous
非同期通信を行うかどうかをbool値で設定します。
特に理由がない限り非同期にしましょう。さもなくば、リソースの取得が成功または失敗するまでクライアントがフリーズします。
method
リソースの取得に使う述語を文字列で設定します。普通のリソースを取得するなら "GET"メソッドです。掲示板投稿と言ったものについては"POST"メソッドを使うことになるでしょう。
parameters
GETメソッドやPOSTメソッドで対象のリソースに渡すパラメータを設定します。
文字列による表現か、オブジェクトのリテラル表現 { ... } による表現を設定することが出来ます。
requestHeaders
サーバに対してリソースの取得を要求するときに送信する追加のヘッダを文字列配列で設定します。
["項目名", "項目の内容", "項目名", "項目の内容", ... ] と "項目名"、"項目の内容"を交互に記述します。
利用できる項目名についてはRFC2068の5.3を参照して下さい。
onSuccess
リソースの取得に成功したときに呼び出されるメソッドを設定します。
通常、このパラメータには匿名メソッド、つまり、function(...) { } の形を与えます。
メソッドには一つの引数が渡され、その引数から、リソースの内容等を取得することが出来ます。
渡される引数は以下のプロパティとメソッドを含んでいます。
  • status
サーバから返されたステータスコードです。
  • statusText
サーバから返されたステータスコードの文字列表現です。
  • responseText
取得したリソースのUTF-8エンコーディング文字列です。Shift_JIS等の他エンコーディングのリソースを取得した場合は思いっきり文字化けているので注意して下さい。
  • transport
Ajax.Requestが利用している XMLHttpRequestオブジェクトです。
  • getAllResponseHeaders()
リソースを取得したときにサーバから返されたヘッダのコレクションを返します。
getAllResponseHeaders().match(ヘッダ項目の名前)で、指定したヘッダ項目が存在するかどうかを確かめることが出来ます。
  • getResponseHeader(ヘッダ項目の名前)
リソースを取得したときにサーバから返されたヘッダ項目の内容を文字列で返します。
onFailure
リソースの取得に失敗したときに呼び出されるメソッドを設定します。
設定方法や渡される引数についてはonSuccessと同様です。
on(数字)
サーバから指定したステータスコードが返ったときに呼び出されるメソッドを設定します。
設定方法や渡される引数についてはonSuccessと同様です。

他にも色々ありますが、基本的なことを行うにはこれで十分です。
利用例として、以下に、Google の電卓機能をスクレイピングするスクリプトの一部を紹介します。
/* アクセスするアドレスを設定する */
var url = "http://www.google.co.jp/search?ie=utf-8&oe=utf-8&q=" + encodeURIComponent(text);
/* Ajax通信を開始する */
new Ajax.Request(url, {
  /* 非同期実行する */
  asynchronous : true,
  /* GET メソッドを使う */
  method : 'get',
  /* 常に最新の結果を得る為に最終更新日時チェックをとんでもなく古くする */
  requestHeaders : ["If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT"],
  /* 取得が成功したときに実行される */
  onSuccess : function(o) {
    /* 電卓結果を抜き出す.無ければエラーを表示して終了する */
    if(
    !o.responseText.match(/<img src=\/images\/calc_img\.gif/) ||
    !o.responseText.match(/<font size=\+1><b>(.+?)<\/b>/)) {
      send(channel, '"' + text + '"は計算できないのぜ。');
      return;
    }
    /* 結果を加工して返答する(gsubはprototype.js、
       unescapeHTMLは[文字参照を実体化する]の拡張を利用しています) */
      var resr = RegExp.$1.
        replace(/<sup>([^<]+)<\/sup>/g, '^$1').
        replace("\xA0", ' ').
        gsub(/<[^>]+>/,'').unescapeHTML();
      send(channel, resr + " なのぜ。");
  },
  /* 取得に失敗したときに実行される */
  onFailure : function(o) {
      send(channel, "なんか計算できなかったのぜ。");
  }
}); 

文字参照を実体化する

文字参照とは、HTMLやXMLの要素中に書けない文字を &なんとか; という形で表現する方法です。(例: < → <)
ブラウザは読み込むときに元に戻してくれますが、スクリプトでは読み込んでもそのままの状態で保持されます。
実は、Prototype.js に String#unescapeHTMLなる変換用メソッドが存在して、これを使えばめでたく変換できる……と言いたいところですが、内部的には、ブラウザ(正しくはDOMノード)に読み込ませて、変換結果を得ています。Prototype.jsを動かすスタブコードは、何にも出来ない偽のDOMノードしか作れないようにしてあるので、この機能を使うことは出来ません。
従って、実体文字参照の変換表を以て自前で変換する必要があります。
変換表はHTML4仕様書の文字実体参照(http://www.w3.org/TR/html4/sgml/entities.html)か、XHTML1.0仕様書の実体集合(http://www.w3.org/TR/xhtml1/#h-A2)に記載されている表を用いると良いでしょう。
上記の表をダウンロードした後、適当なスクリプトか手打ちで、変換表を作ります。
var entities = {
  /* -- Latin-1 characters -- */
   "nbsp"   : 160, /* no-break space = non-breaking space,
                     U+00A0 ISOnum */
  "iexcl"  : 161, /* inverted exclamation mark, U+00A1 ISOnum */
...
  "hearts"   : 9829, /* black heart suit = valentine,
                        U+2665 ISOpub */
  "diams"    : 9830  /* black diamond suit, U+2666 ISOpub */
} 
次に、変換方法について考えます。
単純にfor...inで回してreplaceしていくというのはどうでしょうか。
result = "P&G";
for(var entity in entities) {
  result = result.replace(
    "&"+entity+";",
    String.fromCharCode(entities[entity]));
}
log(result); // ==> "P&G"
 
一見これで良いように思えますよね。実は、この方法には落とし穴があります。
"amp"の変換対が変換表の中途にあると、こんなことが起きてしまいます。
result = "&amp;lt;";
for(var entity in entities) {
  result = result.replace(
    "&"+entity+";",
    String.fromCharCode(entities[entity]));
}
log(result); // ==> "<" ???
 
&lt;と表示されて欲しいところですが、<と表示されてしまいます。
これは何故かというと、コードはまず、resultから"&amp;"を見つけて、"&amp;lt;"を、"&lt;"に変換します。次に "&lt;"を見つけて、"&lt;"を"<"に変換します。そして,これ以上変換するものがないのでこれを変換結果とします。
そうです、変換途中に新しく変換対象が出現しているのです。これを防ぐには、"amp"の変換対を変換表の最後に持つことで、新しい変換対象が出現しても変換されない様にすればいいのです。
var entities = {
...
  "amp"     : 38  /*  ampersand, U+0026 ISOnum */
} 
さて、これで終わりかというと、そうではありません。
実体参照にはもう2つ別の表現があります。それは数値文字参照です。
&#71;  (10進数) ==> "G"
&#x40; (16進数) ==> "@"
さすがに、これを変換表に組み込むのは現実的ではありません。replaceの第二引数に匿名メソッドを指定することで、数値を個別に抽出し、それをString#fromCharCodeで文字に変換するという方法が低コストな方法でしょう。
"&#71;&#x40;".replace(/&#(\d+|x[0-9A-F]);/ig, function(whole, entity) {
  if (entity.match(/(\d+)/)) {
    /* 10進数の数字文字参照 */
    return String.fromCharCode(RegExp.$1);
  } else
  if (entity.match(/(x[0-9A-F]+)/)) {
    /* 16進数の数字文字参照。先頭の0を付け足して0x~にする */
    return String.fromCharCode("0" + RegExp.$1);
  } else {
    /* それ以外 */
    return whole;
  }
}); // ==> "G@"
 
しかし、これを文字実体参照の変換とセットで使うと、上で解決した、変換により新しく変換対象が出現して再変換してしまう、という問題が再発してしまいます。
これを防ぐには、文字実体参照と数値文字参照を同時に変換するように書き換えてしまいます。
"&amp;#70;&#71;&#x40;".replace(/&(#\d+|#x[0-9A-F]|\w+);/ig, function(whole, entity) {
  if (entities[entity]) {
    /* 文字実体参照 */
    return String.fromCharCode(entity);
  } else
  if (entity.match(/#(\d+)/)) {
    /* 10進数の数字文字参照 */
    return String.fromCharCode(RegExp.$1);
  } else
  if (entity.match(/#(x[0-9A-F]+)/)) {
    /* 16進数の数字文字参照。先頭の0を付け足して0x~にする */
    return String.fromCharCode("0" + RegExp.$1);
  } else {
    /* それ以外 */
    return whole;
  }
}); // ==> "&#70;G@"
 
利用用途としては、例えば、Ajax通信で得られたHTMLファイルやXMLファイル等をスクレイピングするときに、それに含まれる文字参照を元に戻すことが挙げられます。他にも色々使い方はあると思いますので、興味があれば研究してみるのもよいでしょう。
上記の方法で String#unescapeHTML を実装するサンプルコードを用意しましたので、使ってみたい方や、より深く研究したい方はどうぞ。
サンプルコードをダウンロードする

他のスクリプト言語を使う(基礎編)

LimeChat ScriptはJavaScript、正確に言えばJScript Engineによって動いているので、通常他のスクリプト言語で書くことは出来ません。
しかし、ScriptControlというオブジェクトを用いることにより、それを可能にすることが出来ます。
var extCode = ここにスクリプトコードの文字列を入れる
var extScript = new ActiveXObject('ScriptControl');
/* ScriptControlオブジェクトの処理言語を他言語に切り替える */
extScript.Language = 使いたいスクリプト言語名の文字列;
/* ScriptControlオブジェクトにコードを追加する */
extScript.AddCode(extCode); 
上記のコードで、追加したコードが実行可能な状態になります。
追加したコードを呼び出すときは、ScriptControl#Runメソッドを使います。
extScript.Run(実行したいプロシージャ名の文字列, パラメータの配列); 
また、コード片を今すぐ実行したい場合は、ScriptControl#ExecuteStatementメソッドを使います。
extScript.ExecuteStatement(実行したいコード片); 
なお、スクリプト言語の指定ですが、通常の環境ではJScriptと、VBScriptのみ指定できます。
ActiveScriptRubyや、Haskell Script等をインストールしている場合は、それらを指定することも出来ると思います。

拡張ライブラリを利用する

LimeChatスクリプトの非公式な拡張ライブラリ、Orthoclaseを使うとこんなことが可能になります。
  • 外部スクリプトのインポート(evalと違ってエラーがちゃんと出る!)
  • イベント接続(WScript.ConnectObjectと同じ)
  • スクリプト間でデータを共有(文字列、数値、関数呼び出し)
  • 書式文字列変換(いわゆるString.Format)
  • ActiveXオブジェクトメソッドの途中引数を省略

タグ:

+ タグ編集
  • タグ:

このサイトはreCAPTCHAによって保護されており、Googleの プライバシーポリシー利用規約 が適用されます。

最終更新:2009年02月08日 10:32
添付ファイル