まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2017/05

先日、HTML5 Canvas のサーバーサイド版である Node-Canvas を紹介しました:
Node.js から使えるサーバーサイド Canvas : node-canvas


HTML5 Canvas と互換性のある API を使って動的に Canvas 上に描画をします。実際には単に描画をするだけでなく、描画した内容を画像として取り出したりすることで、サーバー側の JavaScript で動的に画像ファイルを生成することができるようになり、とても便利なライブラリです。

実際に使っていて、1点問題気になることがありました。開発中は(日本語環境バッチリな)自分のマシンを使って動作テストなどを行うので気が付きにくいのですが、実際の本番サーバー上で動かした際にキャンバス上に描画した日本語文字列が化けてしまうケースを発見しました。

例えばこのようなコードを実行して、サーバーサイドで画像を動的に生成したとします:
  :
var fs = require( 'fs' );
var Canvas = require( 'canvas' ),
    Image = Canvas.Image;

  :

var canvas = new Canvas( 300, 300 );
var ctx = canvas.getContext( '2d' );

//. 赤い直線を1本描画する
ctx.beginPath();
ctx.moveTo( 100, 100 );
ctx.lineTo( 200, 200 );
ctx.strokeStyle = 'red';
ctx.stroke();

//. 文字列を表示する
ctx.fillText( 'ハローワールド!', 10, 30 );

//. 画像データをファイルにして保存する
var b64 = canvas.toDataURL().split( ',' )[1];
var buf = new Buffer( b64, 'base64' );
fs.writeFileSync( __dirname + '/images/aaa.png', buf );

  :


このコードを Node.js で実行すると、./images/aaa.png というファイル名で動的に画像ファイルが生成されて保存されます。が、その時に生成される画像で「ハローワールド!」と明示されている文字列はサーバー実行環境によって正しく表示されたり、されなかったりします:

(正しく表示される場合)
2017052901

(文字化けを起こす場合)
2017052902


これはサーバー OS 側に日本語フォントが導入されているかどうかで決まります。特にクラウド環境においては OS は(デフォルトでは)英語版が用意されることが多く、そのまま利用しても日本語フォントが用意されていないため、「ハローワールド」という文字列を正しく描画できず、結果として文字化けを起こしてしまうことになります。

ということは、仮に文字化けを起こしても、OS に日本語フォントを導入した上でアプリケーションサーバーを再起動すれば回避できる、ということになります。

が、ここで問題が発生します。IBM Bluemix のような PaaS 環境の場合、OS 環境設定の自由度が低く、用意されたプラットフォームをカスタマイズして使うハードルが高くなります。現に SSH でログインすることはできますが apt-get などは使えません。またフォントファイルを後から無理やりコピーしてもアプリケーションサーバーを再起動するとそのファイルは消えてしまいます。このような自由度があまり高くないプラットフォーム上で日本語を正しく表示させることが結構難しかったりします。

そしてなんと更に、Node-Canvas の Issue ボードによると、この部分にはどうもバグがあるらしく、(2017/May/29 現在)ステータスが Open になっているので、まだ解決できていない模様でした:
https://github.com/Automattic/node-canvas/issues/783


・・・と、とても困った状況なのですが、結論から言うとなんとかアプリケーション側の工夫で回避することができました。まずは日本語フォントが必要になるので、フリーの日本語フォントを適当にダウンロードしてきます。例えば eFont プロジェクトから配布されているさざなみフォントの最新版を使うことにします。アーカイブファイル(*.tar.bz2)をダウンロード&展開し、sazanami-***.ttf というファイルをアプリケーション内の fonts/ ディレクトリに格納しておきます:
2017052903

(以下のコードではゴシックフォントしか使いませんが、とりあえず明朝フォントと合わせて展開しておきます)


そしてコードの該当部分を以下のように変更します:
  :
var fs = require( 'fs' );
var Canvas = require( 'canvas' ),
    Image = Canvas.Image;

  :

var canvas = new Canvas( 300, 300 );
var ctx = canvas.getContext( '2d' );

//. 赤い直線を1本描画する
ctx.beginPath();
ctx.moveTo( 100, 100 );
ctx.lineTo( 200, 200 );
ctx.strokeStyle = 'red';
ctx.stroke();

