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

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

タグ:nodejs

これまであまり積極的に使うことのなかった webpack を使ってみました:
2018082300


webpack は複数のモジュール(ファイル)を1つに(場合によっては複数に)まとめるバンドラ(bundler)ツールです。Node.js でいうと、エントリーポイントとなる main.js ファイルと、ここから呼ばれるサブ機能が sub1.js や sub2.js などの別ファイルに分かれている場合に、これら3つのファイルを1つのファイルにまとめるというものです。

【サンプルの紹介】
今回は jsonwebtoken パッケージを使ったシンプルなサンプルコード(main.js)を用意しました:
// main.js
const jwt = require( 'jsonwebtoken' );

function main( params ){
  const id = params.id;
  const password = params.password;
  const token = jwt.sign( { id: id, password: password }, 'secret' );

  console.log( token );
}

main({
  id: 'userid',
  password: 'password'
});

global.main = main;

↑ main() 関数の中で2つのパラメータ(id と password)を受け取り、JSON をトークン化して出力する、というものです。そして id = 'userid', password = 'password' を指定して main を実行しています。

この main.js を実行する前に、コード内で使っている jsonwebtoken パッケージをインストールする必要があります。以下の内容の package.json を用意します:
{
  "name": "jwt_test",
  "version": "0.0.1",
  "main": "main.js",
  "dependencies": {
    "jsonwebtoken": "^8.3.0"
  }
}

そして以下のコマンドで jsonwebtoken パッケージをインストールします:
$ npm install


その後に main.js を普通に実行すると、{ id: 'userid', password: 'password' } をトークン化した結果が表示されます:
$ node main.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRvdG5zZiIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJpYXQiOjE1MzQ5OTU5NTh9.LUx0q5ycwIFeR1HNnNFgUrlX0w0cny0qDVsPmiVtVnI

とりあえずこの状態で動くことは確認できました。コードは main.js 1つだけに記述されていますが、jsonwebtoken パッケージを利用しているので、実行前に $npm install (jsonwebtoken) を実施しておく必要があり、実際にはここでインストールされるモジュールファイルも使われながら動作します。これらを1ファイルにまとめた上で実行できるようにするというのが今回の目的です。


【webpack のインストール】
webpack は npm を使ってインストールします。例えば以下のコマンドでグローバルインストールすることができます:
$ npm install webpack -g

今回は webpack を(このプロジェクト用に)ローカルインストールして使うことにします。なお webpack 4.0 以降では webpack-cli も合わせて導入する必要があるのでまとめてインストールし、webpack コマンドが実行できるようローカルモジュールへのパスを通します:
$ npm install webpack webpack-cli

$ export PATH=$PATH:./node_modules/.bin


【config ファイルの用意】
webpack を使ってバンドルする作業の条件を config ファイルと呼ばれるファイルで指定します。以下の内容で webpack.js ファイルを作成します:
var path = require( 'path' );
module.exports = {
  entry: './main.js',
  output: {
    path: path.resolve( __dirname, 'dist' ),
    filename: 'bundle.js'
  },
  target: 'node'
};

↑ main.js をエントリーポイント(最初に実行するファイル)とする一連の処理の中で必要とされるモジュール(今回の例では jsonwebtoken パッケージ)がバンドルの対象となり、バンドル結果は dist/bundle.js というファイルに出力される、という指定をしています。


【バンドルと実行】
ここまでに準備した環境とファイルを使って webpack を使ってみます。

まずはバンドル、上記で作成した webpack.js を config ファイルとして指定して webpack コマンドを実行します:
$ webpack --config webpack.js

バンドルが成功すると dist/bundle.js というファイルが生成され、この1ファイルに main.js と jsonwebtoken パッケージがバンドルされているはずです。

確認のため生成されたファイルを実行します。念の為、これらのパッケージモジュールが存在しないテンポラリディレクトリ(/tmp)に bundle.js ファイル1つだけをコピーした上で実行します:
$ cp dist/bundle.js /tmp
$ cd /tmp
$ node bundle.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRvdG5zZiIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJpYXQiOjE1MzUwMDM1MjF9.llZuuWxqX607AWrJBvMEDpFksFX8IvigUrdIWbhItRg

