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

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

タグ:nodejs

IBM Cloud から提供されているマネージド NoSQL データベースである IBM Cloudant をよく使っています。最大の魅力は無料アカウントでも容量 1GB まで使えるストレージで、かつデータが分散管理されているという安全性です。仕様が曖昧な状態からアジャイルに作り始める際は(テーブル定義などを意識する必要がないこともあって)むしろ NoSQL データベースの方が便利だったりもします。

この IBM Cloudant を Node.js プログラムから使う場合のライブラリとして長く @cloudant/cloudant を使ってきましたが、2021/12/31 を以って End of Support を迎えることになり、現在は deprecated なライブラリ、という扱いになっています。2021年8月の今は「まだ使えるけど、もうすぐ使えなくなるよ」って所でしょうか。可能であれば、これから新たに作り始める時には利用を避けたほうが安全だと思います。

で、その後継ライブラリとして公開されているのが @ibm-cloud/cloudant (IBM Cloudant Node.js SDK)です。IBM Cloudant だけでなく、そのベースとなっている Apache CouchDB に対してももちろん使うことができるものです。現在 @cloudant/cloudant ライブラリを使っているアプリケーションはなるべく今年中にこの新しい @ibm-cloud/cloudant に移植することをおすすめします。

・・・と言うのは簡単ですが、実際この @ibm-cloud/cloudant は @cloudant/cloudant と比べてどのくらい似ていて/同じで、移植はどの程度簡単/難しいのでしょう? というわけで、まずは @ibm-cloud/cloudant を使ってみることにしました。


【サンプルソースコード】
以下で紹介するサンプルアプリケーションのソースコードを github で公開しています:
https://github.com/dotnsf/cloudant-node-sdk_sample


【接続方法の決定】
@ibm-cloud/cloudant を使って Cloudant(CouchDB) にアクセスする場合の接続方法には3通りあります:
(1)IAM
(2)COUCHDB_SESSION
(3)BASIC


(1)の IAM は IBM Cloudant を IAM 接続サポート形式で作成した場合に利用できる方法です。公式ドキュメントでも「この方法が使える場合はこの方法で」と紹介されています。

(2)の COUCHDB_SESSION を使った方法でも接続が可能です。

(3)の BASIC はいわゆる「ベーシック認証」です。この3つの中では「この方法しか使えない場合の選択肢」と紹介されています。CouchDB を自分でインストールして使う場合など、この方法で接続する前提のセットアップをしているとこの方法でしか接続できないことになります。もちろん Cloudant でもベーシック認証をサポートした形式で作成している場合は利用可能です。

(1)、(2)、(3)のいずれの方法でアプリケーションとデータベースを接続するかを決めておく必要があります。正確には @ibm-cloud/cloudant は「環境変数を設定して接続する」のですが、どの方法で接続するかによって、設定が必要になる環境変数が変わる点に注意が必要です。


【Node.js から接続する際に必要な環境変数】
@ibm-cloud/cloudant を使って Cloudant/CouchDB に接続する際に設定が必要な環境変数は以下です(横のカッコ付き数字は、上述の(1)、(2)、(3)のどの方法を使った時に必要な環境変数か、を表しています)。また変数名の頭の "CLOUDANT" 部分は別の値でも構いませんが、同じ文字列に統一して設定する必要があります:

変数意味
CLOUDANT_AUTH_TYPE上述の接続方法。"IAM"(1), "COUCHDB_SESSION"(2), "BASIC"(3) のいずれか。デフォルト値は "IAM"
CLOUDANT_URLデータベースの URL((1)、(2)、(3))
CLOUDANT_APIKEYAPI キーの値((1))
CLOUDANT_USERNAMEユーザー名((2)、(3))
CLOUDANT_PASSWORDパスワード((2)、(3))


環境に合わせてこれらの値を用意して実際に接続してみます。


【Node.js から Cloudant に接続】
(1)IAM 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'IAM'; //(デフォルト値なので設定しなくてもよい)
process.env['CLOUDANT_APIKEY'] = 'xxxxx'; //(API Key の値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

このコード実行が成功すると、Cloudant に接続したインスタンスが client という変数に格納され、実際のデータの CRUD 処理が可能になります。

なお上記コードの serviceName 値として 'CLOUDANT' を指定していますが、この部分は変更可能です。ただ変更する場合は環境変数として指定した変数名の頭の CLOUDANT 部分をここで指定する値と同じものに変更してください。

(2)COUCHDB_SESSION 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'COUCHDB_SESSION';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

(3)BASIC 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'BASIC';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'http://xxx.xxx.xxx.xxx:5984'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT', disableSslVerification: true } ); 

なお(CouchDB の場合に多いと想像していますが) SSL 接続が不要の場合は上述のように接続時のパラメータに disableSslVerification: true を追加してください。

