2011/06/12

Android端末のスクリーンショット保存を補助するスクリプト

今回は、過去に紹介したいくつかの機能を組み合わせて、Android端末のスクリーンショット撮影を補助するツールを作成する例を紹介します。
Android端末のスクリーンショットを保存する方法としては、Android SDKに同梱のDDMSというプログラムを使用するのが代表的ですが、1枚の画像を保存するために「更新ボタンを押す→保存ボタンを押す→保存ファイル名を指定」という手間が必要で、あまり使い勝手がよくありません。
そこで、特定のキーを押すだけで最新の画面を連番ファイル名で保存出来るようにします。
var Clipboard=require('Clipboard').Clipboard;
var dir=cwd().directory("E:\\docs\\work\\NR_android\\ss");
var prefix="ss_", len=3, num=1;

Main.createNotifyIcon();
Main.process.priority=Process.priority.high;

require('Hotstrokes').Hotstrokes.defineConditions({
    DDMS:function()(this.activeWindow.title=='Device Screen Capture'),
}).map('DDMS',{
    "NumEnter":function(){
        Thread.create(function(){
            var fmt=prompt('change prefix / number'), m;
            if(fmt.match(/^\d+$/)){
                num=fmt*1;
            }else if(m=fmt.match(/^(.*?)(\d+)$/)){
                prefix=m[1];
                len=m[2].length;
                num=m[2]*1;
            }else{
                prefix=fmt;
            }
            Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
        });
    },
    "NumMinus":function(){
        num--;
        Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
    },
    "NumPlus":function(){
        num++;
        Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
    },
    "Num0":function(){
        Thread.create(function(w){
            w.control('Button',0).lbuttonClick();
            sleep(800);
            w.control('Button',3).lbuttonClick();
            sleep(500);
            var file=dir.file(prefix+num.format(len)+'.png');
            try{
                var img=Clipboard.image;
                img.save(file);
            }finally{
                free(img);
            }
            Main.notifyIcon.showInfo('Saved: '+file);
            num++;
        },this.activeWindow);
    },
}).register();
キーボード操作への割り当てにはHotstrokesを使用します。端末の操作をしながら撮影操作を行なうため、押しやすいテンキーの0に割り当てることにします。
DDMSのキャプチャウィンドウがアクティブな時だけ有効になるように、ウィンドウタイトルで判定する条件を定義して、その条件下への割り当てを行ないます。

画像の保存は、ウィンドウ上のボタンの自動クリックにより画面の更新とクリップボードへのコピーを実行させた後、Clipboardユニットの画像取得機能で取得した画像を保存することで実現しています。
ボタンを押してからDDMS側で処理が完了するまでにはタイムラグがあるので、適当にsleep()を入れて待機します。
一連の処理には時間がかかるため、キー操作をブロックしないように新規スレッド上で行なうようにしています。

Clipboard.imageで取得したImageオブジェクトはfree()で解放しなければならないことに注意が必要です。
(Clipboardユニットの開発時点では、取得の度にコピーを生成しなければならないケースを想定していなかったため、このような仕様になっています)

その他、プレフィクスや番号をプロンプトで指定する機能や、間違えて撮影したときのために連番を巻き戻す機能を用意しました。
保存先ディレクトリは、頻繁に変更するものではないので、スクリプト上にハードコーディングしています。

簡単なスクリプトですが、通常のスクリーンショット保存手順に比べるとかなり手間が軽減できるはずです。

2011/05/31

ZIPファイル内のファイルを解凍したり直接読み込む

NILScriptのZLibユニットに用意されているZIPクラスを利用すれば、ZIPファイルの圧縮や解凍を行なえます。

ZIPファイル内の全てのファイルを指定のディレクトリに解凍するには、以下のようにZIPクラスのextract()メソッドを使用します。
require('ZLib').ZIP.extract(zipfile, directory);

extractText()やextractBytes()メソッドでは、ZIPファイル内のファイルをローカルファイルに保存することなく直接変数に読み込むことが出来ます。
extractText()は文字列、extractBytes()はポインタオブジェクトを返します。
以下の例では、ZIPファイル内の「changelog.txt」の内容を表示します。
巨大なZIPファイル内の小さなファイルだけを利用したい場合などには、特に効率化が期待できます。
println(require('ZLib').ZIP.extractText('F:\\downloads\\nil.zip','changelog.txt'));


ZIPクラスのインスタンスオブジェクトを作成すれば、更新日時や拡張子などの条件に一致するファイルだけを解凍したり、ファイルの解凍や読み込みは行なわずにサイズや更新日時などの情報を取得して利用するなど、様々な処理を行なえます。
以下の例では、ZIPファイル内のファイルとローカルファイルを比較し、ZIPファイルにだけ存在するファイルやZIPファイル内の方が新しいファイルを列挙します。
var dir=Main.scriptDirectory;
var file="F:\\downloads\\nil.zip";
try{
    var zip=require('ZLib').ZIP.open(file);
    for(let f in zip.descendants){
        var lf=dir.file(f.path);
        if(f.isDirectory){
        }else if(!lf.exists){
            println('[new] '+f.path);
        }else if(f.time>lf.mtime){
            println('[updated] '+f.path);
        }
    }
}finally{
    free(zip);
}
ZIPオブジェクトのdescendantsプロパティで、ZIPファイル内の全てのファイルがZIPFile/ZIPDirectoryオブジェクトとして列挙できます。
ZIPDirectoryオブジェクトを判別するために、isDirectoryプロパティが用意されているので、これを利用して除外します。
「Main.scriptDirectory」などのDirectoryオブジェクトのfile()メソッドの引数にZIPFileオブジェクトのpathプロパティを指定すれば、ZIP内のパスに対応するローカルファイルのFileオブジェクトを得られます。
ZIPFileのtimeプロパティでは、更新日時をDateオブジェクトとして得られます。これをFileオブジェクトのmtimeプロパティと比較すれば、ファイルが新しいかどうかが分かります。

その他、ZIPFile/ZIPDirectoryオブジェクトには、指定のローカルファイルとして解凍するcopy()メソッドなどが用意されています。ZIP関連の機能の詳細は、NILScriptに同梱のdoc\ZLib.txtを参照して下さい。

2011/05/29

ファイルをZIP圧縮する

ZIPなどのアーカイブファイル形式は、複数のファイル群や巨大な圧縮されていないファイルをネット経由で公開・送受信したい場合に役立ちます。
今回は、NILScriptでZIPファイルを作成する方法について説明します。

ZIPファイルの読み書きを行なうには、ZLibユニットのZIPクラスを使用します。
単に一つのディレクトリ配下のファイルをZIPファイルにするだけなら、以下のようにZIPクラスのcompress()メソッドをZIPファイルとディレクトリのパスを引数にして呼び出すだけです。
require('ZLib').ZIP.compress(zipfile, directory);

以下の例では、正規表現に一致するファイルを除外して、残りのファイルだけを圧縮します。
var exclude=/\\nil\\(release|test\d*)\.ng|\\nil\\users|\\users\\Owner/;
var src=Main.scriptDirectory;
var dest="E:\\www\\public_html\\nil.zip";

try{
    var zip=require('ZLib').ZIP.create(dest);
    (function(from,to){
        for(var f in from.children){
            if(!String(f).match(exclude)){
                println(f.path);
                if(f instanceof Directory){
                    arguments.callee(f, to.directory(f.name).create(f.mtime));
                }else{
                    to.add(f);
                }
            }
        }
    })(src,zip);
}finally{
    free(zip);
}
まず、ZIPファイルのパスを引数にしてZIP.create()を呼び出し、ZIPオブジェクトを取得します。
次に、Directoryオブジェクトのchildrenプロパティで圧縮対象ディレクトリ内のファイル/ディレクトリを列挙し、条件に一致するファイルをZIPファイルに追加していきます。

ZIPファイルの直下にファイルを追加するには、追加したいファイルを表すFileオブジェクトを引数として、ZIPオブジェクトのadd()メソッドを呼び出します。

Directoryオブジェクトを引数にadd()メソッドでを呼び出せば、配下のファイルを全て追加できますが、今回はサブディレクトリ内もフィルタリングしたいので、ZIPファイル内にディレクトリを作成したあと、再帰呼び出しで配下のファイルの処理を行なうことにします。