期待通りにトークンが表示される結果になりました。この bundle.js 1ファイルの中に jsonwebtoken パッケージやその依存パッケージまでが含まれる形でバンドルされて実行されたことが確認できました。


webpack を使うことで今回の例のように依存関係ごとファイルをまとめることができるだけでなく、ファイルのモジュール化が促進されたり、一度のロードで全ファイルを読み込めることから SPA(Single Page Application) が作りやすくなったりします。その一方で、バンドル先のファイルが大きくなってしまうと最初のロードに時間がかかるようになる、という問題もあります。個人的にはパッケージ化されたあとに開発コンソールを使ったデバッグが難しくなることに懸念も持っています。

まあケース・バイ・ケース、なんだろうなあ。。

 

Node.js を使ったアプリケーション開発中に npm install を実行して、こんなエラーに遭遇することがあります(個人的な印象では下の例のように canvas モジュールをインストールしようとした際によく遭遇します):
$ npm install canvas

> canvas@1.6.11 install /Users/dotnsf/src/tmp/node_modules/canvas
> node-gyp rebuild

gyp WARN download NVM_NODEJS_ORG_MIRROR is deprecated and will be removed in node-gyp v4, please use NODEJS_ORG_MIRROR
gyp ERR! configure error 
gyp ERR! stack Error: Python executable "/Users/dotnsf/.pyenv/shims/python" is v3.6.5, which is not supported by gyp.
gyp ERR! stack You can pass the --python switch to point to Python >= v2.5.0 & < 3.0.0.
gyp ERR! stack     at PythonFinder.failPythonVersion (/Users/dotnsf/.nvm/versions/node/v8.9.4/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:492:19)
gyp ERR! stack     at PythonFinder. (/Users/dotnsf/.nvm/versions/node/v8.9.4/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:474:14)
gyp ERR! stack     at ChildProcess.exithandler (child_process.js:267:7)
gyp ERR! stack     at emitTwo (events.js:126:13)
gyp ERR! stack     at ChildProcess.emit (events.js:214:7)
      :
      :

スクリーンショット 2018-08-21 9.44.07


Node.js でネイティブモジュールのビルドが必要になった場合に node-gyp を使ってビルドが行われるのですが、その際に使われる Python のバージョンがあっていない、というエラーです。ちと厄介なのは Python のバージョンが古くて問題になっているのではなく、新しすぎてエラーが発生している、ということです(上の例では v2.5.0 以上 v3.0.0 未満でないといけないのに v3.6.5 がインストールされていてエラーが発生している、というメッセージが表示されています)。

この解決のためにわざわざ古いバージョンをインストールしないといけないのか、ということはなく、以下のようにバージョンを明示して npm install することで解決できます:
$ npm install canvas --python=python2.7

> canvas@1.6.11 install /Users/dotnsf/src/tmp/node_modules/canvas
> node-gyp rebuild

gyp WARN download NVM_NODEJS_ORG_MIRROR is deprecated and will be removed in node-gyp v4, please use NODEJS_ORG_MIRROR
      :
      :

+ canvas@1.6.11
added 2 packages in 10.027s


「エクセルファイルを扱えるライブラリ」といえば、Java であれば Apache POI などがありますが、Node.js ではどうだろう?? と思って調べてみました。その名もズバリの xlsx という npm ライブラリを見つけたので使ってみました:

npm - xlsx
https://www.npmjs.com/package/xlsx

2018080100


ライブラリ名は xlsx ですが、対応フォーマットは xls や XML に加えて ODS まで含まれていて、かなり柔軟に使えそうです。


【扱うサンプル】
こんな感じのエクセルファイルを用意して使うことにします:
2018080101


データとしては "A1:C14" の範囲にまとまっていて、その右に2軸の折れ線グラフが1つあります。この表の B14 セルは B2:B13 の合計(SUM)、C14 セルは C2:C13 の平均値(AVERAGE)がマクロで定義されています。まあ「よくあるシート」だと思っていますが、このエクセルファイルを xlsx で扱ってみます。ちなみに同じファイルがこちらからダウンロードできます:


【読み込み例】
まず npm で xlsx ライブラリをインストールします:
$ npm install xlsx

そして xlsx ライブラリを使ってエクセルファイルを読み込む Node.js コードを作成します。この例ではファイル名を指定して読み込み、"Sheet1" シートを取得して console.log() で出力しています:
var XLSX = require( 'xlsx' );

// ファイル読み込み
var book = XLSX.readFile( './SalesSample.xls' );

// シート
var sheet1 = book.Sheets["Sheet1"];
console.log( sheet1 );


実行結果はこんな感じになります。ダンプされたような感じです:
{ '!margins':
   { left: 0.7,
     right: 0.7,
     top: 0.75,
     bottom: 0.75,
     header: 0.3,
     footer: 0.3 },
  B1: { v: '売上', t: 's', w: '売上' },
  C1: { v: '前年比', t: 's', w: '前年比' },
  A2: { v: 1, t: 'n', w: '1' },
  B2: { v: 7370, t: 'n', w: '7370' },
  C2: { v: 0.87, t: 'n', w: '0.87' },
  A3: { v: 2, t: 'n', w: '2' },
   :
   :
  A12: { v: 11, t: 'n', w: '11' },
  B12: { v: 24380, t: 'n', w: '24380' },
  C12: { v: 0.812, t: 'n', w: '0.812' },
  A13: { v: 12, t: 'n', w: '12' },
  B13: { v: 28283, t: 'n', w: '28283' },
  C13: { v: 0.814, t: 'n', w: '0.814' },
  B14: { v: 156518, t: 'n', f: 'SUM(B2:B13)', w: '156518' },
  C14:
   { v: 0.8603333333333333,
     t: 'n',
     f: 'AVERAGE(C2:C13)',
     w: '0.860333333' },
  '!protect': false,
  '!ref': 'A1:C14',
  '!objects': [ , , , , { cmo: [Object], ft: [Object] } ] }

この結果の見方を少し説明します。例えば sheet1["!ref"] の値は 'A1:C14' となっていて、このシートの中で有効なセルとして認識されている範囲は A1:C14 とされています。つまりグラフ部分は完全に無視されていて、このライブラリでは現在は扱えない情報ということになります。xlsx はあくまで表部分の読み書きを対象としています。

次に sheet1["C2"] の値は { v: 0.87, t: 'n', w: '0.87' } となっています。この意味は以下のようになります:
 v: 0.87(数値としての値は 0.87)
 t: 'n'(数値のセルとして認識されている)
 w: '0.87'(表示されているテキストは '0.87')

なお、t の値は以下のいずれかになります:
 b: Boolean
 n: 数値
 d: 日付時刻
 s: 文字列
 z: スタブ
 e: エラー

したがって sheet1["C2"] の { v: 0.87, t: 'n', w: '0.87' } の意味は「値が 0.87 の数値セルで、画面上では '0.87' と表示されている」ということになります。

また sheet1["B14"] の値は { v: 156518, t: 'n', f: 'SUM(B2:B13)', w: '156518' } となっています。この中の f: 'SUM(B2:B13)' は値が数式で定義されていて、その式が SUM(B2:B13) であることを意味しています(sheet1["C14"] も同様です)。表としてはこのレベルで各セルの値を取得できている、ということがわかります。

この読み込み例のサンプル(app1.js)はこちらからダウンロードできます。こちらのファイルは(後述の書き込み機能の結果を確認できるように)上記のコードに少し機能を追加していて、コマンドラインパラメータで読み込むエクセルファイルを指定可能にしています(無指定の場合は SalesSample.xls を読み込みます):
$ node app1.js (xxx.xls)


【書き込み例】
xlsx ライブラリを使ってファイルの書き込みを行うサンプルです。この例では SalesSample.xls を読み込んだあとに "C13" セルの値を上書きして、SalesSample2.xlsx というファイル名で保存しています:
var XLSX = require( 'xlsx' );


// ファイル読み込み
var book = XLSX.readFile( './SalesSample.xls' );

// シート
var sheet1 = book.Sheets["Sheet1"];
console.log( sheet1 );

