PILにかえてPillowをいれた

昨日 PIL をインストールしたけど、「python pil 使い方」でぐぐったらこんなページが最初に来た。

 cf. PIL 利用ノート [obsolete]

曰く、

Warning

PIL は随分長い間、Python のバージョンで言うと 2.7 くらいまでだろうか、 画像処理パッケージとして愛用してきたが、現在は Pillow というものに取って代わられたようである。 そこで、筆者の環境も PIL を放棄して Pillow を導入した。

だそうである。なんてこった…orz。

使ってる Python が 2.7.6 なのでこのままでもいいのかもしれないけど、せっかくなので Pillow に移行することにした。
Pillow のインストールは↓ここに詳しい。

 cf. Installation – Pillow (PIL Fork)

このページの Linux Installation の項に Ubuntu 14.04 LTS でのインストールの仕方も載っている。まずは依存ライブラリのインストール。

takatoh@nightschool $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev  libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk

つづいて Pillow を pip でインストール。

takatoh@nightschool $ sudo pip install pillow

試してみよう。

takatoh@nightschool $ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from PIL import Image
>>> img = Image.open("sample.jpg")
>>> img.info
{'jfif_version': (1, 1), 'jfif': 257, 'jfif_unit': 1, 'jfif_density': (72, 72), 'dpi': (72, 72)}
>>> img.size
(1280, 720)

フォーマットを変換するには、単に拡張子をかえて保存すればいい。

>>> img.save("sample.png")

縦横比を保ってサムネールを作るには thumbnail メソッドが使える。ただし、オブジェクト自身を変更するので注意。と言ってもいきなり上書きされるわけでもない。保存するには上にも書いた save メソッドを使う。

>>> img.thumbnail((360, 360))
>>> img.size
(360, 202)
>>> img.save("thumbnail.jpg")

とりあえずはこんなところか。うまくいってよかった。

Python Imaging Library (PIL)を入れてみた

pip で簡単にインストールできると書いてあるページもあるけど、少なくとも今回はうまくいかなかったので、↓このページの通りにインストールした。

 cf. Python Imaging Library (PIL)をインストール – rakkyooの備忘録