ZIPファイル内にディレクトリを作成するには、ZIPオブジェクトのdirectory()メソッドでZIPDirectoryオブジェクトを取得し、そのcreate()メソッドを呼び出します。create()メソッドの返り値は、そのZIPDirectoryオブジェクト自身です。
create()メソッドの引数には、格納するディレクトリの更新日時を表すDateオブジェクトを指定できます。

ZIPDirectoryオブジェクトも、ZIPオブジェクトと同様に、add()メソッドでディレクトリ内のファイルとしてファイルを追加したり、directory()メソッドで子ディレクトリを表すZIPDirectoryオブジェクトを取得できるので、共通の関数で再帰処理を行なえます。

処理が完了したら、free()関数でZIPオブジェクトを解放する必要があることに注意して下さい。

この他のZIPファイル読み書き機能については、NILScriptに同梱のdoc\ZLib.txtを参照して下さい。

2011/05/11

生成したプロセスの標準入出力を別のプロセスにリダイレクト

NILScriptでは、「Process.create()」やそのエイリアスである「run()」で外部プロセスを起動できます。
このメソッドでは、第2引数にオブジェクトとして様々なオプションを指定する事で、標準入出力の扱いなどを柔軟に設定出来ます。
例えば、「stdin」オプションにあらかじめProcess.create()で生成しておいた別のProcessオブジェクトを指定すると、そのプロセスの標準出力を次のプロセスの標準入力にリダイレクト出来ます。
コマンドプロンプトで「|」で複数のコマンドを繋いだときのように、出力と入力を直接繋ぐので、スクリプト側で読み出しと書き込みを行うよりも効率的に動作します。
他にも、ファイルやNULデバイスへのリダイレクトも可能です。これらの機能の詳細は、NILScriptに同梱のdoc\base_task.txtを参照して下さい。

以下は、この機能の利用例です。
//プログラムのパスと保存先を指定
var rtmpdump='D:\\bin\\rtmpdump-2.1d\\rtmpdump.exe';
var ffmpeg='D:\\bin\\ffmpeg.exe';
var dir='F:\\downloads';

var url=prompt('Enter MySpace URL(artist or song)','http://www.myspace.com/astralmess');
var swf='http://lads.myspacecdn.com/videos/MSMusicPlayer.swf';
var http=new (require('HTTP').HTTP)();

var downloadSong=function(sid){
    var xml=http.getText({
        url:'http://www.myspace.com/music/services/player?songId='+sid
          +'&ptype=4&action=getSong&el='+encodeURIComponent(url)+'&sample=0',
        referer:swf,
    });
    var title=xml.match(/<title>([^>]*)<\/title>/)[1].unescapeHTML();
    var rtmp=xml.match(/<rtmp>([^>]*)<\/rtmp>/)[1].unescapeHTML();
    println('Downloading: '+title);
    var p1=Process.create('"'+rtmpdump+'" -r '+rtmp+' -W '+swf+' -o -',{
        show:0
    });
    var p2=Process.create('"'+ffmpeg+'" -i - -acodec copy -metadata title="'
                    +title+'" "'+dir+'\\'+title.format('asfE')+'.mp3"',{
                        stdin:p1,
                        stdout:"",
                    });
    p2.wait('exit');
};

var m;
if(m=url.match(/^http:\/\/www\.myspace\.com\/[^\/]*\/music\/songs\/.*\-(\d+)$/i)){
    downloadSong(m[1]);
}else if(m=url.match(/^http:\/\/www\.myspace\.com\/[^\/]*\/?$/)){
    var html=http.getText(url);
    m=html.match(/<param[^>]*value="[^>"]*MSMusicPlayer\.swf"[^>]*>[\s\S]*?<param name="flashvars" value="([^">]*)"/)
    var param={};
    for(let pair in $G(m[1].split(/&/g))){
        var [,name,value]=pair.match(/^(.*?)=(.*)$/);
        param[name]=value;
    }
    http.getText({
        url:'http://www.myspace.com/music/services/player?artistUserId='
            +param.profid+'&playlistId='+param.plid
            +'&action=getArtistPlaylist&artistId='+param.artid,
        referer:swf,
    }).grep(/<song\s(?=[^>]*playType="FullSong")[^>]*songId="(\d+)"/g).map("1").execute(downloadSong);
}
音楽系SNS「MySpace」の指定したURLのページからプレイリストを抽出し、ストリーミングURLを「rtmpdump」でダウンロードし、「ffmpeg」でFLVからMP3を抽出して保存するというスクリプトですが、rtmpdumpの出力をffmpegにリダイレクトすることで、一時ファイルを作らずに処理を完了しています。
太字になっている部分が、ffmpegの標準入出力を指定している部分です。
「stdout:""」は、NULへのリダイレクトを示します。このようにして進捗などの出力を読み捨てないと、出力バッファが一杯になって処理が止まってしまう可能性があるので、注意が必要です。

2011/04/28

種文字列から一定の乱数列を生成する

NILScriptのCipherユニットには、種文字列を指定して乱数列を得るRandomクラスが用意されています。
通常のMath.random()と違うのは、同じ種文字列を指定すると常に同じ乱数列が生成されるという点です。
この機能を利用すれば、ゲームのステージ生成などで、生成されたステージを丸ごと保存しなくても、種文字列を保存しておくだけで過去に生成したステージを再現できるようになります。

下記の例は、Cipher::Randomを利用して迷路を生成するというスクリプトです。
コマンドライン引数に横の広さ、縦の広さ、種文字列を指定すると、迷路が生成され表示されます。
種文字列が省略された場合は、String.random()で生成したランダムな文字列が使用されます。
var w=(Main.params[0]||32)*2-1, h=(Main.params[1]||18)*2-1;
var r=new (require('Cipher').Random)(Main.params[2]||String.random(16)); //(1)
var map=[],joints=[];
//周辺の枠の生成
map.push(('-'.times(w-1)+'+').split(''));
for(var x=2,m=w-2;x<m;x+=2){
    joints.push({x:x,y:0});
    joints.push({x:x,y:h-1});
}
for(var y=1;y<(h-1);++y){
    map.push(('|'+' '.times(w-2)+'|').split(''));
    if(!(y&1)){
        joints.push({x:0,y:y});
        joints.push({x:w-1,y:y});
    }
}
map.push(('+'+'-'.times(w-1)).split(''));

//壁の生成
var j;
while(joints.length){
 //新たに生やし始める箇所を選択する
    if(!j){
        j=joints[r.next(joints.length)]; //(3)
    }
    //壁を延ばす方向の候補を列挙
    var c=[];
    if((map[j.y-2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y-1}, next:{x:j.x  ,y:j.y-2}, mark:'|'});
    }
    if((map[j.y+2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y+1}, next:{x:j.x  ,y:j.y+2}, mark:'|'});
    }
    if((map[j.y]||[])[j.x-2]==' '){
        c.push({via:{x:j.x-1,y:j.y  }, next:{x:j.x-2,y:j.y  }, mark:'-'});
    }
    if((map[j.y]||[])[j.x+2]==' '){
        c.push({via:{x:j.x+1,y:j.y  }, next:{x:j.x+2,y:j.y  }, mark:'-'});
    }
    //延ばせる方向があれば、そのうちのどこかに延ばす
    if(c.length){
        var m=c[r.next(c.length)]; //(4)
        map[m.via.y][m.via.x]=map[m.next.y][m.next.x]=m.mark;
        if(map[j.y][j.x]!=m.mark){
            map[j.y][j.x]='+';
        }
        joints.push(j=m.next);
    }else{
        joints.remove(j);
        j=null;
    }
}
map[1][0]=map[h-2][w-1]=' ';
//表示
for(var y=0;y<h;++y){
    var l=[];
    for(var x=0;x<w;++x){
        l.push(map[y][x]);
        if(x&1){
            l.push(map[y][x]);
        }
    }
    println(' '+l.join(''));
}
println('Seed='+r.seed);
free(r); //(2)

最初に「r」変数にRandomオブジェクトを格納します。(1)
Randomオブジェクトは、不要になったら「free()」でリソースを解放する必要があります。(2)

迷路の生成には、既存の壁から壁を生やして、他の壁にぶつからないように延ばしていくという方式を使用しています。
新たに壁を生やし始める箇所を選択する処理(3)と、壁を延ばす方向を選択する処理(4)で乱数を使用しています。
Randomオブジェクトのnext()メソッドは、Number.random()と同じように、指定した範囲の整数を返します。