// セル更新
sheet1["C13"] = { v: 1.01, t: 'n', w: '1.01' };

// シート更新
book.Sheets["Sheet1"] = sheet1;

// ファイル書き込み
XLSX.writeFile( book, './SalesSample2.xlsx', { type: 'xlsx' } );


(注1 最後の XLSX.writeFile() 実行時の最後のオプション { type: 'xlsx' } を指定しないとマクロ関数が無効な状態で保存されてしまいます)

この書き込みのサンプル(app2.js)はこちらからダウンロードできます。実行はそのまま node コマンドで実行します:
$ node app2.js

実行すると SalesSample2.xlsx というファイルが出来ているはずです。試しにこのファイルをエクセルで開いてみるとこのようになります:
2018080201



"C13" セルの値は { v: 1.01, t: 'n', w: '1.01' } に上書きしましたが、たしかに 0.814 から 1.01 に更新されています。同時に "C14" セルの値も(AVERAGE関数が再計算されて)変わっています。一方でグラフが完全に消えてしまいました。まあ読み込みを実行した時点でグラフの情報は消えていたので、その内容で保存するとこのようになってしまうのだと思います。xlsx ライブラリの制限事項になると思いますが、実際に使う際にはご注意ください。

(注2 厳密には SalesSample2.xlsx ファイルが生成された時点では C14 セルの値は変わっていませんが、このファイルをエクセルで開くと AVERAGE 関数が再計算されて開くので、そこで値が正しく変わったように見えます)


以上、詳細は本家のドキュメントを参照いただきたいのですが、少なくともグラフを操作せずにシートの中身を取り出す用途であれば充分につかえて、対応フォーマットも多そうだな、、という印象を持っています。自然言語処理機械学習の学習データとしてエクセル資産を活用する、なんて話になった時に活躍できそうなライブラリですね。



以前に express-ipfilter ライブラリを使って、Node.js アプリの IP アドレスフィルタリングを行うサンプルを紹介しました:
http://dotnsf.blog.jp/archives/1066182158.html

↑ここで紹介したサンプルは一応動くものですが、アプリケーションを IBM Cloud の Cloud Foundry アプリとしてデプロイすると( IP アドレスフィルタリングが)正しく動かないことがわかりました。原因は Cloud Foundry 内のルーティングで x-forwarded-for ヘッダの情報が変わってしまい、正しい IP アドレスを取得できなくなってしまうようでした。

IBM Cloud の Cloud Foundry 環境でもこの IP アドレスフィルタリングを有効にするには、フィルタリングを行う前に Express() の use メソッドを使って、
app.use( 'trust proxy', true );

を呼び出してからフィルタリングを行う必要があります。

(解説)
http://expressjs.com/ja/api.html



 

IBM ワトソン対応の CMS である BlueCMS を公開しました。IBM Cloud を使ったセットアップ手順はこちらをご覧ください:
ワトソン対応の IBM Cloud 向き CMS "BlueCMS" を公開しました(セットアップ手順)


今回は初期セットアップ後の、実際の使い方を紹介します。


コンテンツタイトル等

初期セットアップの中で管理者権限を持った最初のユーザーを作っているので、このユーザーの ID とパスワードでログインします:
2018071001


管理コンソール画面が表示されます。管理コンソールにはコンテンツタイトルなどコンテンツ全体に関係する設定項目に続き、現在までに登録されている文書の一覧テーブルと、添付ファイルの一覧テーブルが表示されますが、ログインユーザーが管理者権限を持っている場合はコンテンツの設定項目の下にユーザー一覧テーブルも表示されます:
2018071101
(↑上からコンテンツ設定、ユーザー一覧)

2018071102
(↑上から文書一覧、添付ファイル一覧)

コンテンツ設定は以下のようになっています:
2018071103


これらは OGP(Open Graph Protocol) と言われる設定項目になっており、有名どころでは facebook で BlueCMS のトップページや各記事を共有した場合に表示される内容を定義します。

