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へのリダイレクトを示します。このようにして進捗などの出力を読み捨てないと、出力バッファが一杯になって処理が止まってしまう可能性があるので、注意が必要です。