上記のスクリプトを「20 10 XYZ」を引数にして実行すると、以下のような迷路が表示されるはずです。
 ---------------------------------------------------------+
                                                          |
 |  ---+  +-----------+  +-----+---  +--------+  +--+  +--+
 |     |  |           |  |     |     |        |  |  |  |  |
 |  +--+  |  +-----+  |  |  |  +-----+  +--+  |  |  |  |  |
 |  |     |  |     |     |  |           |  |  |  |  |  |  |
 |  +--+  +--+---  +-----+  +--------+  |  |  +--+  +--+  |
 |     |                             |  |  |           |  |
 |  +--+  +--------+  +-----+-----+--+  |  +--+  +--+  |  |
 |  |     |        |  |     |     |     |     |  |  |     |
 |  +-----+  +-----+  +--+  |  +--+  +--+--+  +--+  +--+  |
 |           |           |  |  |     |     |           |  |
 +--+  ---+  +--------+  |  |  +-----+  +--+  +-----+  |  |
 |  |     |           |  |  |           |     |     |  |  |
 |  +--+  |  +---  +--+  |  |  |  +-----+  +--+  +--+  |  |
 |     |  |  |     |  |     |  |  |        |  |  |     |  |
 |  ---+--+  +-----+  +-----+--+  +---  +--+  |  +-----+  |
 |                                      |
 +--------------------------------------+------------------

2011/04/14

FTPでログファイルなどをレジュームダウンロードする

NILScriptのFTPユニットを利用すれば、自分の管理するWebサイトにFTPで接続してアクセスログなどのファイルを定期的に自動ダウンロードさせたりすることも可能です。

末尾に追記されていくタイプのログファイルの場合、毎回丸ごとダウンロードするのではなく、取得済みのバイト数だけスキップして未取得の部分だけをダウンロードし、ローカルの取得済みファイルに追記するレジュームダウンロードを行うと、無駄な転送を減らせて効率的です。

NILScriptのFTPユニットでは、下記の例のように、FTPオブジェクトのfile()メソッドで取得したFTPFileオブジェクトのdownload()メソッドを呼び出すとき、第2引数にtrueを指定すれば、第1引数に指定した保存先ファイルが存在した場合にそのファイルにレジュームダウンロードが行われます。

try{
    var ftp=new (require('FTP').FTP)({
        host:'ftp.example.com',
        user:'aaa',
        password:'bbb',
    });
 var f=ftp.file('/public_html/access.log');
 f.download('R:\\access.log',true);
}finally{
    free(ftp);
}

第2引数を省略したりfalseを指定した場合は、既存の保存先ファイルの内容は上書きされます。
画像などの追記型でないファイルをレジュームダウンロードしてしまうと、ファイル破損などの問題が生じるので注意して下さい。

FTPでファイルをアップロードする

Webサーバへのファイルアップロードなどで用いられているFTPによるファイル転送やファイル情報の取得機能を提供する「FTP」ユニットが新たに追加されました。
この機能を利用すれば、NILScript上でコンテンツを生成してアップロードまで行うことが出来ます。

以下は、FTPによる基本的なファイルアップロードの例です。

try{
    var ftp=new (require('FTP').FTP)({
        host:'ftp.example.com',
        user:'aaa',
        password:'bbb',
    });
    var d=ftp.directory('/public_html');
    d.upload('C:\\www\\index.rdf');
}finally{
    free(ftp);
}

FTPクラスのコンストラクタに、接続先サーバのホスト名やログインユーザー名、パスワードなどのメンバを持つオブジェクトを引数として与えると、サーバへの接続とログイン処理が行われ、インスタンスが生成されます。
FTPインスタンスオブジェクトの「directory()」メソッドを、サーバ上の絶対パスを引数にして呼び出すと、そのディレクトリに関する操作を提供するFTPDirectoryオブジェクトが得られます。
このFTPDirectoryオブジェクトの「upload()」メソッドをローカルファイルのパスやFileオブジェクトを引数にして呼び出すことで、ファイルのアップロードが行われます。

FTPDirectoryオブジェクトには、他にもディレクトリ内のファイルやディレクトリを列挙するchildrenメンバなどの機能が用意されています。各機能の説明は、同梱のdoc/FTP.txtを参照して下さい。

2011/04/08

Mailユニットで送受信するメールを暗号化する

Mailユニットに用意されているPOP3クラスを使用すれば、定期的に新着メールをチェックして特定の書式のメールがあったときに本文に記述された命令を実行するというスクリプトを起動しておき、外出先からメールで自宅のPCを操作することも可能です。
しかし、何者かに偽の命令メールで勝手に操作されてしまっては困ります。
命令メールの送信をPC上から行うのであれば、送信時に内容を暗号化するようにすればよいでしょう。

メールを暗号化するには、Cipherユニットを使用し、以下のようにします。
var subject='NILScript:Command';
var addr='test@example.com';
var msg="HELLO";
var key='password';
var opt={cipher:"AES-128-CFB",salt:true,encoding:"utf8"};
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    smtp.send({
        subject:subject,
        from:addr,
        to:addr,
        message: require('Cipher').Cipher.encode(JSON.stringify(
            {time:now().getTime(),text:msg}
        ),key,opt),
    });
}finally{
    free(smtp,cp);
}
Cipherクラスのencode()メソッドを、平文データとパスワード、暗号化オプションを引数にして呼び出すことで、暗号化されたBASE64文字列を取得できます。
これをSMTPオブジェクトのsend()メソッドの引数オブジェクトのmessageメンバに指定して、メール本文として送信します。
なお、BASE64文字列ではなくバイト列へのPointerオブジェクトを返すCipher.encodeToBytes()などのメソッドも用意されています。

受信側では、以下のようにスレッドを生成して定期的にPOP3サーバに接続し、規定の件名のメールがあったときに、本文をCipher.decode()で復号化します。
この例では「Main.notifyIcon.showInfo()」で本文を表示しているだけですが、eval()関数に渡せば、任意のNILScript文を実行させたりもできます。
var subject='NILScript:Command';
var addr='test@example.com';
var msg="HELLO";
var key='password';
var opt={cipher:"AES-128-CFB",salt:true,encoding:"utf8"};
Main.createNotifyIcon();
var last=now().getTime();
Thread.create(function(){
    while(true){
        try{
            var pop3=new (require('Mail').POP3)({
                host:'vm01xp',
                user:'test',
                password:'asdf',
            });
            
            for(let m in pop3.items){
                if(m.subject==subject){
                    var {text,time}=JSON.parse(
                        require('Cipher').Cipher.decode(m.message,key,opt)
                    );
                    if(text && time && (time>last)){
                        Main.notifyIcon.showInfo(text);
                        last=time;
                    }
                    m.remove();
                }
            }
        }catch(e){
            println(e);
        }finally{
            free(pop3);
        }
        sleep(60000);
    }
});

セキュリティを強化するため、単に命令本文を暗号化するのではなく、実際の命令本体と命令の送信日時の2つのメンバからなるオブジェクトのJSON文字列を暗号化して、受信側では前回実行した命令よりも新しい命令でないと実行しないようにしています。
こうしておかないと、メールを傍受した何者かが、後でそのメールを送りつけることで過去に送信された命令を任意のタイミングで再実行できてしまいます。
ハッシュなどによる改竄チェックは行っていないので、出鱈目なBASE64文字列が送りつけられても復号化してJSON文字列の解析まで行われてしまいますが、偶然timeとtextのメンバを持つオブジェクトのJSON文字列として正しくデコード出来てしまう可能性は低いので問題ないでしょう。

POP3でメールを受信する

最近追加されたMailユニットには、メールの受信を行うPOP3クラスが用意されています。
この機能を利用すれば、新着メールをチェックするツールや、メールで受信した命令に従って処理を自動実行するツールなどを実現できます。

POP3クラスの基本的な使用方法は以下のようになります。
try{
    var pop3=new (require('Mail').POP3)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
    });
    for(let m in pop3.items){
        println(m.subject);
    }
}finally{
    free(pop3);
}
SMTPの場合と同じように、new POP3()でインスタンスを生成すると、サーバとの接続やログイン処理が行われ、free()でログアウトと切断が行われます。
POP3オブジェクトは、「count」プロパティでメールボックス内のメールの件数が取得したり、「items」プロパティで各メールの情報取得を行うRemoteMailオブジェクトを列挙できます。
RemoteMailオブジェクトには、ヘッダを受信して件名を返す「subject」や、本文テキストを返す「message」などのプロパティが用意されています。また、「remove()」メソッドで、サーバ上からメールを削除することも可能です。
上記の例では、サーバ上のメールの本文を表示しています。