//. フォントを強制指定する
var Font = Canvas.Font;
var myFont = new Font( 'myFont', 'fonts/sazanami-gothic.ttf' );
ctx.addFont( myFont );
ctx.font = '20px myFont';

//. 文字列を表示する
ctx.fillText( 'ハローワールド!', 10, 30 );

//. 画像データをファイルにして保存する
var b64 = canvas.toDataURL().split( ',' )[1];
var buf = new Buffer( b64, 'base64' );
fs.writeFileSync( __dirname + '/images/aaa.png', buf );

  :


これで実行すると、フォントが指定のものに強制指定され、日本語が化けずに表示できるようになります(下図は IBM Bluemix 環境下で実行した結果です):
2017052904


なお、上記の方法を使う場合、Ubuntu はこのままでも動くのですが、Mac OS X 環境においては Pango ライブラリを無効にした上で実行する必要があります。具体的には Node-Canvas を npm install した後で、node_modules/canvas/binding.gyp ファイルを編集し、15行目の値を 'false' にする(8行目と同じにする)作業を行い、再度 npm rebuild した上で node コマンドを実行する必要がありました。ご注意ください。


位置情報付きのマンホール画像情報ポータルサイトである、拙作「マンホールマップ」を作って運用しています(詳しくはこちら):
2017052401


このサービスに限った話をすると、仕様を決める&変えるのも、作るのも、管理運用するのも、基本的には全部僕が1人でやっています。なので現状 DevOps 的な観点では非常に楽ちんです。

ただ一般的(?)にはこれらは分業制が取られることが多いです。そこで DevOps 的なアプローチやツールが活用されたりするわけですが、特に「業務で地図アプリを作る」場合に意識しておくべきことが大きく3つあります:
(1) どこの地図 API を採用するのか?
(2) (1) の API が有償の場合、誰が支払いをするのか?
(3) 誰がキーを登録するのか?



まず (1) です。一般的、というか理想的にはまず最初に仕様を決めて、その仕様にあったものを作っていきます。地図アプリの場合、「その地図を使ったどういうアプリを作るのか」というのが仕様にあたるわけですが、「それが実現できるのは、どこの地図か?」ということを意識して決める必要があります。ウェブで使える地図にも Google MAP だったり、Yahoo! Developers の YOLP だったり、オープンマップだったり、・・・と色々あって、まず地図の見た目からして異なります。そして持っている機能や拡張機能も異なります。アプリケーションサービスとして作る場合は、そこから提供されている API でどこまでできるのか/できないのかも異なってきます(更にいうとウェブ上の情報量も異なります)。というわけで、やりたいことを実現できる地図はどこの地図か? を見極めるのがかなりの労力を要する作業になります。 機能や情報量の面で比較的メジャーなのは Google MAPs だと思っているので、とりあえず Google MAPs にしておけば柔軟な対応も可能にはなりますが、その場合は (2) の問題を意識する必要がでてきます。

次に (2) です。例えば Google MAPs は商用利用可ですが有償です。この利用料金を支払うのは仕様を決める人?作る人?管理する人?それとも別の人?作る内容によって利用頻度(つまり料金)が変わることもあるし、作っている段階で支払いが発生する可能性があるものですが、開発時の利用ルールを決めたりするとそれが足枷になったりすることもあるので、仕様を決める人と作る人、管理する人が分かれていたりすると、この支払い責任を誰が持つのか、というのが悩ましくなります。

#かといって無償のだと、それなりの機能だったりします。。。

最後に (3) です。ほとんどの地図 API はコードを書けば動く、というものではなく、アカウントを作って、そのアカウントを使ってアプリケーションキーを登録し、そのアプリケーションキーを使ってコードを書く、という開発スタイルになります。ということは、極端な例えですが、アプリケーションキーを誤って消してしまったりするした場合、作ったアプリケーションの地図は動かなくなってしまいます。 そんな大事なアプリケーションキーを管理するのは仕様を決める人、作る人、管理する人、誰のアカウントで作るべきでしょうか? で、これも (2) 同様に誰の権限でこのキーを登録するか、という問題が生じるわけです。


以下は私の私見です。

