YAPC::Asia 2014 に参加してきた

ブログを書くまでが YAPC らしいのでブログを書こうと思う(ブログを書かなければいつまでも YAPC ということか)

個人的には最近 Golang に興味があるので Go for perl mongers がすごく勉強になったし、ずっと Web アプリケーションばかり書いていたので ウェッブエンジニアのローレベルプログラミング このセッションもすごく面白かった。 こうして振り返ってみるとほとんど Perl の話は聞いていない気がする。そもそも Perl の話そんんなに多くなかったというと身も蓋もないかもしれない。YA'P'C とは?という感じでもあるけど、 Perl がどうこうということではなく、Perl に親しいエンジニアにはベテランエンジニアが多いのでそういう人たちの話を聞ける貴重なイベントという感じだった。

あと、自分より年下のエンジニアがスピーカーになっているのを見るといろんな意味でいい刺激になる。普段それなりに仕事しているつもりだけどアウトプットはあんまり出来ていなくて、なるべくアウトプットしていこうと思った。来年スピーカーとして話せるよう、がんばろうと思う。

ビール投げ売りしてた

1ケース3000円

f:id:hatz48:20140614145217j:plain

やまや に行ったらビール投げ売りしてたので思わずケースで買ってしまった。やまやのサイトを見ると1ケース4800円と書いてあるが、過剰入荷してしまったようで京都のいくつかの店舗では賞味期限が近いものに限り1缶125〜130円という発泡酒以下の値段で購入できる。

ピルスナー2種と白ビール(ヴァイス)のものがあって投げ売りする前から200円で売ってた。日本で白ビールを買おうと思うと少し値段が張るので、白ビール飲みたいと思ったらこれ買って飲んでた。 最近よく見かける白ビールだとヒューガルデン・ホワイトとか水曜日のネコとかがあるけど、Amazonだとどちらも1ケース6〜7000円強する

水曜日のネコ 350ml 24缶 1ケース

水曜日のネコ 350ml 24缶 1ケース

エッティンガーのヴァイスは今だと1ケースで3000円なのでめちゃくちゃお買い得。 味は白ビールにしては割とあっさり目、後味すっきりという感じ。ドイツで一番売れているらしいけど、やっぱりさらりと飲めるのが人気なのだろうか。コク派の人には物足りないかもしれないけどその分ご飯と一緒に飲んでもいける。

賞味期限は7/1くらいまでらしい?(箱には EXP: 30.10.2014って書いてある)けど、君たちそのくらい飲むだろうしやまやを助けると思って買いに行くべき。

画像をまとめて zip にしてダウンロードする拡張を作った

前回 xhr で画像をダウンロードして zip するコードを書いた。せっかくなので拡張にしてみた。

使い方

上の拡張をインストールすると、URLバーの横に

f:id:hatz48:20140204010831p:plain

こういうボタンがでるので、画像をダウンロードしたいページでそれを押す。 押すと、マウスオーバーした html 要素がふわっとハイライトされるようになる。

f:id:hatz48:20140204135358p:plain

これだと選択した要素内に画像がないので空の zip になってしまう

f:id:hatz48:20140204135356p:plain

こんな感じに選択されればオッケー。 クリックすると、その要素内の画像が zip してダウンロードされる。

実装

chrome 拡張内で実行されるコードはページをブロックしないので、重い処理は拡張内のコードで実行してあげればよい。

  1. ページ内からダウンロードしたい画像のURLを抽出する
  2. 拡張のコードで、画像を取得・zip (ここは前回と同じ)
  3. zip をローカルにダウンロードする

けど拡張からページの document オブジェクトを直接操作することはできない(たぶん)

要素をハイライトしたり、クリックした要素内から画像 URL を抽出するといったコードは、そのページのコンテキストで実行する必要があるので chrome.tabs APIexecuteScript メソッドを利用する。 executeScript はスクリプトの最後で評価した値を callback に渡してくれるが、ここで渡せる値は jsonable な値のみなので、promise オブジェクトとかを渡すことはできない。クリックで選んだ要素内の画像を・・・ということをしたい場合、スクリプト終了時にはまだ画像URLをとることはできないので、ページのスクリプトと拡張のコードで通信する必要がある

これを使う。クリックで要素を選択したら、その要素内の画像URLを抽出して sendMessage するようにした。

通知された URL を元に画像を取得して zip した後は、chrome.downloads API を用いてユーザーのダウンロードフォルダにダウンロードさせる。 downloads APIchrome 31 くらいから入ったようなので、それ以前の chrome では使えない。


やっつけ感があるので、zip ファイルの名前がよくわからない 16 進文字列だったり、zip 処理中のフィードバックがなかったりとアラはいろいろとあるけどそれなりに便利と思う。

適当にご利用ください。

Web Worker を使って web ページ内の画像を zip してダウンロードする

クロスドメイン制約がない状況で、Web ページ内に表示されている画像を一括で zip してダウンロードしたいみたいな欲求ありませんか。私にはありました。

ありがたいことに jsZip というライブラリがあり、これを使えば JavaScript で zip ファイルを作成することができます。手順としては以下のような感じでしょうか

  1. ダウンロードしたい画像の url 一覧を作る
  2. 画像をすべてダウンロード
  3. 完了したら画像データを jsZip で zip する
  4. zip したものを blob にして、ダウンロード用のリンクを作る