スクリーンショットをメール送信する

NILScriptのMailユニットのSMTPクラスでは、添付ファイルの送信も可能です。
添付ファイルには、Fileオブジェクトの他、メモリ上のバイト列を指すPointerオブジェクトを指定することも可能です。
画像を扱うImageオブジェクトには、ファイルに書き込むことなくPNGやJPEGなどの画像ファイルのバイト列を生成してPointerオブジェクトで得るtoBytes()というメソッドが用意されているので、以下のようにデスクトップのスクリーンショットなどをファイルに保存することなくメール送信することが可能です。
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    var img=require('Image').Image.capture();
    var buf=img.toBytes('.png');
    smtp.send({
        subject:"ScreenShot",
        from:'test@example.com',
        to:'test@example.com',
        message:"test",
        files:{
            'image/png:ss.png':buf
        },
    });
}finally{
    free(smtp,buf,img);
}
「require('Image').Image.capture()」でデスクトップのスクリーンショットをImageオブジェクトとして取得し、img変数に代入します。
次に、img.toBytes('.png')でPNGファイルのバイト列を生成しています。
これらのオブジェクトは、最後にfree()関数で解放する必要があります。
そして、SMTPオブジェクトのsend()メソッドの引数で、filesメンバに添付したいファイルの情報を与えます。
添付するのがFileオブジェクトの場合は、FileオブジェクトそのものやFileオブジェクトの配列として指定するだけで、元のファイル名で添付できますが、Pointerオブジェクトの場合はファイル名を指定する必要があるので、上記のように「"content-type:filename":data」のような形式のオブジェクトにして指定します。
Content-Typeの部分を省略して「"filename":data」のようにすると、Content-Typeは「application/octet-stream」として添付されますが、メールソフトの添付画像表示機能などが効かなくなる場合があるので、形式が分かるならなるべく指定しておいた方がいいでしょう。

処理完了などのイベントをメールで知らせる

最近のアップデートで、SMTP/POP3サーバに接続してメールの送受信を行う「Mail」ユニットが追加されました。
この機能を使えば、自動処理の完了など何らかのイベントの発生時にメールを送信して知らせたり、外出先からメールで送信した指令に応じて自動処理を行うことなどが可能になります。

SMTPクラスによるメール送信の基本的な手順は以下の通りです。
try{
    var smtp=new (require('Mail').SMTP)({
        host:'vm01xp',
        user:'test',
        password:'asdf',
        pop:true,
    });
    
    smtp.send({
        subject:'テストメール',
        from:'test@example.com',
        to:"test@example.com',
        encoding:'iso-2022-jp',
        message:"テスト本文",
    });
}finally{
    free(smtp);
}

まず、SMTPクラスのコンストラクタにホスト名やユーザー、パスワードなどのオプションを与えて呼び出し、インスタンスオブジェクトを得ます。この時、サーバとの接続が確立され、ログイン処理などが行われます。
POP before SMTPを使用するには、オプションに「pop:true」を指定します。未指定時はSMTPのコマンドによる認証が行われます。

こうして得たSMTPオブジェクトのsend()メソッドに、件名や宛先、本文などを指定するオプションオブジェクトを与えて実行することで、メールの送信が実行されます。
「encoding:'iso-2022-jp'」は、本文や件名を日本語の7ビット文字コードとして一般的なiso-2022-jpでエンコードすることを指定しています。
未指定時はutf-7でエンコードされますが、古い日本製のメールソフトなどではデコードできないかもしれないので、人間に読ませる日本語メールを送信する場合はこのオプションを指定しておいた方がよいでしょう。

メールの送信が終ったら、「free(smtp)」のようにしてサーバからの切断を行います。
この処理は、エラー発生時でも確実に実行されるように、try...finallyのfinally説で実行するようにします。

その他の細かな機能の説明などは、NILScriptに同梱のdoc\Mail.txtを参照して下さい。

2011/03/30

ノートPCのAC電源復旧時にWoLでデスクトップPCを起動

停電時でもPCに稼働し続けて欲しければ、バッテリを搭載しているノートPCを使用するのが無難ですが、PCI接続の機器などを使う必要がある場合、デスクトップPCを選択せざるを得ません。
デスクトップPCでは、バッテリによる予備電源を提供するUPS(無停電電源装置)を利用しても、長時間に及ぶ停電を無停止で乗り切ることは出来ません。
また、USB接続などでPCと連動して停電時に自動で安全なシャットダウンを行う機能は多くのUPSに用意されていますが、電源復旧時にPCを自動で起動する機能は用意されていないことがあり、外出などで手動での電源投入が行なえない状況では、長時間シャットダウンされたままになってしまうことがあります。

どうしてもデスクトップPCの停止時間を最小限にとどめたければ、別途ノートPCを起動しておき、そこからデスクトップPCを自動起動させるとよいでしょう。

NILScriptでは「System.hasAC」で電源が供給されているかどうかを取得できる他、System.observe()で「powerStatusChange」イベントにコールバック関数を登録することで、電源の状態が変化したときに任意の処理を実行させられます。
また、LAN上にマジックパケットをブロードキャストして指定のコンピュータを起動させるWOL(WakeOnLAN)の機能も最近追加されました。
これらの機能を利用して以下のようなスクリプトを作成し、AC電源に接続したノートPC上で実行しておけば、AC電源が断たれた状態から復旧したときに自動で指定のPCを起動してくれるはずです。

var mac="00 01 02 03 04 05";//起動対象PCのMACアドレス
var dest="192.168.1.255"; //パケット送信先のブロードキャストアドレス

Main.createNotifyIcon();
var power=System.hasAC;

System.observe('powerStatusChange',function(){
 if(!power && System.hasAC){
  require('WOL').WOL.send(mac,dest);
 }
 power=System.hasAC;
});

なお、WOLでPCを遠隔起動するには、対象のPCがWOLに対応したLANカードでルーターに有線接続されていて、BIOSの設定でWOLが有効になっている必要があります。
また、停電でルーターが停止してしまう場合、powerStatusChangeイベントで電源復旧を検出した直後にWOL.send()を行ってもパケットが到達しない可能性があるため、適宜sleep()などを挿入して下さい。

2011/03/29

録画したテレビ番組のL字を除去する

大災害が発生したりすると、テレビ番組の画面の周囲にL字状の枠が追加されて被害情報などが表示されますが、録画した番組を後で視聴するときには邪魔でしかありません。
エンコード時にクロップして除去してしまえばよいのですが、CMなどでL時の有無が切り替わる部分ではフェードイン・アウトが行われるため、綺麗に除去するエンコード定義を手動で作成するのは非常に面倒です。
そこで、動画からL字の出現範囲を自動検出して、フェードイン・アウトの部分まで正確に除去するAviSynthスクリプトを生成するというNILScript用スクリプトを作成しました。

AviSynthは、動画の読み込みや加工処理を定義するスクリプト言語の処理系です。
AviSynthをインストールすると、VideoForWindows APIで拡張子「.avs」のスクリプトを動画として読み込めるようになります。
VirtualDubなどのソフトで読み込んで編集・エンコードする一般的な使い方ですが、NILScriptのVideoユニットもVFWを利用しているためAviSynthスクリプトを読み込み可能で、今回のスクリプトでも映像処理の大部分はAviSynthスクリプトで行っています。

NILScriptのVideoユニットは、このL字除去スクリプトを作成するために暫定的に実装されたものなので、限られた機能しか用意されていませんが、このスクリプトのようにAviSynthと組み合わせれば、様々な動画処理ツールを手軽に作成できるでしょう。


スクリプトと定義データ一式は以下のURLからダウンロードできます。
使用するには、別途NILScript本体とAviSynth、MPEG-2 VIDEO VFAPI Plug-Inが必要です。
スクリプトの内容や詳しい使用方法は、上記アーカイブ内のファイルを参照して下さい。

http://lukewarm.s151.xrea.com/rem.zip


L字の検出には、L字の左上辺りの映像が不変である部分のサンプル画像を用意しておき、動画の各フレーム画像との差を調べるという手法を用いています。
ほとんど全てのピクセルが一致した場合に完全なL字とみなし、それ以外はL字無しかフェードイン・アウト中のフレームとみなすことにしました。