最終的な運用をイメージしなければ(試験的に使ってみるだけなら)、(2) と (3) は作る人がやると手っ取り早いと思います(試験的であればおそらく (1) も)。が、実際の運用を考えるとこれは運用者の責任範囲で行うべきです。が、そうなると見積もりとかいろいろ話が長くなって、なかなか進まなくなるというのも現実です。また実際の運用となると「商用利用」だと思うので、(1) の選択肢においても利用規約を意識する必要がでてきて、話がややこしくなるわけです。


単に「アプリで地図を使う」だけでも、上記のようなややこしい要素が含まれていて、これらを意識しておかないと (1) の調査や稟議で時間がかかってしまったり、仕様変更時に (1) に戻らないといけなくなったり、(2) や (3) で見積もりや手続きで時間がかかってしまったり、、、ということが起こり得るのでした。逆にこの辺りをタイムリーに判断できると、地図アプリを効率よく開発・運用していけるのだと思っています。

ファイルのアップロードを伴うアプリケーションを作っていて悩ましいことの1つに「中身の変わらない(全く同じ)ファイルが別のファイル名でアップロードされることがある」ことです。

アプリケーション側の実装としては、アップロードされたファイルをストレージ等に保存する処理を用意しているのですが、その際のファイル名をどうするか? という問題があります。

例えば "a.jpg" という画像ファイルと、"b.jpg" という異なる画像ファイルがあり、これら2つがアップロードされるケースだけを考えるのであれば、ストレージに保存する際にも元のファイル名をそのまま使えることになります:
2017052201
↑異なる2つのファイルを元のファイル名のまま保存する場合(このケースは問題なし)


ところが更に "a.jpg" というファイル名で、既に保存済みの "a.jpg" とは異なるファイルがアップロードされることもあります。この3つ目のアップロードファイルは "a.jpg" というファイル名で(上書き)保存するわけにはいきません(元のファイルが消えてしまう)。ということは元のファイル名をそのまま使うことは正しい処理ではなくなります:
2017052202
↑同じファイル名で中身の違うファイル保存しようとすると上書きすることになってしまう


また別のケースとして、"c.jpg" というファイル名で、"a.jpg" と中身が全く同じ画像ファイルがアップロードされるケースを考えてみます。この場合、ファイル名そのものは元のもの(c.jpg)を使っても被ることがなくいいのですが、全く同じ画像ファイルを2つ保存することになり、無駄にストレージを消費することになります。画像ファイルであればそのサイズもたかが知れているのかもしれませんが、これが仮想イメージとかだったりすると1ファイルで数10Gバイト消費することもあるため、中身の全く同じファイルであれば複数保存せずにすませたいものです:
2017052203
↑異なる名前で中身の同じファイルを元のファイル名のまま保存すると、無駄な保存領域を使うことになる


上記の問題点を実現する方法として、「同じ中身(バイナリ)のファイルは同じファイル名で、異なる中身のファイルは異なるファイル名を用意して保存する」仕組みを用意する方法があります。で、これを比較的簡単に実現する方法がファイルのハッシュ値を使うことが考えられます:
2017052204
↑ファイルのバイナリデータのハッシュ値をファイル名に使えば、中身が異なるファイルは異なるファイル名になる


試しに Node.js で実装したものを Github に公開しました:
https://github.com/dotnsf/node-upload-sample


画像のバイナリデータ(バイト配列)からハッシュ値を生成しているのは app.js 内の以下の箇所です。Node.js 標準の crypto ライブラリを使って、SHA512 アルゴリズムで path のファイルストリームからハッシュ値を生成している箇所です:
  :