ご存知のようにブラウザの JavaScript はシングルスレッドで動作しており、JavaScript で時間のかかる処理を行うとユーザーの操作がブロックされます。jsZip による zip 処理もファイル数が少ないうちはいいのですが、ファイル数が増えてくると結構時間がかかってしまうようです。この際なので画像をダウンロードする処理もあわせて WebWorker にやってもらうことにしましょう。以下では 2, 3 の手順について調べた/書いたことを紹介します。

Web Workers

ウェブワーカーの基本 にウェブワーカーの基本が書いてあります。大まかに言うと、ある JavaScript ファイルを指定して new Worker("worker.js") してあげるとそのスクリプトが別スレッドで実行される、メインスレッドとのやり取りは postMessage メソッドと message イベントを使う、別スレッドの処理が終わったら worker.terminate() を呼んでワーカーを終了させるといった感じです。

画像をダウンロードする

ここで注意したいのは、ワーカースレッドからは window オブジェクトやDOMなどにアクセスすることが出来ないということです。

ワーカーで次の機能にはアクセスできません:

  • DOM(非スレッドセーフ)
  • window オブジェクト
  • document オブジェクト
  • parent オブジェクト

コンソールでちょこちょこと確認しただけですが、どうやら Image クラスもないようです。以下のお手軽画像ダウンロードが禁じられてしまいました。

var img = new Image();
img.src = url;
img.onload = ...

途方に暮れていたのですが XMLHttpRequest は使えるということで、xhr で画像も取得できるだろうと調べてみました。Google 先生にしつこく聞いたところ私にぴったりの記事を教えてくれました。

xhr.responseType に希望の型を指定すればよいようです。

var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "arraybuffer";

xhr.onload = function (event) {
    var arrayBuffer = xhr.response;
        ...
}

これで画像を arrayBuffer や blob で取得することができました。めでたいですね。

ダウンロード完了をまって zip する

ウェブワーカー上で動かす XMLHttpRequest もやはり非同期処理を行うことができます。ワーカー自体が非同期なので、さらにその中で非同期にするのか、という気持ちもありますが画像をたくさんダウンロードするにはやはり非同期にしたほうがよいでしょう。問題はすべての画像がダウンロードされるのを待って処理を行わなければならないということです。

普段から jQuery に甘えっぱなしなのでここも $.when(deferreds) といきたいところです。ワーカー内で他のスクリプトを読み込むには importScripts("script1.js", "script2.js") などのようにします。script1.js の中でグローバルにセットされた値をワーカー内で使うことが出来るようになります。いざ jQuery を読み込むこととしましょう

importScripts("jquery.js")
// -> Uncaught ReferenceError: window is not defined

なんと jQuery は読み込み時点で window オブジェクトなどにアクセスするので、worker からは使えないようです。残念ですが今回は deferred できればよいので、そもそも jQuery は大げさすぎるかもしれません。Promise を使ってみます。今度は global is not defined と言われてしまいましたがこれはグローバルにPromiseをセットしようとしているだけだったので以下のコードで回避することができました。

var global = self;
importScripts("/public/js/promise-3.2.0.js");

メインスレッドと通信する

メインスレッドからは生成したワーカーオブジェクトの postMessage メソッドを使います。ワーカースレッドには、グローバルに postMessage があるのでそれを使います。

だいたいこんな感じになりました。

  • main.js
var worker = new Worker("path/to/worker.js");
worker.postMessage({urls:urls});
worker.addEventListener('message', function(event) {
    var command = event.data.command;
    if (command === 'download') {
        var filename = event.data.filename;
        console.log((++i) + ':' + filename);
    }
    if (command === 'complete') {
        var blob = event.data.blob;
        var $a = $('a.download');
        $a.attr('href', window.URL.createObjectURL(blob));
        $a.attr('download', "hoge.zip");

        worker.terminate();
    }
});
  • worker.js
var global = self;
importScripts("/path/to/jszip.js");
importScripts("/path/to/promise-3.2.0.js");

var zip = new JSZip();

self.addEventListener('message', function (event) {
    var urls = event.data.urls;
    var promises = urls.map(function (url) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "arraybuffer";

            xhr.onload = function (event) {
                var arrayBuffer = xhr.response;
                var filename = url.replace(/^(.*)\//, '');
                zip.file(filename, arrayBuffer, { binary: true });
                postMessage({
                    command: 'download',
                    filename: filename
                });
                resolve(true);
            };
            xhr.onerror = function (event) {
                resolve(false);
            };
            xhr.send();
        });
    });
    Promise.all(promises).then(function () {
        postMessage({
            command: 'complete',
            blob: zip.generate({ type: "blob" })
        });
    });
});

メインスレッドから落としてほしい url の配列を渡します。ワーカーは self.addEventListener('message', ...) でメッセージを受け取って処理を行います。メインスレッドに進捗を伝えるために画像のダウンロードが終わるたびに通知するようにしてみました。メインスレッドからはすべて message イベントとして通知されるので、進捗報告なのか完了報告なのかわかるように command プロパティを含ませてみました。 もともとバックグラウンドで動く拡張とか、クロスドメインできない普通のwebサイトではほとんど役に立たない話でした。Chromeアプリなんかだと使えるんじゃないかと思います。

どうしてもメモリを食うようです。あんまり大量に zip しようとするとページが落ちるかもしれません。