参考までに、@cloudant/cloudant の場合は、
var Cloudantlib = require( '@cloudant/cloudant' );
var cloudant = Cloudantlib( { account: "username", password: "password", url: 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud' } );
var db = cloudant.db.use( "dbname" );

といった感じでデータベースまでを取得した上で、
db.get( "id", function( err, result ){
  if( err ){
    console.log( err );
  }else{
    console.log( result );
  }
});

のようにして特定データを取得したりしていましたが、この @ibm-cloud/cloudant ではまだデータベースが特定されていない点にご注意ください。


【接続後の CRUD 処理の例】
では接続後の Cloudant クライアントを使って、IBM Cloudant のデータを読み書きしてみましょう。まず対象データベースと id がわかっている特定データを取得してみます。@ibm-cloud/cloudant では以下のような getDocument() メソッドを使うコードとなります:
client.getDocument( { db: "dbname", docId: "id" } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

client.getDocument() を実行し、そのパラメータ内でデータベースと id を指定する、という方式になります。この点からして @cloudant/cloudant とは異なってますね。

また特定データベースの全データをまとめて取得(@cloudant/cloudant だと db.list() )する場合は以下のような postAllDocs() メソッドを使うコードです:
client.postAllDocs( { db: "dbname", includeDocs: true } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

データを一件追加する場合は以下のような postDocument() メソッドを使うコードになります:
client.postDocument( { db: "dbname", document: { name: "Kimura" } } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

以上、あくまでいくつかのメソッドを紹介しただけですが、全般的に行うオペレーションとメソッド名の関係がわかりやすく整頓されているように感じて、比較的慣れやすいのではないかと感じています。



IBM Watson から提供されている AI 系 API の中で、自然言語テキストを分類する機能を持った NLC(Natural Language Classifier) の提供が終了することがアナウンスされました。2021年9月9日までは新規インスタンスの作成が可能ですが、それ以降の新規作成はできません。また既存インスタンスは2022年8月8日まで利用できますが、それまでに移行先を決め、その移行作業を済ませておく必要があります:
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating


NLC は個人的にも IBM Watson を使い始めるきっかけになった API で、思い入れの深いサービスだったりします。NLC を使って作った多くのデモアプリは WordCamp Tokyo や特定のお客様向けに作ったものを含めて多くの場で紹介させていただきました。感慨深いものがあります。


上記アナウンス内で、公式な移行先として同じ IBM Watson の NLU(Natural Language Understandings) が紹介されています。が、具体的な移行手順や方法に関する情報が少ないこともあり、実際に自分が NLC API を使って開発したアプリケーションをどのように NLU に移行すればよいかがわかりにくいように感じました。 自分自身の場合は普段 Node.js を使ってアプリケーションを開発しているのですが、Node.js の場合は具体的にどのようにすれば NLC から NLU へ移行できるか? という調査をしてみました。実際に Node.js でプログラムを書き、具体的にはまず NLC API を使って、
 ・認証
 ・日本語データ学習
 ・学習状況確認
 ・問い合わせ(分類)
 ・学習の削除
といった5種類のオペレーションを行えるようなアプリケーションを作りました。 そしてそのアプリケーションを NLC から NLU へ実際に移植する、といった作業を行ってみました。結論として上記5種類のオペレーションは NLU でも NLC 同様に行うことができました。ただ API には互換性はなく、ソースコードレベルではそれなりに変更が必要になるため、どの程度の変更が必要になるのか、という調査の意味も含めて作業した様子を以下に紹介します。

なお、NLC は IBM Cloud の(無料版の)ライトアカウント向けには提供されていない API である点に注意してください。ライトアカウントの状態で NLC の新規インスタンスを作成することは(9月9日以前であっても)できません。ベーシック以上の(有償の)アカウントであれば無料枠含めて提供されている API です。 一方の NLU はライトアカウントでも利用することが可能ですが、ライトアカウントの場合はライトプラン(無料)の NLU を1インスタンスのみ作成でき、1インスタンスにつき1モデルだけ学習で作成できます(要は複数の学習モデルを作成するにはベーシック以上のアカウントで、有料プランを選択する必要があります)。
2021082200



【サンプルコードのダウンロード】
以下で紹介する一連の手順を行うための Node.js 用サンプルコードを作って公開しました:
https://github.com/dotnsf/nlc2nlu

git clone するかコードをダウンロード&展開してください。以下の3つのフォルダが含まれています:
|- csvtool
|- nlc
|- nlu

csvtool は実際に NLC や NLU で使うサンプルの学習データを作成するツールです。NLC や NLU を既に使っていて、どのような学習データを用意すればよいかわかっている場合は、ご自身で学習データ(CSV ファイル)を用意いただいてもかまいませんが、そうでない人向けに Yahoo! ニュースの RSS(https://news.yahoo.co.jp/rss) を使って、その場で学習データを作成できるようにしたものです。

nlc と nlu は名前の通りで、学習データを学習させて、学習状況を確認しながら学習が完了したら、実際に適当な日本語テキストを送信して、学習データに基づくテキスト分類を実施します。また実施後に学習データを削除する、といった操作も可能です。これらの内容を NLC および NLU それぞれで行えるようにしたツールが含まれています。

ではそれぞれのフォルダの使い方を説明します。


【csvtool : 学習データの用意】
csvtool/csvgenerator.jsYahoo! ニュース RSS から学習データとしての CSV ファイルを作成します。NLC / NLU とも学習データは以下のフォーマットの CSV ファイルを用意します※:
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
  :

※ NLU は JSON フォーマットでも学習データを準備して読み込むことが可能ですが、NLC からの移植を考えると NLC と同じ条件で学習データを用意するべきと考え、同じ CSV ファイルを学習データとして使うことにします。

このような(NLC 向けの)学習データを既にお持ちであればそれを使って後述の作業を続けていただいてもかまいませんが、多くの人はそのような学習データを持っていないと思うので、新たに用意することにします。その場合は csvtool/csvgenerator.js を使います。

実行方法は単純で、csvtool フォルダに移動し、まず普通に $ npm install を実行して依存ライブラリを導入します:
$ cd csvtool

$ npm install

その後、node コマンドでこのファイルを実行します。実行コマンドの最後に出力先 CSV ファイル名を指定します(以下の例だと同一フォルダ内の csvfile.csv):
$ node csvgenerator csvfile.csv

実行したタイミングで Yahoo! ニュースの RSS を参照して、その時点のニュースを取得して CSV ファイルに書き出します。なお、このツールでは「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」の5つのカテゴリーのニュースを収集します。 ツールの実行が完了すると、指定した CSV ファイルが以下のような内容で生成されます:
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
   :

ここまでできれば学習データの準備は完了です。ではこのデータを NLC と NLU それぞれで学習させて問い合わせる、という作業をこれ以降で行っていきましょう。


【nlc : NLC で操作】
まずは NLC API を使って開発したアプリでこのデータを学習させ、NLC API を使って問い合わせを行ってみましょう。先ほどダウンロードしたソースコードの nlc フォルダ内にある nlc/nlc.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlc.js はコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlc create [csvfilename]  ・・・データ学習

$ node nlc status  ・・・学習状況確認

$ node nlc classify [日本語テキスト] [classifier_id]  ・・・問い合わせ(分類)

$ node nlc delete [classifier_id]  ・・・学習データ削除

では実際に nlc ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLC インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082201


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlc/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlc_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlc_url = 'サービス資格情報内の url 属性値';
exports.nlc_name = 'nlc2nlu';
exports.nlc_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlc create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlc create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlc status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています(データ量にもよりますが、自分が試した時はこうなるまでおよそ 10 分程度かかりました)。同時に表示されている classifier_id の値はこの後で使うので合わせてメモしておきましょう:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "application/json",
    "x-xss-protection": "1",
    "content-security-policy": "default-src 'none'",
    "x-content-type-options": "nosniff",
    "cache-control": "no-cache, no-store",
    "pragma": "no-cache",
    "expires": "0",
    "content-length": "428",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-request-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-global-transaction-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "22",
    "date": "Tue, 24 Aug 2021 05:48:32 GMT",
    "connection": "close"
  },
  "result": {
    "classifier_id": "e87efex297-nlc-650",
    "name": "nlc2nlu",
    "language": "ja",
    "created": "2021-08-24T05:45:20.090Z",
    "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650",
    "status_description": "The classifier instance is now available and is ready to take classifier requests.",
    "status": "Available"
  }
}


学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。日本語テキストと上述で確認した classifier_id 値を指定して、以下のようなコマンドを実行します:
$ node nlc classify [日本語テキスト] [classifier_id]

すると以下のような結果が表示されます。指定した日本語テキスト(「パッキャオがんばれ」)を学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。この例では「スポーツ」の可能性が 50% 強である、と判断されているようです:
$ node nlc classify パッキャオがんばれ e87efex297-nlc-650
{ "status": 200, "statusText": "OK", "headers": { "content-type": "application/json", "x-xss-protection": "1", "content-security-policy": "default-src 'none'", "x-content-type-options": "nosniff", "cache-control": "no-cache, no-store", "pragma": "no-cache", "expires": "0", "content-length": "679", "strict-transport-security": "max-age=31536000; includeSubDomains;", "x-dp-watson-tran-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-request-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-global-transaction-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "server": "watson-gateway", "x-edgeconnect-midmile-rtt": "7", "x-edgeconnect-origin-mex-latency": "43", "date": "Tue, 24 Aug 2021 05:51:22 GMT", "connection": "close" }, "result": { "classifier_id": "e87efex297-nlc-650", "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650", "text": "パッキャオがんばれ", "top_class": "スポーツ", "classes": [ { "class_name": "スポーツ", "confidence": 0.5022989563107099 }, { "class_name": "エンタメ", "confidence": 0.20692538301831817 }, { "class_name": "経済", "confidence": 0.1200947580342344 }, { "class_name": "科学", "confidence": 0.11456564859743573 }, { "class_name": "IT", "confidence": 0.05611525403930185 } ] } }


作成した学習データを IBM Watson NLC から削除するには以下のコマンドを実行します:
$ node nlc delete [classifier_id]

ここまでのオペレーションで「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを NLC API を使って実現し、そのアプリケーションを実行する方法を紹介しました。


【nlu : NLU で操作】
では改めてこの nlc アプリケーションを NLU API で作り直し、同じ4つのオペレーションができることを確認してみます。先ほどダウンロードしたソースコードの nlu フォルダ内にある nlu/nlu.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlu.js もコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlu create [csvfilename]  ・・・データ学習

$ node nlu status  ・・・学習状況確認

$ node nlu analyze [日本語テキスト] [model_id]  ・・・問い合わせ(分類)

$ node nlu delete [model_id]  ・・・学習データ削除

※前述の NLC では問い合わせ時に "classify(分類)" という命令を指定していましたが、NLU では同じ作業を "analyze(解析)" と表現しているようです。またパラメータとして指定する ID も NLC では "classifier_id(分類ID)" だったのですが、NLU では "model_id(モデルID)" という表現になっていました。

では実際に nlu ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLU インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082401


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlu/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlu_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlu_url = 'サービス資格情報内の url 属性値';
exports.nlu_name = 'nlc2nlu';
exports.nlu_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlu create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlu create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlu status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています。同時に表示されている model_id の値はこの後で使うので合わせてメモしておきましょう。この辺りまでは前述の NLC の時とほぼ同様ですね:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "x-powered-by": "Express",
    "content-type": "application/json; charset=utf-8",
    "content-length": "401",
    "etag": "W/\"191-CBnewThU0u/CjNzD01hil1wp9qo\"",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-request-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-global-transaction-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "228",
    "date": "Tue, 24 Aug 2021 05:54:08 GMT",
    "connection": "close"
  },
  "result": {
    "models": [
      {
        "name": "nlc2nlu",
        "user_metadata": null,
        "language": "ja",
        "description": null,
        "model_version": "1.0.0",
        "version": "1.0.0",
        "workspace_id": null,
        "version_description": null,
        "status": "available",
        "notices": [],
        "model_id": "619ad785-0b0c-4981-8217-bd51064896a3",
        "features": [
          "classifications"
        ],
        "created": "2021-08-24T05:43:32Z",
        "last_trained": "2021-08-24T05:43:32Z",
        "last_deployed": "2021-08-24T05:50:10Z"
      }
    ]
  }
}

学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。NLC では "classify" と指定していた部分は NLU では "analyze" となる点に注意してください。日本語テキストと上述で確認した model_id 値を指定して、以下のようなコマンドを実行します:
$ node nlu analyze [日本語テキスト] [model_id]

すると以下のような結果が表示されます。指定した日本語テキストを学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。同じ学習データで同じ問い合わせをして、こちらでも「スポーツ」の可能性が高いと判定されていますが、その確率や2位以下の結果には違いがあることがわかります:
$ node nlu analyze パッキャオがんばれ 619ad785-0b0c-4981-8217-bd51064896a3

{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "server": "watson-gateway",
    "content-length": "499",
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-cache, no-store",
    "x-dp-watson-tran-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15, 93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "content-security-policy": "default-src 'none'",
    "pragma": "no-cache",
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY",
    "x-xss-protection": "1; mode=block",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-request-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-global-transaction-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-edgeconnect-midmile-rtt": "10",
    "x-edgeconnect-origin-mex-latency": "354",
    "date": "Tue, 24 Aug 2021 05:56:16 GMT",
    "connection": "close"
  },
  "result": {
    "usage": {
      "text_units": 1,
      "text_characters": 9,
      "features": 1
    },
    "language": "ja",
    "classifications": [
      {
        "confidence": 0.402721,
        "class_name": "スポーツ"
      },
      {
        "confidence": 0.326038,
        "class_name": "経済"
      },
      {
        "confidence": 0.314985,
        "class_name": "IT"
      },
      {
        "confidence": 0.255796,
        "class_name": "エンタメ"
      },
      {
        "confidence": 0.21978,
        "class_name": "科学"
      }
    ]
  }
}


作成した学習データを IBM Watson NLU から削除するには以下のコマンドを実行します:
$ node nlu delete [model_id]

ここまでのオペレーションで NLC 同様に NLU でも「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを API で実現し、そのアプリケーションを実行する方法を紹介しました。とりあえず、この4つのオペレーションについては NLC から NLU へ移行することはできそうだ、という感触が持てる結果になりました。


【API レベルの移行作業】
同じオペレーションを行う NLC のツールと NLU のツールを実際に Node.js + IBM Watson SDK で開発してわかったことを記載しておきます。

まず API に互換性はありません。IAM を使った認証部分についてはほぼ同じなのですが、今回行った4つのオペレーションを実現するためのそれぞれの API は NLC のものと NLU のものは全く異なります:
NLC の関数 オペレーションの種類 NLU の関数
IamAuthenticator() 認証 IamAuthenticator()
createClassifier() 分類器/モデルの作成 createClassificationModel()
listClassifiers() 作成した分類器/モデルの参照 listClassificationModels()
classify() 分類/解析 analyze()
deleteClassifier() 分類器/モデルの削除 deleteClassificationModel()


パラメータの指定方法など、詳しくはソースコード(nlc/nlc.js / nlu/nlu.js)を参照していただきたいのですが、大まかには上記表のような関数の違いがあります。特に Node.js + IBM Watson SDK を使っているアプリケーションにおいて NLC から NLU へ移植する場合は、表の左列にある関数を使っている箇所を右列の関数に置き換える、というのが大まかな流れになると思います(実際にはパラメータの指定方法だったり、返り値のフォーマットの違いもあったりするので、単なる文字列置換というわけにはいかないと思います)。また NLC では「分類器(Classifier)」と呼んでいたものが NLU だと「分類モデル(ClassficationModel)」と呼び名が変わっていたりもするので、資料を参照する場合の注意も必要だと思います。

加えて、上記のように NLC を使って問い合わせた結果と NLU に移植後に問い合わせた結果は必ずしも同じ結果とはいきません。このあたりの整合性についても移植の前後で意識しておく必要があります。

とはいえ、少なくとも API のレベルで NLC から NLU へアプリケーションを移植することは不可能ではなさそう、という感触も得ることができました。主要なオペレーションに関しては上述の表を使って関数を新しいものに置き換え、実行結果のフォーマットの違いをプログラミング内で吸収することができれば、ある程度の実現目途は立ちそうだと思っています。


今回の NLC のサービス終了自体は残念ではありますが、一方で見方を変えると、これまで有償サービスでしか提供されていなかった NLC の機能が、無料のライトプランで使える NLU でも利用できるようになった、とも言えます。API レベルでの互換性はありませんが、機能的には珍しくこれまでの機能の多くが移植されていて、「アプリケーションの作り変え」による対応が可能なレベルでマイグレーションができるようになっていると感じました。「料金的にも発展的なサービス統合」であると感じています。


Node.js 以外の開発言語での場合や、IBM Watson SDK の利用有無の違いをどこまで吸収できるかまでを調査したわけではないのですが、一部の言語については後述の参考資料の中でサンプルコードもあるようなので、別環境においてもぜひ挑戦していただき、情報が共有されていくことを願っています。



【参考資料】
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating

https://cloud.ibm.com/apidocs/natural-language-understanding?code=node


Tips 的な小ネタです。

Node.js + Express によるウェブアプリケーションコードの中で、何らかの URL への GET リクエスト(POST とかでもいいですが、処理内容は GET の時と同じなので GET で考えることにします)を受けて処理している時の、アクセス時のフル URL をサーバー側で知る方法です。 なお、ここでの「フル URL 」とは、プロトコル+ホスト名+(デフォルトと異なる場合は)ポート番号+アクセスパス+実行時のURLパラメータ のこととします。

この値はクライアント側の JavaScript を使えば windows.location オブジェクトを参照することで取得できます。ただこちらはあまり意味がないというか、アクセスしたユーザーは自分のブラウザのアドレス欄を見れば URL を確認できるのでわざわざ別途必要になるケースが珍しいはずです。このクライアントサイドでの話ではなく、サーバーサイドの処理内で知る方法、という意味です。

結論としては以下のようなコードで取得することが可能です:
//.  app.js
var express = require( 'express' ),
    app = express();

app.get( '/*', function( req, res ){
  res.contentType( 'text/plain; charset=utf-8' );
  var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;
  res.write( url );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

まずルーティングのパス定義部分を '/*' としています。これによってルートパス以下のすべてのパスへの GET リクエストをこのハンドラが受け持つ、と宣言します。

肝心のフル URL ですが、このハンドラ実行時のパラメーター: req(リクエストオブジェクト)を使って、以下のように求めることができます:
 var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;

req.protocol にはプロトコル("http" または "https")、req.get( 'host' ) でポート番号まで含めたアクセス時のホスト名、そして req.originalUrl にはアクセス時のフルパスが URL パラメータまで含めた形で取得できます。これらをつなぎ合わせることでアクセス時のフル URL が取得できるので、これをレスポンスで返す、という処理をしています。

試しに実行していくつかの URL パターンでアクセスしてみた所、以下のようにいずれも期待通りの結果になりました:
(http://localhost:8080/)
2021062301


(http://localhost:8080/abc/hello?x=100)
2021062302


(http://localhost:8080/abc/hello?x=100&y=200)
2021062303


1つの環境で複数のサーバー名を持って稼働するサーバーの場合に、「何というホスト名でアクセスされているのか」をサーバー側からも知ることができる、という情報でした。

なおこのアプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/access_url


どれだけ需要があるかわかりませんが、docker イメージ(dotnsf/access-url)の形で以下からも公開しています:
https://hub.docker.com/r/dotnsf/access_url


利用可能な Kubernetes クラスタ環境(と接続設定などが済んだ kubectl コマンド)があれば、以下の手順でコマンドを実行することで Deployment と(spec.type = "NodePort" の) Service を作成できます:
$ git clone https://github.com/dotnsf/access_url

$ cd access_url

$ kubectl -f yaml/app_deployment.yaml

IBM Cloud の無料版 IKS(IBM Kubernetes Services) で、上記コマンドを実行して作成したアプリケーションにアクセスした時の様子が以下になります。アクセスした URL が正しくサーバーサイドで取得できている様子が確認できます:
2021062401

 
最後に余談を。上述のクライアントサイド JavaScript による(window.location オブジェクトを用いた)取得方法との取得できる情報の違いについて補足します。

クライアントサイドで取得する場合、サーバーサイドで取得できない情報が1つ取得できます。それが「ハッシュ」と呼ばれる情報で例えば、
 http://xxx.xxx.xxx.xxx/abc/hello?x=1&y=2#here
という URL アドレスの最後の "#here" 部分の情報です。

この情報はクライアントサイドであれば window.location.search を参照することで取得することが可能ですが、サーバーサイドでは取得する方法がありません。ただハッシュ情報はクライアントサイドで(HTML 内の特定位置を参照するなど)利用するためのものであって、サーバーサイドで生成する情報としてはハッシュによる差異はありません。要はサーバーサイドでは意味のない情報であるためサーバー側では取得できなくなっている、ものと思われます。



IBM Cloud から提供されている Db2 on Cloud を、2021/05/23 時点では最新となる v4 API を使ってアクセスするまでを実現した Node.js のサンプルアプリケーションを作りました。アクセストークンを取得して SQL を実行し、その結果を取得するまでの一連の流れを紹介します。


Db2 on Cloud は IBM 製リレーショナルデータベース製品である Db2 をマネージドサービスとして IBM Cloud から提供しているサービスです。IBM Cloud のライトアカウントを作成し、フリープランを選択することで、データは 200MB までなどの制約はありますが無料で利用することも可能です:
2021052301


この Db2 は製品としての経緯もあり、専用(ネイティブ)クライアントライブラリをインストールした上で JDBC/ODBC などから利用することが多かったのですが、近年は Node.js 向けのライブラリなども提供されるようになり、各種プログラミング言語からの利用もできるようになっていました。その API の最新版(v4)では REST API 対応が行われ、(ラズベリーパイなど)専用クライアントライブラリが存在しなかった環境からの利用も可能になりました。

というわけで、今回のブログエントリではこの v4 API を使って Db2 on Cloud のインスタンスにアクセスして SQL を実行し、その実行結果を取得するまでの一連の手順をサンプルアプリケーションとそのコードを参照しつつ紹介します。


【サンプルアプリケーション】
以下で紹介するサンプルアプリケーションの(Node.js 向け)ソースコードはこちらで公開しています:
https://github.com/dotnsf/node-db2


Node.js がインストールされていれば Windows でも Mac でも Linux でも動きます。特にこのアプリケーションは v4 API で作っているので、これまでは(クライアントライブラリが提供されていないため)Db2 にアクセスできなかったラズベリーパイなどの環境からでも利用できる点が特徴になっています。なのでラズパイ環境をお持ちだったらぜひラズパイから利用してみていただきたいです。

サンプルアプリを利用するには、まず IBM Cloud 側の準備が必要です。大まかに以下2段階の準備を行います:
(1)IBM Cloud にログインして Db2 on Cloud サービスインスタンスを作成
(2)作成したインスタンスの接続情報を取得


まず(1)を行います。IBM Cloud に(必要であればアカウントを作成して)ログインし、Db2 サービスインスタンスを作成します:
2021052302


この際に "Lite" プランを選択しておくとデータ量 200MB 上限や、同時接続数 15 などの制約がありますが、無料で利用することができます:
2021052303


(1)のインスタンスを作成したら(2)の準備を行います。作成したインスタンスの「サービス資格情報」タブを選択し、「新規資格情報」ボタンで「サービス資格情報」を1つ追加して、その中身を確認します:
2021052304


以下のような JSON フォーマットの情報を確認することができます:
{
  "db": "BLUDB",
  "dsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50000;PROTOCOL=TCPIP;UID=username;PWD=password;",
  "host": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "hostname": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "https_url": "https://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "jdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "parameters": {
    "role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager"
  },
  "password": "password",
  "port": 50000,
  "ssldsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50001;PROTOCOL=TCPIP;UID=username;PWD=password;Security=SSL;",
  "ssljdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50001/BLUDB:sslConnection=true;",
  "uri": "db2://username:password@dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "username": "username"
}

ここで必要になるのは以下の3つの値です:

username
password
hostname


加えて deployment id を取得します。この ID はサービス固有の ID で、すべての REST API 実行時に必要な値です。取得方法はサービスを表示しているブラウザ画面の URL を確認します:
2021052305


おそらくこのようなフォーマットの URL 文字列になっています:
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

この中の "crn" で始まる文字列(? が含まれる場合はその手前まで)を URL デコードした値が deployment id です。URL デコードといってもパターンは2つに決まっていて、"%3A" を ":" に、"%2F" を "/" に変換するだけです。例えば該当の Db2 on Cloud サービスを表示している時の URL 内文字列がこの内容だった場合、
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

deployment id は crn で始まる青字部分を URL デコードした値ということになり、具体的には以下の値となります:
crn:3v1:bluemix:public:dashdb-for-transactions:us-south:a/xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx::

以上、ここまでの作業で Db2 on Cloud のサービスインスタンスを作成し、その username, password, hostname, deployment id の4つの情報を取得することができました。この4つの値が取得できれば、実際にサンプルアプリケーションを動かすことができます。


上述のリポジトリ URL から git clone やダウンロード&展開するなどしてソースコード一式を取得します。そして settings.js をテキストエディタで開いて、先程取得した4つの値を入力して保存します:
2021052306


これでソースコード側に必要な変更も完了です。では実際に Node.js で動かしてみましょう:
$ cd node-db2

$ npm install

$ node app

ここまでのコマンドを実行すると、サンプルのウェブアプリケーションは 8080 番ポートで待ち受ける形で起動します。ウェブブラウザ(別 PC からでも可)からサンプルの HTTP リクエストを送信して、Db2 on Cloud で SQL を実行し、その結果を取得します:
http://(上記の $ node app を実行したマシンの IP アドレス):8080/ping

なお、この GET /ping リクエストは Db2 on Cloud インスタンスに対して以下の SQL を実行した結果のシステム情報を取得するように作られたものです:
select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;

実際にウェブブラウザでこの URL にアクセスするとこのような結果になります。上述の SQL を実行した結果が JSON で返ってきました。仮にアプリケーションをラズベリーパイ上で動かしていても同様の結果が取得できているはずです。これまでは Db2 のクライアントライブラリが導入できる環境からでないと実行できなかった SQL を、v4 API では(サーバー上のもの以外には)クライアントライブラリを使わない環境でも実行して結果を取得することができる、ということがわかります:
2021052307


【ソースコード解説】
ではソースコードを見ながら v4 API の解説をします。上述のオペレーションで使っているファイルは変数設定以外は app.js のみなので、全容を確認したい場合はこのファイルを参照ください。

上述で行ったのは1つの SQL を実行して、その結果を取得する、というごく普通のオペレーションですが、v4 API では大きく3つのパートに分かれて処理されています((2)と(3)が別れて処理されている点に注目):

(1)アクセストークン取得
(2)SQL 実行
(3)SQL 結果取得


まず(1)について。これまでの Db2 API では認証時にユーザー名とパスワードがあればログインして SQL を実行することができましたが、v4 API では(今どきらしく)まずアクセストークンを取得して、アクセストークンを付与しながら SQL を実行する必要があります。というわけでアクセストークンの取得手順を紹介します。

具体的には以下のようなコードを実行しているのですが、settings.js に設定した username と password を https://(hostname)/dbapi/v4/auth/tokens に POST しています。またその際の HTTP リクエストヘッダ内で deployment id の値を送付しています(HTTP リクエストヘッダに deployment id を含める、というのは以下すべての REST API で共通です)。成功した場合、返されるオブジェクトの token キーにアクセストークンが付与されて返ってくるので、この値を保存しています(実際のアプリではセッション等に保存しておくべきです):
var access_token = null;
var option = {
  url: 'https://' + settings.hostname + '/dbapi/v4/auth/tokens',
  json: { userid: settings.username, password: settings.password },
  headers: { 'content-type': 'application/json', 'x-deployment-id': settings.deployment_id },
  method: 'POST'
};
request( option, async function( err, res0, body ){
  if( err ){
    console.log( { err } );
  }else{
    if( body && body.token ){
      access_token = body.token;
      console.log( { access_token } );
    }
  }
});


GET /ping にリクエストがあると、このアクセストークンを使って(2)SQL を実行します。app.js では以下のように実行しています:
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  if( access_token ){
    var sql0 = 'select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;';
    var option0 = {
      url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs',
      json: { commands: sql0, limit: 10, separator: ';', stop_on_error: 'no' },
      headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
      method: 'POST'
    };
    request( option0, async function( err0, res0, body0 ){
      if( err0 ){
        console.log( { err0 } );
        res.status( 400 );
        res.write( JSON.stringify( { error: err0 }, null, 2 ) );
        res.end();
      }else{
        if( body0 && body0.id ){
            :
            :

GET /ping リクエストへのハンドラとして、まず "select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;" という SQL を https://(hostname)/dbapi/v4/sql_jobs に POST して実行しています。この処理が成功すると、返されるオブジェクトに id というキー値が含まれています。

そして(3)では、実行した SQL の結果を取得します。その際には(2)で取得した id キー値を指定して GET https://(hostname)/dbapi/v4/sql_jobs/(id) を実行して、その結果が先程のブラウザ画面のようなフォーマットで取得されていたのでした:
            :
            :
        if( body0 && body0.id ){
          console.log( { body0 } );
          var option1 = {
            url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs/' + body0.id,
            headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
            method: 'GET'
          };
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              res.status( 400 );
              res.write( JSON.stringify( { error: err1 }, null, 2 ) );
              res.end();
            }else{
              //. body1 は string
              body1 = JSON.parse( body1 );
              console.log( { body1 } );
              res.write( JSON.stringify( body1, null, 2 ) );
              res.end();
            }
          });
        }else{
          res.status( 400 );
          res.write( JSON.stringify( { error: body0 }, null, 2 ) );
          res.end();
        }
      }
    });
  }else{
    res.status( 400 );
    res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    res.end();
  }
});


NoSQL とは違って、トランザクションを含む(時間のかかる)処理を実行する可能性もあるため、その間で処理をブロックしないようにこのような仕様で API が用意されているのだと思います。上の例では SQL の実行と結果の取得を続けて行って実行結果を返していますが、(途中に取得できる id をいったんクライアントに返すことで)これら2つの処理を別の REST API で分けて実行させることも可能になります。シングルスレッドの Node.js での実装を考慮すると、こちらのほうがより正しいといえるかもしれません。


【良い点・悪い点】
なんといっても「ラズベリーパイなどの、Db2 クライアントライブラリが提供されていないシステムから Db2 サーバーに接続してクエリーを実行できる」ようになったことが大きな改善点であると感じました。また時間のかかる処理を実行する際にも、まとめて実行して、その間の他のリクエストをブロックすることなく、後から結果を確認できるようになっている点もクラウド時代らしいといえます。

唯一残念な点は「プレースホルダー型の SQL に未対応」な点です。SQL を比較的安全に実行する上でよく使われるプレースホルダー型の SQL("SELECT * from xxxxx where id = ?" で、パラメーターの id を分けて送って実行する SQL)には現時点ではこの REST API では未対応のようで、現時点では開発者が気をつけて実装するか、(クライアントライブラリが提供されているシステムから)従来の専用ライブラリを使って実行する必要がありそうでした。


【参考】
IBM Db2 on Cloud REST API


簡易ブラウザゲームなど、HTML + 画像 + CSS + (クライアントサイド)JavaScript のみで作成できる静的ページの公開手段として Github ページを使っている人は少なくないと思っています。かくいう自分もその一人で、例えば以下のページはすべて自作した上で Github ページを使って公開している例です:
ブラウザ移植版 "MOLEMOLE"
マッチ棒を1本だけ動かして正しい式にする(を自動解答)
JavaScript + CSS + HTML オンラインプレビューワー


あらためて Github ページを簡単に紹介します。

ベースとなっている Github は無料で(も)使えるソースコードのオンラインバージョン管理システムです。個人的には 10 年前は全く別の Subversion というバージョン管理システムを自分で自宅サーバーにインストールして使っていました(一応まだ残ってますが、ほぼ使わなくなりました)が、この 10 年で状況ががらりと変わり、今ではほぼ Github (というか、Git を使ったシステム)がバージョン管理の主流となりました。 Github ページはこの Github の機能の1つとして提供されているもので、Git プロジェクト(の一部)のフォルダをまるごと HTML のドキュメントルートとして公開するというものです。いわば「Github の安定した HTTP サーバーを使って公開する無料のウェブページ」とも言えるものです。あくまで静的コンテンツしか公開できないため、データベースにデータを登録する機能などは使えませんが、CORS の設定や REST API 、AJAX などを併用することで外部ファイルや外部データベースからデータを取得して表示する、といった程度であれば実現できます。自分自身は上述のような簡易アプリケーションに加えて、ドキュメント類やランディングページを公開する際にも便利に使っています。


このように便利な Github ページですが、Github に登録した後は便利なのですが、登録前のコンテンツ作成時にちょっと使いにくいと感じることもありました。例えば以下のようなケースを考えてみます:
2021042901


↑リポジトリの説明に必要な README.md と、Github ページで表示する index.html を同じフォルダに配置しています(このルートフォルダを Github ページで公開する、という想定です)。

index.html 内では imgs/icon.png ファイルを参照しています。また外部 JavaScript である jQuery(https://code.jquery.com/jquery-2.2.4.min.js)の URL を指定して利用しています:
2021042902


ちなみに img/icon.png は「いらすとや」さまの「静かにしてください」のマークを使わせていただきました:
icon


index.html 自体は上述のように比較的シンプルな内容です。(ローカルファイルをフルパス指定して)ブラウザで表示すると以下のような内容が表示されるものです:
2021042903


ちなみに <body> 部はこれだけ:
<body>

<h1>GHP(GitHub Pages) サンプル</h1>

<img src="./imgs/icon.png"/>

</body>

このプロジェクトを(深く考えずに)Github ページで公開する場合の手順を紹介します。そんなに複雑な話ではなく、まず単純に Github にパブリックなプロジェクトを作り、このフォルダ内の全ファイルを同プロジェクトに git push します(以下は自分のリポジトリに push した際の例です):
$ git init

$ git add .

$ git commit -m 'first commit'

$ git branch -M main

$ git remote add origin https://github.com/dotnsf/ghp.git (ここは実際の URL を指定してください)

$ git push -u origin main


2021042904

 
これでプロジェクトのバージョン管理を Github で行えるようになりましたが、今回の目的はバージョン管理というよりも Github ページによるコンテンツ公開なので、もう1ステップ必要になります。同プロジェクトの "Settings" メニューから "Pages" を選び、公開方法(どのブランチのどのフォルダ以下をページとして公開するか)を指定します。今回の場合は main ブランチのルートディレクトリをそのまま公開するだけなので、以下のように指定して "Save" します:
2021042905


すると以下のような画面になり、このコンテンツが Github ページで公開されます(実際にインターネットからアクセスできるようになるまで1分弱かかるようです):
2021042906


表示されている URL にアクセスすると、ちゃんとコンテンツとして公開できていることを確認できます。(オリジナルドメインで GitHub ページを使う設定をしていなければ)HTTPS にも自動的に対応されていて、非常に便利な機能です:
2021042907


ここまでが Github ページの概要および使い方です。index.html の内容を更新したり、新しいファイル(ページ)を追加した場合でも同様にしてプロジェクトの更新ファイルを Github に登録(git push)するだけで公開されるコンテンツの内容も更新されます。静的コンテンツの公開目的には非常に便利なサービスですが、一方で細かな点ではありますが、この方法だと少し非効率に感じる点がないわけではありません。自分の場合は以下の2点で少し不便に感じています。

まず一点目として、ローカルでの動作確認時にブラウザでローカルファイルを開く必要がある、という点があります。上述でもローカルファイルの内容を参照する際には file:/// から始まる URL で(つまりブラウザのメニューや Ctrl + O で「ファイルを開く」を選択してから、目的のフォルダを探し、その中の index.html を指定して)開いていました。自分の場合は普段 Node.js を使ってウェブアプリケーションを作るのですが、その際の動作確認時は http://localhost:8080/ といったシンプルな URL でアプリケーションを開けるようにしています。この差もあって、動作確認するたびにこの「ファイルを開く」オペレーションをするのは少し面倒に感じてしまいます:
2021042903


もう一点は「外部ファイルのプロトコル」についてです。例えば index.html 内で jQuery を呼び出す部分は以下のように記載しています:
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

気になるのは赤字の部分、つまり目的のファイルを https 指定で呼び出しています。現実問題として https で指定できるファイルを https で呼び出すことにはセキュリティ面での問題はないとも言えます。

一方で、これまで Node.js でウェブアプリケーションを開発している中でこのような外部 JavaScript の呼び出しを行う必要がある際、http か https かを指定せずに記述することがあります。つまり普段は以下のように指定しています:
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>

この記述方法だと表示するページ(今回の例だと index.html)を呼び出す際に使うプロトコルと同じプロトコルで jquery ファイルを呼び出すことになります。つまりローカルでの動作確認時など http://localhost:8080/ で呼び出している場合は http プロトコルで、実際に Github ページとして https://dotnsf.github.io/ghp/ で呼び出している場合は https プロトコルで、それぞれ呼び出すことになり、外部ファイル呼び出しもアクセス時のセキュリティレベルに合わせて行うことができるようになっています。

ただこの方法はローカルファイルを直接呼び出す際の file プロトコルでアクセスした場合は正しく読み込むことができません(file:///code.jquery.com/jquery-2.2.4.min.js は存在しないため)。ローカルファイルを呼び出す場合はプロトコルを省略せずに明記する必要があり、http か https のいずれかを指定して記述する必要があるのでした。

つまり、Github ページで運用する際には index.html 内の外部ファイル呼び出し部でプロトコルまで指定せずに記述することができるのに、動作確認時はプロトコルを指定して呼び出す必要がある。ということになります。これは非常に面倒な話で動作確認が終わったらプロトコル記述を削除する(Github ページを更新した後に、更にまた手元のファイルを変更する必要が生じた場合はもとに戻す)という運用は面倒だし、だからといってそのためだけにプロトコルを固定して記述しなければならないのだとしたら筋の違う話だと思っています。ローカルで常に HTTP サーバーを動かして、対象フォルダをドキュメントルートにしておけば、動作確認時も http://localhost:8080/ のようにアクセスできるようにはなりますが、それも本末転倒な話です。なんとかして Github ページを作る際もローカル動作確認時だけ HTTP サーバーを使って動作させることができないものか・・・


・・・と、長い前振りでしたが、ここからが本題です。アプリケーション・サーバーや HTTP サーバーを併用しないと解決が難しそうで、かつ Github ページ利用時に余計なコードが公開されないようにしたい、という上述の問題を解決する方法を紹介します。以下に紹介する方法は Node.js + Express を使った方法で、どちらかというとサーバーサイド JavaScript のアプリケーションエンジニア向けですが、言語環境によっては同様のことができる別方法(というか別言語環境)があるかもしれません。

答としてはプロジェクトを以下のようなファイル構成に変更します:
2021043001


Github ページで公開していたコンテンツ関連ファイル(今回の例では index.html と imgs/icon.png)を新たに作成した docs/ フォルダ内に移動しました。プロジェクトのルートフォルダには README.md を残した上で、新たに .gitignore, app.js, package.json ファイルを追加しています。以下、追加したファイルの内容を紹介します。

.gitignore ファイルは一般的なもので、Node.js では node_modules/ フォルダを Git 連携の対象外フォルダとすることが一般的です。というわけで、今回は以下の内容の .gitignore ファイルを使います:
node_modules/
package-lock.json
!.gitkeep

package.json ファイルも特殊なものではなく普通の package.json ファイルです。後述の app.js が express パッケージのみ利用するものなので、このような内容にしました:
{
    "name": "ghp",
    "version": "0.0.1",
    "scripts": {
        "start": "node app.js"
    },
    "dependencies": {
        "express": "^4.17.1"
    },
    "repository": {},
    "engines": {
        "node": "10.x"
    }
}

そして Node.js のメインファイルとも言える app.js 、これも実はごくシンプルな内容のものです:
//. app.js
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/docs' ) );

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

特筆というほどのことはないのですが、肝といえるのは赤字部分です。Express を使ってウェブアプリケーション内のスタティックコンテンツが含まれるフォルダを docs/ フォルダに指定しています。これにより docs/ フォルダ以下にあるファイルはアプリケーションのルートパスから相対パスで表示できるようになります。つまりブラウザなどのクライアントから ./index.html と指定すると ./docs/index.html ファイルが、./imgs/icon.png と指定すると ./docs/imgs/icon.png ファイルを参照することができるようになります。つまり Github ページで公開するのとほぼ同じ状態をローカルホストで作ることができるようになります。

実際にローカルホストで動作確認用に動かす場合は、Node.js インストール後に以下のコマンドを実行します。特にオプションを指定しない場合は 8080 番ポートで HTTP サーバーが起動します:
$ npm install

$ node app
server starting on 8080 ...

8080 番以外の番号で待ち受けたい場合は起動時に環境変数 PORT に待受ポート番号を指定します(以下は 8081 番で待ち受ける場合):
$ PORT=8081 node app
server starting on 8081 ...

起動後、localhost の待受ポート番号を指定してブラウザでアクセスすれば(Ctrl + O で index.html ファイルを指定して開かなくても)目的のページを開くことができます:
2021050101


この状態であれば file プロトコルではなく http(s) プロトコルを使っているので、 docs/index.html 内の外部 JavaScript 参照部分でのプロトコルも明示する必要がなく、以下のような記述でも(ローカルでの動作確認時も、Github ページでの利用時も)動作するようになります。ローカルでの開発時と Github ページで公開する時の差を意識する必要もなくなりました:
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>

最後にこの変更を Github ページでも反映する必要があります(ルートディレクトリを Github ページとして公開している設定だと index.html ファイルが開かないので)。まずは更新した内容を Github リポジトリにもコミット&プッシュして反映させます:
$ git add . -A

$ git commit -m 'node.js 対応'

$ git push origin main

次に Github ページの設定を変更し、/(ルートフォルダ)ではなく /docs/ フォルダ以下を Github ページとして公開するように変更して Save します:
2021050102


少し待つとこの変更が Github ページにも反映され、先程と同じ URL で同様のページを参照することができるようになります:
2021042907


この状態ができてしまえば、手元でページを編集するときは docs/ フォルダ以下のファイルを対象に変更を加えて、必要に応じて `$ node app` でローカルの HTTP サーバーでコンテンツの出来を確認し、Github にコミット&プッシュすれば自動的に Github ページにも更新が反映されるようになります。

もうちょっとスマートなやり方があるのかもしれませんが、Node.js + Express であればこのように「Express の静的コンテンツフォルダを docs/ にする」+「Github ページの対象フォルダを docs/ 以下にする」という設定を組み合わせることで面倒からの開放は実現できそうでした。


このページのトップヘ