スクリプトが実行されると、各フレームのL字の有無を示す小さな画像からなる動画を出力するAviSynthスクリプトを生成して、Videoユニットで読み込み、フレームを一定間隔で飛ばしながらL字の有無を調べます。
L字の有無が切り替わっている箇所が見つかったら、間のフレームを調べて正確な境目を探します。
ここでも、最初に範囲の中央を調べ、既に切り替わっていたら範囲の前半、切り替わっていなければ範囲の後半を再帰的に調べるという二分探索的な手法を採ることで高速化しています。
フェードイン・アウトのフレーム数は放送局ごとに一定なので、完全にL字である範囲がわかれば、フェードイン・アウトの範囲と、L字無しの範囲も確定します。
動画の末尾まで調べたら、元の動画を読み込んで各部分に対応するフィルタを適用して連結するというAviSynthスクリプトを構築して保存すれば処理完了です。

2011/03/24

動画ファイルから画像を取得してサムネイル生成などを行う

動画ファイルから寸法などの情報や任意のフレームの画像を取得できる「Video」ユニットを暫定的に実装しました。

動画のフレームは、Imageユニットで定義されているImageオブジェクトとして取得され、リサイズなど様々な処理が可能です。

下記のスクリプトは、コマンドライン引数で指定した動画ファイルから等間隔にフレームを抜き出し、縮小して並べた画像をファイルに保存するという例です。
var file=cwd().file(Main.params[0]);
var scale=4;//縮小率
var cx=4, cy=4;//横、縦に並べる数
try{
    var vf=require('Video').VideoFile.open(file);
    var st=vf.openVideo();
    var dist=Math.floor(st.length/(cx*cy));
    var w=Math.floor(st.width/scale), h=Math.floor(st.height/scale);
    var img=require('Image').Image.create(w*cx,h*cy);
    for(var i=0;i<cy;i++){
        for(var j=0;j<cx;j++){
            try{
                var frame=st.getFrazme((i*cx+j)*dist);
                img.drawImage(frame,w*j,h*i,w,h);
            }finally{
                free(frame);
            }
        }
    }
    img.save(cwd().file(file.name+'.jpg'),{quality:80});
}finally{
    free(st,vf,img);
}
実行すると、左記のような画像ファイルが生成されます。


現在の所、VideoForWindowsのAPIを利用しているため、AVIなど限られた形式にしか対応していませんが、AviSynthをインストールして、DirectShowSource()などで動画を読み込むAVSスクリプトを作成してそれを読み込むようにすれば、MPEGなど他の形式にも対応させられるはずです。

2011/03/06

非アクティブのウィンドウにキー操作を送信

GUIプログラムの自動操作を行うとき、対象のウィンドウをいちいちアクティブにしていたのでは、他の作業の邪魔になってしまいます。
ウィンドウを非アクティブのままで操作したければ、キー操作時に送られるウィンドウメッセージを送信する「sendKeys()」を使うといいでしょう。(最近追加された機能なので、使用するには最新版にアップデートして下さい)

下記の例は、ある音楽プレイヤーソフトにPauseのショートカットキーを送信する例です。
require('Window').Window.find('{DA7CD0DE-1602-45e6-89A1-C2CA151E008E}').sendKeys('[x]');
「require('Window').Window.find('...')」で、指定のクラス名を持つウィンドウの内、一番手前のものを表すWindowオブジェクトが返されます。
そのsendKeys()メソッドを呼び出すことで、ウィンドウメッセージによる擬似的なキーストロークの送信を行います。
大抵のソフトでは、タスクトレイに格納されたりして非表示になっていても、ウィンドウ自体は非表示の状態で存在しているため、上記の要領で操作可能です。

ショートカットキーは、親ウィンドウではなく配下のコントロールに送信しなければ動作しない場合があります。
配下のコントロールを表すオブジェクトを取得するには、「control()」メソッドを使用します。
次の例は、「TVXEForm」というクラス名の一番手前のウィンドウの配下の「TEditorX」というクラス名のコントロールに「Ctrlキーを押しながらsキーを押して放しCtrlキーを放す」というキーストロークを送信するというものです。
require('Window').Window.find('TVXEForm').control('TEditorX').sendKeys('Ctrl+[s]');
controlの第2引数に数値を指定すれば、複数の同クラス名コントロールの内、任意のオフセットのものを取得できます。未指定時は0を指定したものとみなされ、最初の物が取得されます。

sendKeysで指定するキーストロークの定義には、NILScriptの「Hotstrokes」ユニットで使用されているストローク定義文字列の内、通常のキーボードのキーのみで構成された物が指定できます。
Hotstrokesでは様々なキーストロークを定義するためのルールがありますが、標準的なショートカットキーを送信するだけなら、修飾キー名(Shift/Ctrl/Alt)に「+」を付けたものに続いてキー名を「[]」で囲んで記述するという方式を覚えておけば十分でしょう。

なお、送信されるキーストロークの内、修飾キーの部分だけはウィンドウメッセージではなくシステム全体に影響する方式で擬似的なキーストローク生成が行われます。
これは、多くのソフトのショートカットキー認識では、修飾キーの状態をシステムに問い合わせて取得しており、ウィンドウメッセージでCtrlの押し下げを送信しても無視されてしまうためです。
このため、修飾キーを伴うショートカットキーを送信すると、他のソフトに意図しない操作が認識されてしまう可能性があります。
できれば実行したい動作を「F1」のような修飾キーを伴わないショートカットキーに割り当てておき、それを送信すると良いでしょう。

2011/02/17

ContentExtractorのプラグインの仕様

NILScriptに同梱の「Notebook.ng」でWebページから本文を抽出するために用意されている「ContentExtractor」では、本文の抽出方法をプラグインとして追加できるようになっています。
現在のところ、はてなダイアリーやFC2ブログなどの代表的なブログサービスや、Vector、窓の杜、ITMediaなどのIT系情報サイトに対応するプラグインなどが用意されていますが、各自でプラグインを作成すればどんなサイトにでも対応させられます。
今回は、ContentExtractorプラグインの仕様について説明します。

基本的な定義

ContentExtractorのプラグインは、NILScriptのディレクトリ配下の「plugins\ContentExtractor」ディレクトリ内にプラグイン名のディレクトリを作って格納します。
各プラグインのディレクトリには、プラグイン名に拡張子「.ng」を付けたファイル名でプラグインのスクリプトファイルを格納します。
スクリプトファイル内には、以下のようなオブジェクト定義を記述します。

({
    description:String(<![CDATA[
        http://wiredvision.jp/の巡回と記事本文抽出。
    ]]>),
    level:50,
    rules:{
        atom:{
            urlPattern:/^http:\/\/(wiredvision\.jp|rss\.rssad\.jp\/rss\/wiredvision)\/.*\/atom\.xml$/i,
            inherit:"std/atom",
            childContentBodyPattern:true,
            replace:['NoTracker'],
        },
        article:{
            urlPattern:/^http:\/\/wiredvision\.jp\/.*\.html$/i,
            contentPattern:/<!--\s*google_ad_section_start\(name=s2\)\s*-->([\s\S]*?)<!--\s*google_ad_section_end\(name=s2\)\s*-->/i,
            sectionLinkPattern:/(<p><a href="http:\/\/wiredvision\.jp\/[^">]+">[^<]*へ続く<\/a><\/p>)/i,
            replace:[
                
            ],
        },
    },
})

オブジェクトの「rules」メンバの下にページの解析ルールを列挙し、各ルールのurlPatternメンバで、そのルールが対応しているページのURLにマッチする正規表現を指定します。
上記の「atom」ルールの「inherit:"std/atom"」は、stdプラグインで定義されている「atom」ルールを継承し上書きすることを示しています。
std/atomルールは、Atomフィードから各entry要素のlinkやcontentを子アイテムとして抽出するためのルールです。他に「std/rss」や「std/rdf」もあります。
これらのルールでフィードから項目を抽出する際には、アクセス解析用の中継URLをリダイレクト先のURLに置換したり広告項目を除去する機能などが自動的に適用されるようになっています。
「childContentBodyPattern:true」は、content要素を無視してlinkが示すWebページから対応するルールで抽出した本文を本文として使用することを示しています。
これは、フィードに含まれている本文が途中で終って「続きを読む」のリンクになっている場合に本当の本文を保存させるためなどに使用します。
「replace:['NoTracker']」は、フィードのsummary要素に仕込まれているアクセス解析用の不可視の画像を取り除く処理を定義しています。replaceの詳細については後で説明します。
このようなフィード用定義は、urlPatternとinheritの値以外はほとんど使い回せるので、コピペ・改編して利用するといいでしょう。