var crypto = require( 'crypto' );

  :
  var path = req.file.path;
  var destination = req.file.destination;

  //. Name after Hash value
  var hash = crypto.createHash( 'sha512' );
  var fstream = fs.createReadStream( path );
  hash.setEncoding( 'hex' );
  fstream.on( 'end', function(){
    hash.end();
    var result = hash.read();

      :

ファイル(画像)をストリーム化してハッシュ値を求め、後ろに元の拡張子を付けたものを保存時のファイル名にしています。


このアプリを実際に動かしてみた様子を以下に紹介します。まず動作確認するにあたって、3種類4つのファイルを用意しました。01.png と 02.png はファイル名は異なりますが、全く同じファイルです。03.png は名前も中身も異なります:
2017052301


これらとは別に、中身の異なる 01.png というファイルを用意しました(ファイル名だけ前述のものと被ります)。これら3種類4つのファイルを全て順にアップロードした時の様子が以下です:
2017052302


まずアプリケーションを起動するとこのような画面になります。登録した画像ファイルが一覧表示されますが、まだ何も登録していない状態では何も表示されません。ここで 01.png (03.png と同じ画像の方)を選択して登録してみます:
2017052301


01.png のハッシュ値が生成され、その値をファイル名として保存されました:
2017052302


このリンクをクリックすると、元の 01.png が登録されていることが確認できます:
2017052303


同様にして、今度は 03.png を登録してみます。すると 01.png と 03.png は異なる画像ファイルであるため、ハッシュ値も異なります。そのため別画像として新たにされます:
2017052304


続けて今度は 02.png (01.png と同じ画像ファイル)を登録します。このファイルはファイル名こそ別ですが実体が 01.png と同じものであるためハッシュ値は 01.png と同じものになります。そのため「既存のデータ」と判断され、新たに画像は登録されません(正確には同じ名前のファイル名で、同じ内容を上書きすることになるので、ファイルは増えず、中身が変わることもありません):
2017052305


更にもう1つの 01.png(元の 01.png とはファイル名は同じだが、中身の異なるもの)を登録してみると、今度は中身の違うファイルなのでハッシュ値も別のものになり、新しいファイルとして登録されます:
2017052306


結果的には3種類4つのファイルを登録しましたが、内容の異なる3つのファイルがアップロードされました。中身の同じファイルは二重登録しない、という当初の目的が達成できました:
2017052307


これでストレージの無駄な空間を使わずに済ませられそうです。

サーバーサイドで動的に画像を作りたい、という要求を実現する方法はいくつかありますが、今回は Node.js から使えるライブラリ "node-canvas" を紹介します:
https://github.com/Automattic/node-canvas/


まず、このライブラリを使う上ではいくつかのネイティブライブラリが必要です。詳しくは上記オフィシャルページを参照いただきたいのですが、例えば Ubuntu 環境であれば以下のコマンドを最初に実行して、必要なネイティブライブラリをあらかじめ用意しておきます:
$ sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++

ネイティブライブラリの導入後に、以下のコマンドで node-canvas をインストールします:
$ npm install canvas

ちなみに、Bluemix の SDK for Node.js ランタイムを使う場合には上記のネイティブライブラリが導入されたビルドパックを利用するので、実行環境でのネイティブライブラリの有無に関しては意識する必要はありません。


さて、node-canvas ではサーバーサイドで HTML5 の Canvas を操作するイメージで動的に画像を作ったり、変更したりすることができます。

ブラウザ上でも Canvas は JavaScript で操作することが多いと思いますが、以下のようにほぼ同じような操作で扱うことができます:
var fs = require( 'fs' );

var Canvas = require( 'canvas' ),  //. ここでライブラリ読み込み
    Image = Canvas.Image;          //. 画像生成用オブジェクト

  :

//. public という空ディレクトリをあらかじめ用意しておく(そこに画像を作る)
app.use( express.static( __dirname + '/public' ) );

app.get( '/xxx', function( req, res ){
  //. /xxx に GET アクセスがあったら、その場で画像ファイル xxx.png を生成して、画像にリダイレクトする
  var img = new Image;
  var canvas = new Canvas( 300, 300 );
  var ctx = canvas.getContext( '2d' );

  //. 斜めに赤い線が1本引いてあるだけの画像を作る
  ctx.beginPath();
  ctx.moveTo( 100, 100 );
  ctx.lineTo( 200, 200 );
  ctx.strokeStyle = 'red';
  ctx.stroke();

  //. 画像を Base64 エンコードで取り出して、デコードして、xxx.png という名前で保存する
  var b64 = canvas.toDataURL().split( ',' )[1];
  var buf = new Buffer( b64, 'base64' );
  fs.writeFile( __dirname + '/public/xxx.png', buf, function(){
    res.redirect( '/xxx.png' ); //. 作った画像にリダイレクト
  });
});

上記は僕が作ったサンプルコードの一部を多少変更したものです。Node.js で実行して、/xxx というパスに GET アクセスがあると動的に /xxx.png という画像ファイルを作って(そこにリダイレクトして)表示する、というものです。実際に画像を描いたり作ったりしているのは赤字の部分なのですが、ほぼそのままブラウザ内の JavaScript でも動く記法になっています。

普段から HTML5 の Canvas を使っている人であれば、そのスキルをそのままサーバーサイドで動的に画像を作る技術に応用できると思います。とても便利。

 

Linux ラブ!なプログラマーが陥った罠(おそらく多くの人が陥る罠)を紹介します。業務で Windows 機を使い、開発したアプリは Linux で上で動かす、という(よくある)環境で育ってきました。個人では Linux をデスクトップ用途でも使ってます。

なお、以下では全角文字で「¥(円)」と「\(バックスラッシュ)」を記述していますが、全て半角文字の時に起こる問題です。


そんな自分が Mac mini を支給され、Mac OS で Node.js の開発をしていた時です。「テキストファイルを読み取って、行ごとに分解(して何らかの処理を)する」という、珍しいとも思えない行分解プログラムをこんな感じで正規表現を使って書いてました:
const fs = require( 'fs' );
var text = fs.readFileSync( "xxxx.txt" );
var lines = text.split( /¥r¥n|¥r|¥n/ );     //. 改行コードがどうなっているのかわからないので、考えられる3通りで
console.log( "#lines = " + lines.length );  //. 試しに何行のデータだったのかを確認

これで改行コードが LF+CR でも LF でも CR でも、どのパターンのファイルでも split できるはず! と思っていたのですが、どんなファイルを試しても
  #lines = 1
と表示されてしまうのでした。"1" ということは1行、つまり分割ができておらず、元のデータがまるごと1つの配列要素になっただけ、ということ。

あれ? 正規表現の指定の仕方はこれじゃないかな?? ちょっと自信がなくなって、改行コードをオペレーティングシステムの設定値から取得するように変えてみたのですが、これでも結果は変わりません(そりゃそうだ、元のファイルの改行コードが OS の設定値になっている保証はない):
const fs = require( 'fs' );
const os = require( 'os' );
var text = fs.readFileSync( "xxxx.txt" );
//var lines = text.split( /¥r¥n|¥r|¥n/ );
var lines = text.split( os.EOL );           //. オペレーティングシステムの改行コードを指定
console.log( "#lines = " + lines.length );  //. 試しに何行のデータだったのかを確認

今まで普通にできていたことが急にできなくなって、ついに若年性ナントカの症状が・・とか不安になったのですが、このことを先人に相談したら一発で解決しました。
  「マックは¥(円マーク)と\(バックスラッシュ)のコードが違う」


え?、どういうこと?? 何言ってるのかよくわからないんですけど。。 

というわけで、歴史的にもややこしい経緯ごと振り返って紹介します。以下は表記上でわかりやすくするために全角で¥と\を書きますが、実際は半角文字だと思ってください。

昔々、国際規格においては(日本の通貨記号である)¥という文字はありませんでした。国際規格では\という文字がキー文字として定義されており、その文字コードは 0x5C でした。ここまでは何の問題もありません。

ここから話がややこしくなってくるのですが、この 0x5C というコードには国際規格的には\(バックスラッシュ)文字が定義された上で「各国が別の文字記号を割り当ててもよい」とされたのでした。そこで日本ではよく使われる¥(円)文字を割り当てました。つまり日本規格では¥というキー文字が定義されており、その文字コードはやはり 0x5C となりました。ここまでもあくまで割り当てルールの話なので、まあわからなくもないです。


そして時代は流れ、コンピュータが世の中に普及してくるにつれ、これまでの先進国だけでなく発展途上国でも使われるようになってきました。すると、これまでのコンピュータ文字体系の仕組みの中で新しい言語を更に追加して個別に扱うことに無理が生じてきました。そして「あらためて世界中の文字を単一体系で表現できるようにしよう」というユニコード(Unicode)の規格が生まれ、これが広まっていきました。このユニコードでは\(バックスラッシュ)と¥(円)も別の文字として区別して定義されました。基本的にはこの Unicode が現在も世界中で使われている規格です。


この新しい規格では¥と\がちゃんと区別されたのはいいのですが、これに移行するにあたってこれまでに日本語のファイルの中で "¥" として書かれた文字は、ユニコードでは "¥" とするべきなのか "/" とするべきなのか、という正解のない新しい問題が生じてしまいました。

この時に、内部的にシフト JIS というコードを採用していた当時のマイクロソフトの日本語版 Windows では Unicode 対応するにあたって「シフト JIS の日本語の¥は、Unicode では全て\として扱う。ただし表示では¥とする」という「見た目を変えない」ことを重視した解決策を取りました。つまり日本語版 Windows で¥のつもりで入力していたものは、実際には全て\でした。その表示上の見た目だけが(本来は異なる)¥になっていたのでした。見た目を変えない現実的な方法だったと思いますが、これがわかりにくい混乱の原因にもなったのでした。

一方、アップルの Mac OS X では元から Unicode 規格が採用されていました。つまり元から¥と\は区別されていました。日本語版のキーボード右上の¥キーを表示されるものは、あくまで¥であり、Option キーと一緒に¥キーを押して表示されるものが\でした(つまり Mac OS X では \ を出す方がややこしい)。


僕自身はこの段階でも「この違いって普通に使ってて問題にならないの?」と感じたのですが、実際はあまり大きな問題にはなっていないようです。マイクロソフトの「見た目を¥にして、内部的に\を使う」という解決策がいかに現実的なものだったかということだと思いますし、なにより自分がいままで¥のつもりで入力していた文字が実は(内部的には)\だった、ということを知りませんでした。知らなくてもどうにかなっていた、という事実の説得力がとても大きく感じられます。


これで¥と\をめぐる歴史的な問題は解決されました。めでたし、めでたし・・・ というわけにはいかない問題が残ってました。日本語でも英語でもない、プログラミング言語が関わった場合の話です。


多くのプログラミング言語においてはバックスラッシュはタブや改行などの目に見えない制御文字を定義する時に使うなど、特殊な意味を持つ記号とされています。この特殊記号が日本のコンピュータ規格では(バックスラッシュではなく、同じコードを共有する)¥とされていました。例えばタブ記号は国際規格的には "\t"(バックスラッシュ+t) ですが、同じものが日本では "¥t" と表示されていたのでした。見た目は異なりますが、元々\と¥は 0x5C という同じコードを共有していたので「要するに同じ文字」で、この段階では日本においては ¥ イコール \ と思っておけばまあ問題ない、とされていました。事実自分も「この2つは同じもので、使うシステムやフォントによって表示時の見た目が変わる」程度に理解していました。

ところが厳密に Unicode が使われるようになると話が変わってきます。例えば画面上で "¥t" と表示されている文字の内部コードは、タブを意味する「バックスラッシュ+t」のことなのか、「円記号+t(こちらは特別な意味のない2文字)」のことなのかが、使っている機種によって違ってくる、という事態が起こってしまいました。例えば Windows を使っている人が書いた "¥t" という文字列(正確には「画面上で "¥t" と表示されている文字列」)と、Mac OS を使っている人が書いた "¥t" という文字列は内部的には別のものになる、ということになります。


で、冒頭の話です。自分はもともと Windows を使っていて(Linux を使う時も Windows のターミナルアプリから使っていて)、その感覚で改行コードは「¥r¥nと¥rと¥nの3種類を考えればいい」と判断して、
var lines = text.split( /¥r¥n|¥r|¥n/ );  //. 本当は全部半角

という、改行制御を行う特殊記号を指定した正規表現の書き方にしました。でもこれは Mac OS では特殊記号を指定する時には¥ではなく\を使って、
var lines = text.split( /\r\n|\r|\n/ );  //. 本当は全部半角

のように書くのが正解だったのでした。要するに慣用的に「¥と\は一緒」と思ってしまうと、この落とし穴に気付くことができず、見て写したり、コピペしたりした結果動かなくなる、という症状に悩まされることになるのでした。

プログラミング以外にも、文字コードで一致を探す検索の機能などでも意識する必要があると思うのですが、裏を返すとプログラミングとかしない人にとっては特に¥と\の違いを意識する必要もなく Mac も Windows も使えているわけで、すごい歴史の一端を垣間見る経験でした。


このページのトップヘ