Python Imaging Library のサイト(http://www.pythonware.com/products/pil/)から、ソースをダウンロードして、展開。今回ダウンロードしたソースは、Python Imaging Library 1.1.7 Source Kit (all platforms) (November 15, 2009)。
展開したディレクトリに移動して setup.py を実行。

takatoh@nightschool $ sudo python setup.py install

ところが、python.h がない、っていうエラーが出た。ググってみたら、python-dev をインストールしておけばいいらしい。

 cf. [python]PILのインストールで「_imaging.c:75: fatal error: Python.h」- うえちょこ@ぼろぐ

takatoh@nightschool $ sudo apt-get install python-dev

再度 setup.py を実行。今度はうまくいったか。

試してみる。

takatoh@nightschool $ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import Image
>>> im = Image.open("banner.gif")
>>> im.info
{'duration': 0, 'version': 'GIF89a', 'transparency': 57, 'background': 56}
>>> im.format
'GIF'
>>> im.size
(200, 40)
>>> 

OKっぽい。

Node.js:コマンドラインオプションを処理する

js-opts というモジュールが使える。
npm でインストール。

^o^ > npm install opts
[email protected] node_modules\opts

js-opts というモジュール名だけど、インストールするのは opts。

昨日のファイル一覧を表示するスクリプトを拡張してみた。

var fs = require('fs');
var opts = require('opts');

var ext = false;

var options = [
  { short: "v"
  , long: "version"
  , description: "Show version and exit"
  , callback: function() { console.log("v0.1.0"); process.exit(1); }
  },
  { short: "e"
  , long: "ext"
  , description: "Specify ext"
  , value: true
  , callback: function(value) { host = value; }
  }
]

opts.parse(options, true);

var ext = opts.get("ext");
var dir = opts.args()[0];

fs.readdir(dir, function(err, files) {
  if (err) throw err;
  files = files.filter(function(file) {
    return fs.statSync(file).isFile();
  });
  if (ext) {
    var re = new RegExp(".+\." + ext + "$");
    files = files.filter(function(file) {
      return re.test(file);
    });
  }
  files.forEach(function(file) {
    console.log(file);
  });
});

オプションをオブジェクトの配列として定義しておいて、opts.parse の引数に渡してやると、オプション解析をしてくれる。true はなんだろう?

オプションの引数は opts.get(オプション名) で取得できる。それから、解析の結果残った引数は opts.args() で配列として取得できる。

実行結果:

^o^ > node listfiles2.js --version
v0.1.0

^o^ > node listfiles2.js --help
Usage: node C:\Users\takatoh\Documents\w\nodejs\listfiles2.js [options]
Show this help message
    --help
Specify ext
    -e, --ext 
Show version and exit
    -v, --version


^o^ > node listfiles2.js .
config.json
filesize.js
get_file.js
get_file2.js
hello.js
hello.txt
httphead.js
listfiles.js
listfiles2.js
read_config.js
server.js
test_argv.js
test_url.js

^o^ > node listfiles2.js --ext js .
filesize.js
get_file.js
get_file2.js
hello.js
httphead.js
listfiles.js
listfiles2.js
read_config.js
server.js
test_argv.js
test_url.js

–help オプションは定義しなくても、自動で作ってくれるみたい。

js-opts のサイト:

 cf. https://bitbucket.org/mazzarelli/js-opts/wiki/Home

[追記]

js-opts のソースを確認したところ、opts.parse の第2引数に true を指定すると、–help オプションを追加してくれるみたい。

Node.js:ファイルの一覧を取得する

指定したディレクトリ中のファイル一覧を取得する。
fs モジュールの readdir を使う。

var fs = require('fs');

var dir = process.argv[2];
fs.readdir(dir, function(err, files) {
  if (err) throw err;
  files.forEach(function(file) {
    console.log(file);
  });
});
^o^ > node listfiles.js .
config.json
filesize.js
get_file.js
get_file2.js
hello.js
hello.txt
httphead.js
listfiles.js
read_config.js
server.js
test_argv.js
test_url.js

Sinatraアプリのログをローテーションする(2)

logrotate の設定をして1日待ってみたけど、ログファイルがローテーションされていない。仕方がないので、-f オプションを使って強制的に行ってみたら、次のようなメッセージが出た。

takatoh@nightschool $ sudo logrotate -f -v sombrero
reading config file sombrero

Handling 1 logs

rotating pattern: /home/sombrero/sombrero/unicorn.log  forced from command line (60 rotations)
empty log files are rotated, old logs are removed
considering log /home/sombrero/sombrero/unicorn.log
error: skipping "/home/sombrero/sombrero/unicorn.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.

なんだかよくわからないけど、親ディレクトリのパーミッションがセキュアじゃないといってる?設定ファイルに su ディレクティブをセットしろともいってる。

ちょっと調べてみると、次のページが見つかった。

 cf. logrotate(8) の create と su ディレクティブについて – はてダ

読んでもよくわからないんだけど、とにかく次の1行を設定ファイルに追加した。

su root root

これでどうだ。

takatoh@nightschool $ sudo logrotate -f -v sombrero
reading config file sombrero

Handling 1 logs

rotating pattern: /home/sombrero/sombrero/unicorn.log  forced from command line (60 rotations)
empty log files are rotated, old logs are removed
considering log /home/sombrero/sombrero/unicorn.log
  log needs rotating
rotating log /home/sombrero/sombrero/unicorn.log, log->rotateCount is 60
dateext suffix '-20150409'
glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
glob finding logs to compress failed
glob finding old rotated logs failed
renaming /home/sombrero/sombrero/unicorn.log to /home/sombrero/sombrero/unicorn.log-20150409
running postrotate script

今度はうまくいった?

takatoh@nightschool $ ls /home/sombrero/sombrero
CHANGES       bin                 lib        unicorn.conf          views
Gemfile       boot.rb             public     unicorn.log
Gemfile.lock  config.yaml         rakefile   unicorn.log-20150409
app.rb        config.yaml.sample  start.cmd  unicorn.pid
app.ru        db                  tmp        version.rb

おお、ちゃんとローテートされたファイル(unicorn.log-20150409)ができて、新しいログファイル(unicorn.log)もできている。
これでOKかな。

Sinatraアプリのログをローテーションする

去年の7月から Sinatra + Unicorn で動かしている Web アプリがあるんだけど、そのログファイルが巨大になっていた。↓の unicorn.log がそれ。75MBにもなっている。
そこで、logrotate を使ってみることにした。

takatoh@nightschool $ ls -lh
合計 75M
-rw-rw-r-- 1 sombrero sombrero  752  7月 26  2014 CHANGES
-rw-rw-r-- 1 sombrero sombrero  122  7月 26  2014 Gemfile
-rw-rw-r-- 1 sombrero sombrero  468  7月 26  2014 Gemfile.lock
-rw-rw-r-- 1 sombrero sombrero 7.5K  1月  4 10:39 app.rb
-rw-rw-r-- 1 sombrero sombrero   64  7月 26  2014 app.ru
drwxrwxr-x 2 sombrero sombrero 4.0K  7月 26  2014 bin
-rw-rw-r-- 1 sombrero sombrero  327  7月 26  2014 boot.rb
-rw-rw-r-- 1 sombrero sombrero  126  7月 26  2014 config.yaml
-rw-rw-r-- 1 sombrero sombrero  104  7月 26  2014 config.yaml.sample
drwxrwxr-x 3 sombrero sombrero 4.0K  4月  8 04:59 db
drwxrwxr-x 3 sombrero sombrero 4.0K  7月 26  2014 lib
drwxrwxr-x 3 sombrero sombrero 4.0K  7月 26  2014 public
-rw-rw-r-- 1 sombrero sombrero  705  7月 26  2014 rakefile
-rw-rw-r-- 1 sombrero sombrero   14  7月 26  2014 start.cmd
drwxrwxr-x 2 sombrero sombrero 4.0K  4月  8 05:00 tmp
-rw-rw-rw- 1 sombrero sombrero  109  7月 26  2014 unicorn.conf
-rw-rw-r-- 1 sombrero sombrero  75M  4月  8 16:50 unicorn.log
-rw-r--r-- 1 root     root        5  3月 24 20:43 unicorn.pid
-rw-rw-r-- 1 sombrero sombrero   32  7月 26  2014 version.rb
drwxrwxr-x 2 sombrero sombrero 4.0K  7月 26  2014 views

参考になったのは次のページ。

 cf. Rails + Unicorn の logrotateAdd Star – yoshioblog
 cf. ログローテーション(logrotate)を使ってみる ( httpd(apache)の設定例 )

特に上のページを参考にして設定ファイルを書いてみた。設定ファイルは /etc/logrotate.d ディレクトリの下に、サービス名でファイルを作ればいいようだ。
書いた設定がこれ。

/home/sombrero/sombrero/unicorn.log {
    daily
    missingok
    rotate 60
    dateext
    compress
    delaycompress

    postrotate
    pid=/home/sombrero/sombrero/unicorn.pid
    test -s $pid && kill -USR1 "$(cat $pid)"
    endscript
}

設定の意味は次の通り。

  • daily 毎日ローテーションする
  • missingok ログファイルがなくてもエラーを出さない
  • rotate 60 60回ローテーションする
  • dateext ローテーションされたログファイルのサフィックスを日付形式にする
  • compress ローテーションされたログファイルを圧縮する
  • delaycompress 圧縮するのを次のローテーションの時にする

postrorate と endscript の間の記述は、ローテーションが終わったあとに実行される。
Unicorn は USR1 シグナルを受け取ると、ログファイルを開きなおしてくれるそうなので、そうしている。

logrotate -d で確認する。

takatoh@nightschool $ logrotate -d sombrero
reading config file sombrero

Handling 1 logs

rotating pattern: /home/sombrero/sombrero/unicorn.log  after 1 days (60 rotations)
empty log files are rotated, old logs are removed
considering log /home/sombrero/sombrero/unicorn.log
  log does not need rotating

エラーらしきものは出てないから大丈夫のようだ。
あとは1日待って確認しよう。

Node.js:async.forEachSeries

昨日のプログラム、うまくいったので書く。

結果から書くと、アクションの中で callback を呼び出させばいいようだ。プログラムはこんなふうになった。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var async = require('async');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      async.forEachSeries(items, function(item, callback) {
        action[item.action](item, callback);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item, callback) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
          console.log("New: " + item.path + "...done.");
          callback(null);
        });
      });
    }
  });
}