「article」ルールでは、記事のページから本文を抽出するルールを定義しています。
ここでは、「contentPattern」に指定された正規表現に従って、ページのHTMLから本文の部分が抽出されます。この時、正規表現中に「()」が使われている場合は、最初の括弧にマッチした部分が抜き出し結果となり、括弧がない場合はマッチ範囲全体が使われます。これは、前後の部分を目印に使いつつ、抜き出し結果には含めたくない場合などのための仕様です。途中に「(p|div)」のような括弧を使ったパターンを含めたい場合は、「(?:p|div)」のように、キャプチャ無し括弧を使ってください。
上記の例のように、コンテキストマッチング型広告サービスを利用しているサイトでは、本文の範囲を認識させるために埋め込まれたコメントタグを利用するのが手っ取り早いでしょう。

正規表現で抜き出しパターンを指定する際に特に注意すべきなのは「.」は改行にマッチしないということです。改行を含むあらゆる文字列にマッチさせるためには「[\s\S]」などと指定しなければなりません。
また、量指定子「*」や「+」は、そのままでは「貪欲」にマッチングを行うため、[\s\S]と組み合わせて使用する場合は、「?」を付けて「無欲」にする必要があります。
これを忘れて「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1><p>BBBB</p>」のようなHTMLに対して「/<h1>.*<\/h1>/i」のようなマッチングを行うと、「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1>」までが一致してしまったりします。
今回のcontentPatternの場合、終了タグが1回しか出現しないので貪欲でも問題はありませんが、無欲の方が余計な検索が行われず高速に動作するはずです。

「sectionLinkPattern」では、本文が複数のページに分かれているときに、以降のページにリンクしている部分を抽出・結合するためのパターンを指定します。
また、この例では登場しませんが、ページのタイトルをtitleタグ以外から抽出したい場合に指定する「titlePattern」や、ページの概要を抽出する「summaryPattern」などもあります。

これらの抜き出しパターンでは、正規表現以外にもいくつかの抽出方法が指定できます。
HTMLの要素に付けられたid属性で抽出するには、「contentPattern:['#contentBody']」のように指定します。
「contentPattern:function(html){/*何らかの処理*/;return(content);}」のように、HTML文字列を引数に受取り抜き出した文字列を返す関数を指定することも可能です。
他にもいくつかの方法がありますが、この3つを覚えておけばほとんどの状況には対応できるでしょう。


HTMLからの記事一覧の抽出

RSSフィードなどを配信していないサイトで、記事一覧のページから記事を抽出したい場合は、「childContentPattern」を使用します。
以下は、「vector」プラグインで定義されているカテゴリ別ソフト一覧のページの定義例です。

        soft_category:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/vpack\/filearea\/(?:win|mac)\/.*$/i,
            childContentPattern:/(<LI>\s*<A HREF="[^">]*\/se\d+\.html">[^<]*<\/A>\s*<IMG[^>]*>[\s\S]*?<BR>[\s\S]*?<\/LI>)/ig,
            childContentTitlePattern:/<a href="[^">]*\/se\d+\.html">(.*?)<\/a>/i,
            childContentInternalLinkPattern:/(<a href="[^">]*\/se\d+\.html">.*?<\/a>)/i,
            childContentSummaryPattern:/<\/A>([\s\S]*?)<\/LI>/i,
            childContentBodyPattern:true,
            
            sectionLinkPattern:/<div class="pagenav">([\s\S]*?)<\/div>/i,
            replace:[
                'NoImageLink',
                [/<img\b[^>]*alt="([^">]*)"[^>]*>([\s\S]*?)<br[^>]*>/ig,'[$1] $2'],
            ],
        },

まず、「childContentPattern」で各子アイテムのHTMLを抜き出します。
「childContentTitlePattern」、「childContentInternalLinkPattern」、「childContentSummaryPattern」は、抜き出した子アイテムのHTMLからタイトル、ページ本体へのリンク、概要を抜き出す定義です。
一覧の項目に本文が丸ごと含まれている場合は、「childContentBodyPattern」で本文の抜き出し定義を指定しますが、この例では本文はリンク先のページから抜き出す必要があるため、「childContentBodyPattern:true」を指定しています。
また、複数ページに分かれた一覧を全て取り出すため、「sectionLinkPattern」も指定しています。
std/atomなどのルールも、このようにして定義されています。


replaceによる置換ルール

contentPatternなどで抜き出した範囲に広告やブログパーツなどの不要部分が含まれてしまう場合は、「replace」に置換ルールの配列を記述して削除などを行います。
配列の要素として単なる正規表現を指定すると、その正規表現に一致する箇所が空文字列に置換されて削除されます。この時、全ての出現箇所を削除するには、正規表現に「g」オプションを付ける必要があることに注意してください。
削除ではなく別の内容に置換したい場合には、「[/<b>([\s\S]*?)<\/b>/ig,'$1']」のように、正規表現と置換文字列の2要素からなる配列を指定します。第2要素には、文字列のreplace()メソッドの第2引数と同じように、関数を指定することも可能です。
よく使われる置換ルールは、「HTMLRewriter」という機能のプラグインで定義されており、ルール名を単に文字列として指定するだけで使用できます。
標準で用意されている置換ルールの一覧と説明は、「plugins\HTMLRewriter\Cleanup\Cleanup.ng」内に書かれています。



画像の保存

contentPatternなどで抽出された本文にimgタグで埋め込まれている画像は、抽出時に自動的に保存されますが、aタグでリンクされているだけの画像は、そのままでは保存されません。
画像が保存されるようにするには、以下のようにして明示的にダウンロードすべきコンテンツのURLであることを指定します。
        image:{
            urlPattern:/^http:\/\/blog-imgs-\d+-origin\.fc2\.com\/.*\.(?:jpe?g|png|gif|bmp)$/i,
            download:true,
        },

本文からリンクされているのが画像ファイル本体でなく画像を表示するためのHTMLである場合には、URLの変換定義を用意することで、画像本体のみを保存し直接リンクさせられます。
URLの変換定義の方式には、「urlRewrite」と「urlRedirect」の2つがあります。
単なるURLの正規表現置換で画像本体のURLが得られ、HTMLにアクセスしていなくても画像に直接アクセス出来る場合は、以下のように「urlRewrite」で置換文字列を指定します。置換後のURLを保存させるための定義も別途必要であることに注意してください。

        image_html:{
            urlPattern:/^(http:\/\/japan\.cnet\.com\/)image\/l\/(.*\.(?:jpg|jpeg|png|gif|bmp))$/i,
            urlRewrite:"$1$2",
        },
        image:{
            urlPattern:/^http:\/\/japan\.cnet\.com\/(?:story_image|image|storage)\/.*\.(jpg|jpeg|png|gif|bmp)$/i,
            download:true,
        },

HTMLのURLから画像本体のURLが定まらない場合や、HTMLにアクセスしてCookieの取得などを行わないと画像本体へのアクセスが出来ない場合などは、以下のように「urlRedirect」に、HTML内から画像本体のURLが含まれる部分を抽出する定義を記述します。この定義で抜き出された範囲に最初に出現するhref=やsrc=のリンク先URLが、変換後のURLとなります。

        softnews_ss:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/magazine\/softnews\/\d+\/n\d+_pic\.html$/i,
            urlRedirect:/<td [^>]*>\s*<a href="[^">]*">(<img [^>]*>)<\/a>/i,
        },

また、フィードから抜き出されたURLに「?ref=rss」のようなゴミがくっついている場合なども、urlRewriteによるURLの置換を定義しておくと良いでしょう。


ContentExtractorのルール定義の主要部分の説明は以上です。
まだ説明されていない機能やテクニックもいくつかあるので、「doc\ContentExtractor.txt」の説明や、「plugins\ContentExtractor\」内の標準プラグイン内の実際の定義なども参照してみてください。

出来上がったプラグインを配布したい場合は、ZIPで圧縮してアップロードするなどしてください。そのうち、プラグインの配布や入手、アップデートなどを行う機能も実装される予定です。