また title と desc は BlueCMS トップ画面の jumbotron の中で表示される内容でもあります。自分のブログのタイトルとその説明を記述するようにしてください。url はブログの URL、image_url は OGP イメージ画像の URL を指定します(指定していない場合は無視します)。

なお、現時点(2018/Jul/12)では個別ページの OGP を設定する機能がなく、個別ページをシェアするとトップページと同じ OGP が表示されます(リンク先の URL だけは個別ページになります)。この辺りは今後の機能拡張で対応したいと思っています。


ユーザー追加/管理

管理者権限を持ったユーザーはユーザー一覧テーブルで登録済みユーザーの一覧を確認したり、編集したり、削除したり、新規にユーザーを追加することができます:
2018071104


新規作成は一番下の編集行の各フィールドに入力して "update"、既存ユーザーの変更は右にある "edit" をクリックすると編集行に値がコピーされるので、ここで変更して "update"、ユーザーの削除は右にある "delete" をクリックします。

なおユーザー編集時には role の値に注意してください。この値が 0 のユーザーは管理者、1 のユーザーは編集者として扱われます。name は画面表示用の名称で、email はメールアドレスですが、これらは現時点では特に利用していません。


文書追加/管理

管理コンソールには現在までに登録されている文書の一覧も表示されます:
2018071105


新規作成は一番下の編集行の各フィールドに入力して "update"、既存文書の変更は右にある "edit" をクリックすると編集行に値がコピーされるので、ここで変更して "update"、文書の削除は右にある "delete" をクリックします。

なお文書の status は 1 のものが公開、0 のものは非公開(ドラフト)となります。body は nicEdit を使ったリッチテキスト編集が可能です。category はカテゴリー文字列を直接指定して入力します(category と body の値は IBM ワトソン連携時に利用する値となります)。

body の入力が狭い nicEdit を使っている点が不便であると理解しています。この辺りも今後も機能拡張の対象と考えています。


添付ファイル追加/管理

管理コンソールには現在までに登録されている添付の一覧も表示されます:
2018071106


添付ファイルの新規作成はファイルを選択後、一番下の編集行の name フィールドに入力して "update"、添付ファイルの削除は右にある "delete" をクリックします。添付ファイルには編集機能はありません。


ワトソン連携

セットアップ時に IBM ワトソンの NLC(Natural Language Classifier) 連携も含めて行っている場合は、BlueCMS 内のコンテンツを NLC に学習させたり、学習結果を使って問い合わせを行うことができます:
2018071107


文書一覧の下に NLC 関連のボタンが3つあります。それぞれ以下のように使います:

- "update NLC" : 現在までに BlueCMS に格納された全文書を NLC のトレーニングデータとして学習を初期化&再学習します。学習時には各文書の body 値と category 値だけを取り出して、body 値の内容を category 値として学習します。これを全ての文書に対して行います。

- "NLC status" : 上記学習命令を発生した後の、ワトソンのトレーニングステータスを確認します。この実行結果が "Available" となれば学習準備は完了していて、後述の "classify" で問い合わせが可能になります。一方、実行結果が "Training" であればまだ学習中なので、いましばらくお待ち下さい。

- "classify" : 学習が済んだ後に問い合わせを実行します。具体的には編集行の body に何か文章を入力した後にこのボタンをクリックすると、上述で学習させたコーパスに対してこの body 内容を問い合わせ、「今までの学習データから、どのカテゴリーがふさわしいか」の結果を取得し、category フィールドを更新します。いわば「ワトソンがその内容に相応しいカテゴリーを自動的に決めてくれる」機能です。


現時点での制限事項等

このブログエントリを編集している 2018/Jul/12 時点での BlueCMS の機能と使い方を紹介しました。上述のように CMS として足りない機能や使いにくい部分も多くあり、ワードプレスなどと比較するとまだまだだと思っています。

一方で新しくスクラッチで開発したからこそできた挑戦的な機能もあります。特に標準で IBM ワトソンと連動する機能については BlueCMS の特徴の1つだと思っています。

自分でも少しずつ使っていきながら感じた機能を拡張させていく予定ですが、もしお試し程度でも使ってみていただける場合は、感想や希望を伝えていただければと思っています。


このページのトップヘ