function deleteItem(item, callback) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
  callback(null);
}

replicate(host);

アクションの最後、特に getNewItem ではネストの一番深いところで callback を呼び出している。これが次のアクションを呼び出すことになっているわけだ。
実行結果。

^o^ > node replicate.js
New item: Penguins.jpg
New: Penguins.jpg...done.
New item: data/Desert.jpg
New: data/Desert.jpg...done.
Delete item: Penguins.jpg
Delete: Penguins.jpg...done.

ちゃんと指示通りの順番で実行されている。

^o^ > tree /F storage
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は FE2A-F7C6 です
C:\USERS\TAKATOH\DOCUMENTS\W\REPLICATE\STORAGE
└─data
        Desert.jpg

Penguins.jpg は削除されて残っていないのがわかる。

Node.js:async.forEachSeriesって配列の順番どおりに処理してくれるんじゃないの?

本当はうまくいってから書くつもりだったけど、どうにもうまくいかないので書くだけ書いておく。

簡単な説明

サーバからの指示でファイルをダウンロードしたり削除したりするプログラムを Node.js で書いている。けど、指示通りの順番で処理してくれないので困ってしまった。async.forEachSeries を試してみたけどダメだった(←今ここ)。

より細かい説明

サーバは http://localhost:8888/ で動いていて、/recent.json にアクセスすると指示を受け取ることができる。指示は JSON 形式で、次のようなもの。