HTTPオブジェクトのCookieを保存しておいて次回実行時に読み込む

HTTPオブジェクトでCookieを扱う際、受取ったCookieをファイルに保存しておいて、次にスクリプトを実行したときに読み込んで利用したい場合があるかも知れません。
このような場合は、HTTPオブジェクトのコンストラクタの引数に以下のようなオプションオブジェクトを指定してください。

try{
    var http=new (require('HTTP').HTTP)({
        cookies:Main.scriptDirectory.file('cookies.json'),
        saveCookies:true
    });
    //何らかの処理
}finally{
 free(http);
}

オプションオブジェクトの「cookies」メンバにファイルパスやFileオブジェクトを指定すると、そのファイルからCookieが読み込まれます。この時、ファイルが存在しなくてもエラーになったりはせず、単に何も読み込まずに初期化されます。
また、cookiesに加えて「saveCookies:true」を指定すれば、オブジェクトをfree()で解放する際に、cookiesで指定したファイルにCookieが保存されます。
ファイルから読み込まずに保存だけを行いたい場合は、cookiesを指定せずにsaveCookiesにファイルパスやFileオブジェクトを指定します。

このCookie保存機能では、ブラウザを閉じると破棄されてしまうセッションCookieも保存できます。これにより、ブラウザをずっと開いたままにしているように装うことも可能となります。
オプションオブジェクトに「discardSessionCookies:true」を指定することで、セッションCookieは保存せず破棄させることも可能です。

2011/02/16

HTTPオブジェクトで受信したCookieを使用したアクセスを行う

会員制のサイトなどのコンテンツにアクセスするには、IDとパスワードを入力してログインしなければならない場合があります。
このようなサイトでは、サーバが正しいログイン情報を受取った時にユーザーを識別するセッションIDを生成して「Set-Cookie」というレスポンスヘッダで送信し、それを受取ったブラウザなどのクライアントプログラムが次回以降のアクセス時に「Cookie」というリクエストヘッダでセッションIDを送信することで認証済みであることを証明するという方式がよく用いられています。

NILScriptのHTTPオブジェクトには、受取ったCookieを保持して次回アクセス時に送信するという機能が用意されており、このような認証が必要なサイトのコンテンツにも簡単にアクセス出来ます。
以下に示すのは、mixiにログインして指定コミュニティのトップページのテキストを抽出・表示するというスクリプト例です。
var user="user@example.com",pass="password";
var url="https://mixi.jp/view_community.pl?id=38328";
try{
    var http=new (require('HTTP').HTTP)();
    http.getText({
        url:"https://mixi.jp/login.pl",
        method:"post",
        body:{
            next_url:"/home.pl",
            email:user,
            password:pass,
            sticky:"",
        }
    });
    println(String((new (require('LooseXML').HTML)(http.getText(url)))
    .$0('#communityIntro')).replace(/]*>/ig,'\n').replace(/<.*?>/ig,''));
}finally{
    free(http);
}
HTTPクラスには「HTTP.getText()」のようなクラスメソッドが用意されていますが、Cookieを用いたアクセスを行うには、new HTTP()でインスタンスを生成する必要があります。
次に、このHTTPオブジェクトのgetText()メソッドでログイン情報を送信します。送信先URLとパラメータ名などは、ログインフォームのHTMLなどから探してください。

ログインに成功すれば、このHTTPオブジェクトにCookieが保存されます。
なお、リクエストオプションオブジェクトで「ignoreCookies:true」を指定すれば、受取ったCookieを無視させることも出来ます。
以降は、普通にこのHTTPオブジェクトのgetText()メソッドやsaveTo()メソッドを呼び出せば、アクセス先サーバから過去に受取って保存されたCookieが送信され、ログイン済みでないとアクセス出来ないコンテンツなどにもアクセス出来ます。

HTTPオブジェクトには次回アクセス時に再利用するためにKeep-Alive状態になっているTCPStreamオブジェクトなども保持されているので、使い終ったらfree(http)のようにして解放する必要があります。

なお、この機能はずっと実装するのを忘れたまま放置されていて、つい最近実装されました。使用するには、最新版にアップデートしてください。

2011/02/08

NotebookのScan機能でサイト上の全ページを保存

NILScriptに同梱されている「Notebook.ng」では、手動でページを保存したり自動巡回で新着ページを保存するだけでなく、リンクを辿ってサイト上のページを全て保存させることも可能です。
この機能を利用するには、リンクを辿る基点にしたいページでブックマークレットの「ScanForm」を実行するか、サイドバーの「Scan from Website」からURL未入力のフォームに進んでください。
すると、以下のような画面で使用するプラグインの選択などを行なえます。
巡回登録の場合と同様に、説明を参考に使用するプラグインを選択してください。

「Start」ボタンを押すと、以下のような画面で進捗状況が表示されます。サイトの規模によってはかなりの時間がかかるので注意してください。

Notebook.ngの巡回機能でWebページの更新チェック

NILScriptに同梱の「Notebook.ng」に用意された巡回機能では、RSSリーダーのように新着記事を収集するだけでなく、特定のWebページの内容の変化をチェックすることも可能です。
この機能を利用するには、巡回ルールを「blog/body」や「std/htmlBody」などにして巡回登録してください。
これらのルールでは、子アイテムの抽出は行われませんが、対象URL自体の内容が抽出され、変化があれば保存されます。更新が検出されたページは、Notebookのメイン画面の一覧の上位に表示されます。


ページに更新があると、Notebookのメイン画面の新着ページ一覧の上位に表示されます。この時、一覧項目の下部の「rev.」の部分の番号が更新され、リンクとしてクリックできるようになります。

リビジョンの部分のリンクをクリックすると、このような画面で前のリビジョンとの差分が表示されます。
色の薄い部分が変化の無かった部分、赤みがかった文字色で打ち消し線が引かれているのは削除された箇所、普通の文字の部分が新たに追加された部分です。(追加された文章を読みやすいように、新着部分が普通の配色になっています。)
更に、サイドバーの「This Page」にある「History」のリンクからは、過去のリビジョン一覧や各リビジョンの内容、任意のリビジョン間の差分なども表示できます。

この機能を利用すれば、RSSなどの配信が行われておらず、対応するContentExtractorルールも用意されていないページも、他のサイトと一緒に更新チェックを行なえるでしょう。

なお、この機能で利用されている差分検出の処理は「Diff」というユニットで定義されています。
自作のスクリプトで差分処理を利用したい場合は、「doc」ディレクトリ内の「Diff.txt」の説明を参照してください。

Notebook.ngの巡回機能でブログなどの新着記事を自動収集

NILScriptに同梱されている「Notebook.ng」には、ブログなどの新着記事を自動収集する巡回機能が用意されています。
プラグインで定義したルールに従ってページの本文などを抜き出す「ContentExtractor」機能により、必要な部分だけを保存して、快適に読み進めることが可能です。

Notebookに巡回対象サイトを登録するには、巡回したいサイト上で「AddCrawl」ブックマークレットを実行してください。
すると、以下のような新規巡回項目設定画面が表示されます。



登録対象ページにRSSやAtomのフィードが用意されている場合は、URL欄の下に「rss feed」などとしてリンクが表示されます。このリンクをクリックすれば、フィードURLを対象とした巡回登録画面に移行します。

その下には、インストールされているContentExtractorプラグインとルールの一覧が表示されます。対応URL認識機能によって対応しているとみなされたルールがあると、リストの上位に表示されます。ルール名の下の説明に従って、使用したいルールを選択してください。

ルール名の横の「Test」のリンクからは、そのルールを使用した場合の抽出結果のプレビュー画面に進めます。「blog/rss」などの汎用的なルールを使用する場合は、上手く抽出できるか確認してから登録すると良いでしょう。

Options以下の部分では、高度な巡回オプションを指定できます。一般的なRSS系のルールの場合は、そのままでも問題ありません

「Filters」では、子アイテムに対して除外や改変などのフィルタリングを行うルールを定義できます。この機能はまだ仮のものなので、準備が整い次第別途説明します。

「Interval」は巡回間隔です。たまにしか更新されないページでは大きめの値にすることで、他の巡回をスムーズに出来る場合があります。

「Content Update Check」では、子アイテムごとの更新チェックを行うかどうかと、更新チェック時にHTMLタグ部分を無視するかどうかを指定できます。追記などがほとんどないサイトでは「No」のままでよいでしょう。画像URLのみの更新なども検出したい場合は「Including HTML Tags」を、テキストの更新のみを検出したい場合は「Without Tags」を指定します。

「Children Order」では子アイテムの並び順を指定できます。「No」を指定すると、常に全ての子アイテムがチェックされますが、記事が更新日時順に並んでいることが分かっているサイトでは、適切なオプションを指定する事で、処理を高速化させられます。

「Depth」は、列挙された子ページを更に巡回登録するかどうかのオプションです。子ページの巡回オプションは、その下の「Options for Children」などで指定します。これらは、掲示板のスレッド一覧などを巡回するための機能です。説明部分に「"Depth"を"Crawl Children"にすることで――」のような記載があるルール以外では、設定してもあまり意味がないので「Crawl this URL only」のままにしておいてください。

フォームの一番下の「Save」ボタンを押せば、巡回設定が登録されます。登録された巡回項目の設定変更や削除などは、サイドバーの「All Crawls」で表示される一覧から行なえます。

サイドバーの「Crawl: Disabled」となっている部分の右の「Start」をクリックして「Crawl: Enabled」にしておくと、定期的に巡回が実行されます。収集されたページは、Notebookのメイン画面の最近更新されたページ一覧などで閲覧可能です。

Webページなどを保存して閲覧・検索できる「Notebook.ng」

NILScriptの最近のバージョンから同梱されるようになった「Notebook.ng」は、Webページなどを保存して閲覧・検索できるスクリプトです。
ページの追加はブックマークレットなどから手動で行う他、RSSなどを巡回登録して自動で新着記事を取り込むことも可能です。
NILScriptにはWebページからの本文抽出処理をプラグイン方式で定義できる「ContentExtractor」機構が用意されており、ブログのサイドバーなどの不要部分を除いた本文だけを保存して快適に閲覧できます。更に、検索時にサイドバーのカテゴリ一覧などにマッチして関係のないページがヒットしてしまうという問題も避けられます。

NILScriptは元々このスクリプトを実装することを第一目標として開発を始めたものですが、ようやく主要な機能を実装し仕様も固まってきたので、そろそろ使い方の説明をしたいと思います。

Notebook.ngを使用するには、Notebook.ngをng.exeかngw.exeで実行します。スタートアップなどへの登録は手動で行ってください。
最初に実行すると、データの保存先などを設定するページがブラウザで開かれます。ここでData Directoryに「Portable」を指定すれば、NILScriptのディレクトリをUSBメモリなどで持ち出して複数のPCで利用することも可能です。


初期設定を行うと、以下のようなメイン画面が表示されます。このURLをブラウザのブックマークなどに登録しておいてください。

次に、サイドバーの「Bookmarklets」のリンクからブックマークレット一覧のページに行き、使用したいブックマークレットをブラウザのブックマークに登録します。

基本的なページの保存を行うには、「Scrap」ブックマークレットを使用します。保存したいページを表示した状態で登録したブックマークレットを開くと、範囲選択を行っている場合は選択範囲の内容が、そうでない場合はサイドバーなどの不要部分を自動的に取り除いた部分が、Notebookに新規項目として保存されます。この際、ページに埋め込まれている画像も保存されるため、ページがサーバから削除されてしまっても、支障なく閲覧できます。


ページの保存が行われると、以下のように保存されたページが表示されます。
ここでは、左のサイドバーから「タグ」としてキーワードを関連付けたり、ページの重要度を表す「スコア」を変更したりできます。

サイドバーの検索ボックスに検索ワードを入力すれば、該当するページを検索できます。キーワードの前に「:」を付けると、タグのみを検索対象に出来ます。スペースや「:」を含む語句を検索したい場合は「""」で囲んでください。他のもいくつか検索オプション構文が用意されていますが、詳しくは同梱の「doc\Notebook.txt」を参照してください。

なお、Notebook.ngはまだテスト段階のため、不具合や不便な点が残っていると思われます。見つけた場合は、BBSやTwitterなどでお知らせください。

2011/01/25

IDLファイルからCOMオブジェクト定義を生成して使用する

WindowsがAPIとして用意している機能の中には、COMオブジェクトの形で提供されている物があります。
これらのCOMオブジェクトを利用するために、NILScriptにはCOMユニットが用意されています。
以前のバージョンでは、メソッドの定義を全て自力で記述しなければならないので面倒でしたが、最近の更新で定義を自動化するスクリプトが同梱されるようになったので、手軽に利用できるようになっています
ここでは、COMを利用した処理を行うスクリプトを記述する手順を説明します。

まず、あらかじめWindows SDKをMicrosoftのサイトから入手してインストールしておいてください。
そして、利用したいAPIがCOMオブジェクトとして提供されていることが分かったら、
「C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\」などのディレクトリ内のファイルを全文検索するなどして、どのIDLファイルで定義されているかを調べます。

次に、コマンドプロンプトで以下のようなコマンドラインを実行してください。
ng tool/idlConv "C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\shobjidl.idl" > shobjidl.ng
すると、IDLファイル内の定義文を元に、以下のようなクラス定義が列挙されたスクリプトが生成されます。
var PersistFile=Persist.define("PersistFile","{0000010B-0000-0000-C000-000000000046}",{
    IsDirty:[],
    Load:[
        WideString,        // [in] LPCOLESTR pszFileName
        UInt,              // [in] DWORD dwMode
    ],
    Save:[
        WideString,        // [in, unique] LPCOLESTR pszFileName
        Int,               // [in] BOOL fRemember
    ],
    SaveCompleted:[
        WideString,        // [in, unique] LPCOLESTR pszFileName
    ],
    GetCurFile:[
        Pointer,          // [out] LPOLESTR *ppszFileName
    ]
});
IDLファイルには多くの定義が含まれるので、必要な定義だけを探して自分のスクリプトにコピーしてください。
この時、「.define」の前の派生元クラスの部分が「Unknown」以外になっている場合は、その派生元クラスの定義も見つけてきて派生先クラスの定義より前の部分に記述しておく必要があります。
また、大元の派生元クラスとなる「Unknown」はCOMユニットで定義されているので、以下のような行を一番最初に記述してインポートしておきます。
var {COM,Unknown,BStr}=require('COM');
「COM」や「BStr」もCOMユニットで定義されているクラスで、COMを利用するスクリプトでたまに必要になることがあるので、常に記述するようにしておくといいでしょう。
なお、idlConvによる変換は常に正しく行われるとは限らないので、必ずMSDNのリファレンスなどと照らし合わせて、正しく記述されているか確認してから利用してください。

次に、COMオブジェクトを利用した処理の記述方法を説明します。
以下は、targetPathで指定したファイルを開くショートカットファイルをlnkPathで指定したファイルに保存するという処理の例です。
try{
 var link=ShellLinkW.create();
 var pf=link.toPersistFile();
 link.SetPath(targetPath);
 pf.Save(lnkPath,0);
}finally{
 free(pf,link);
}
「ShellLinkW.create()」は、C++などでの「CoCreateInstance(CLSID_ShellLink,NULL,CLSCTX_INPROC_SERVER,IID_IShellLink,(LPVOID*)&link);」に相当します。
クラスメソッド「create()」によるインスタンス生成を行うためには、idlConvで生成された定義にクラスプロパティ「classID」の定義が付加されている必要があります。もしclassIDの定義が正しく生成されていなかった場合は、各自でIDLファイルなどから「CLSID_~」の定義を探してきて追加しておいてください。

「link.toPersistFile()」は、「link->QueryInterface(IID_IPersistFile, (LPVOID*)&pf)」に相当します。
このメソッドは、「PersistFile」クラスの定義を実行したときに自動的に追加されます。

link.SetPath()とpf.Save()は、COMオブジェクトのメソッド呼び出しです。通常のNILScriptやJavaScriptのメソッドと違い、最初の文字も大文字になっているので注意してください。

最後に、finallyブロック内で「free(pf,link)」を実行して、生成したオブジェクトを解放する必要があります。
これは「if(pf) pf->Release();if(link) link->Release()」に相当します。

このように、最初の定義が多少面倒ですが、処理本体は比較的簡潔に記述できるようになっています。
COMオブジェクトとして提供されているAPIには様々なものがあるので、COMオブジェクトの使用方法を覚えておけば、役に立つことがあるでしょう。