[
  { "action": "new", "path": "Penguins.jpg" },
  { "action": "new", "path": "data/Desert.jpg"},
  { "action": "delete", "path": "Penguins.jpg"}
]

action がするべき処理を示していて、new は新しくファイルをダウンロード、delete はすでにダウンロードしたファイルを削除する。上の例では、

  1. Penguins.jpg をダウンロードして保存
  2. data/Desert.jpg をダウンロードして保存
  3. Penguins.jpg を削除

となって、結果として data/Desert.jpg だけが保存されるはず。

書いてみたプログラム replicate.js を載せるよ。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      items.forEach(function(item) {
        action[item.action](item);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
        });
      });
      console.log("New: " + item.path + "...done.");
    }
  });
}

function deleteItem(item) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
}

replicate(host);

たいして難しいことはしてない。変数 action にアクションを登録(9~12行目)しておいた上で、サーバから JSON で指示を受け取り(15行目)、パースして配列に直し(22行目)、配列の順番どおりに処理している(23~25行目)。ついでに各処理のはじめと終わりにコンソールに出力している。

さて、これを実行してみると、期待通りには動いてくれない。

^o^ > node replicate.js
New item: Penguins.jpg
New item: data/Desert.jpg
Delete item: Penguins.jpg
Delete: Penguins.jpg...done.
New: Penguins.jpg...done.
New: data/Desert.jpg...done.

処理の開始は配列の順番どおり(最初の3行)だけど、Delete が先に終わってしまって、New があとになっている。結果として、削除するはずの Penguins.jpg が残っている。

^o^ > tree /F storage
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は FE2A-F7C6 です
C:\USERS\TAKATOH\DOCUMENTS\W\REPLICATE\STORAGE
│  Penguins.jpg
│
└─data
        Desert.jpg

原因はアクションの処理が非同期なせいだ。Array.forEach 自体は非同期じゃないらしいけど(だから指示の順番どおりにアクションが始まっている)、アクション自体が非同期なので、ひとつのアクションが終わらないうちに次のアクションが始まり、終わりの順番が入れ替わってしまっているんだ。
これは困った。ちゃんと指示通りに順番に処理してくれないと、上のように Penguins.jpg のダウンロードと削除が指示通りにならない。さて、どうしよう。

async.forEachSeriesを試す

いろいろググってみた結果、async というライブラリでフロー制御ができるらしい、というのがわかった。というわけで、async.forEachSeries を試してみた。
書き換えたのがこのコード。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var async = require('async');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      async.forEachSeries(items, function(item) {
        action[item.action](item);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
        });
      });
      console.log("New: " + item.path + "...done.");
    }
  });
}

function deleteItem(item) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
}

replicate(host);

書き換えたのは2行だけ。5行目で async を読み込み、24行目では items.forEach(function… の代わりに、async.forEachSeries(items, function… としている。これで、うまく動いてくれるだろうか。

^o^ > node replicate.js
New item: Penguins.jpg
New: Penguins.jpg...done.

ダメだーーー!
なんだかわからないけど、最初のアクションしか実行してくれない。ぜんぜん each じゃないじゃないか。どういうことだろう?

Node.jsでMD5を計算する

組み込みの crypto モジュールを使う。

var crypto = require('crypto');
var fs = require('fs');

function md5hash(file) {
  var content = fs.readFileSync(file);
  var md5 = crypto.createHash('md5');
  md5.update(content);
  return md5.digest('hex');
}

var filename = process.argv[2];
var md5 = md5hash(filename);
console.log(md5 + ' ' + filename);
takatoh@nightschool $ nodejs md5.js sample.zip
ae5a874a75de5b5597091d04b55b0b42  sample.zip