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

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

タグ:javascript

ウェブブラウザである Chrome の機能を拡張する Chrome extension を作ってみます。今回は試しに
 Chrome でアマゾンの商品ページを見ている時に、そのページ内の ASIN コードを取り出して表示する
というシンプルな機能を実装してみます。

この Chrome 拡張機能の開発方法に関してはこちらに公式ドキュメントがあります。が、Chrome 自身のバージョンアップにともなって機能変更が加わり、それが混乱を招いてしまっている側面はあると思っています。今回紹介する内容はとりあえず 2016/Jun/23 時点では動くことを確認していますが、将来的な保証はないことを最初にお断りしておきます。

今回用意するファイルは次の3つです:
ファイル名用途・コメント
jquery-3.0.0.min.jsjQuery ライブラリ。ここから最新版をダウンロードし、manifest.json 内にファイル名を記述する
manifest.json拡張機能の定義ファイル
script.js拡張機能の実装ファイル


まず jQuery の最新版ライブラリを使います。jQuery のダウンロードページから最新バージョン(今回の例では 3.0.0)をダウンロードします。

次に定義ファイルである manifest.json です。Chrome 拡張機能を作る際に必須の JSON ファイルで、Chrome はこのファイルの中に記述されている通りに拡張されます。今回は以下の様な内容にします:
{
  "name": "AmazonASIN",
  "version": "0.0.1",
  "manifest_version": 2,
  "description": "Amazon商品ページからASINコードを取り出す拡張",
  "content_scripts": [
    {
      "matches": ["http://www.amazon.co.jp/*","https://www.amazon.co.jp/*"],
      "js": ["jquery-3.0.0.min.js","script.js"]
    }
  ]
}

この中では以下の様な指定を行っています:
  • 拡張の名前(name)は "AmazonASIN"
  • 拡張のバージョン(versoin)は 0.0.1
  • マニフェストのバージョン(manifest_version)は2(固定)
  • description に拡張の説明を記述
  • content_script 内にスクリプトの条件を指定
  • このスクリプトは http(s)://www.amazon.co.jp/ 内のページを参照している時だけ動く
  • 動くファイルは前述の jquery-3.0.0.min.js と後述の script.js

簡単に言うと、アマゾン(www.amazon.co.jp)のページを参照している時に script.js が動く、という指定をしています(jquery-3.0.0.min.js は script.js 内で利用しているのでここに記述しています)。

そして今回紹介する拡張機能の本体となるのが script.js です。この内容は以下のとおりです:
$(function(){
 $(".col2 .pdTab tr:first").each(function(){
  var tr = $(this);
  var td1 = tr.children('td:first');
  if( td1.html() == "ASIN" ){
    var td2 = tr.children('td:nth-child(2)');
    var asin = td2.html();
    window.prompt( "ASIN", asin );
  }
 });
});

jQuery にある程度詳しい人であれば簡単に理解できそうなほどシンプルな内容ですが、中身を一応紹介します。

まず最初に、自動化したい作業はこんな感じです:
  1. アマゾンの商品ページを開いたら、
  2. 「登録情報」の「ASIN」と書かれた箇所(下図参照)を探して、
  3. その横の ASIN 番号を取り出して表示する

2016062301

↑この機能、アフィリエイターに需要ありますかね・・・


そしてこのアマゾン商品ページの HTML ソースを見ると、『「登録情報」の「ASIN」と書かれた箇所』というのが、『col2 クラスを持った div パート内の、pdTab クラスを持った div 内のテーブルの、最初の tr パート』であることがわかります:

2016062302


この tr の2番目の td に目的の ASIN 番号が書かれているのでこれを取り出すのが目的です。というわけで、上記の script.js 内では以下の様な処理を記述しています:
  • Amazon のページでのみ以下を実行する(manifest.json で指定済み)
  • ページが全て読み込まれた後に以下を実行
  • col2 クラス以下の pdTab クラスの更に下にあるテーブルの最初の1行(最初のtr)を取り出す
  • 取り出した tr 内の1つ目の td を取り出し、その中に "ASIN" と書かれていることを確認する
  • 確認できたら tr の2つ目の td (ここに ASIN 番号がある)を取り出して、その HTML 文字列を取り出す
  • 取り出した ASIN 番号を画面に表示する

これで処理の記述はできたので、上記3つのファイルを全て同じフォルダ(下図では c:\tmp\amazon_extension)内に保存します:
2016062303


作成した Chrome 拡張はデベロッパーモードの Chrome 内に読み込ませて利用することができます。Chrome を起動後、右上のメニューボタンから 「その他のツール」-「拡張機能」 を選択します:
2016062304


Chrome 拡張機能のメニューが表示されたら、右上の「デベロッパーモード」にチェックを入れます。これでデベロッパーモードになったので、更に「パッケージ化されていない拡張機能を読み込む」ボタンをクリックします:
2016062305


先程作成した3つのファイルを格納したフォルダ(上記の例では C:\tmp\amazon_extension\)を指定して拡張機能を読み込みます:
2016062306


拡張機能にエラーがなければ正しく読み込まれ、拡張機能一覧に表示されます。「有効」にチェックが入っていることを確認してください(チェックを外すと、この拡張機能は動きません。またその右のゴミ箱アイコンをクリックすることで拡張機能を Chrome から削除することもできます):
2016062307


では改めてアマゾンの商品ページを見てみましょう。例えばこのページを参照してみると、、
https://www.amazon.co.jp/gp/product/B014CGROJW/


商品ページがロードされた後に以下の様なダイアログが表示され、ASIN コードが取り出されたことが確認できるはずです(必要であれば、ここからコピーできます):
2016062308


Chrome 拡張機能のごくシンプルな例ですが、こんな感じで作れます。実体はクライアントサイドの JavaScript ですが、クロスサイトスクリプティングの制約を受けることなく AJAX が実行できたりするので、この中で外部連携も含めた様々な処理を記述できます。例えばここで紹介されているサンプルでは Chrome 拡張から Watson API を実行していたりします:
女優・ディベロッパー池澤あやかさんが作成したアプリとは? |Bluemix Developers Lounge


JavaScript で Cloudant や CouchDB を操作できるようになるライブラリの1つに PouchDB があります:
https://pouchdb.com/

2016031701


PouchDB は CouchDB および CouchDB と互換性のあるローカル NoSQL データベースです。JavaScript を使ってデータを読み書きし、またリモートの CouchDB サーバーにアクセスしたり同期したりすることもできるものです。広い意味ではデータベース本体および JavaScript ライブラリを合わせて PouchDB と呼ぶこともあります。

という特徴をもった、この PouchDB の JavaScript ライブラリを使うと、リモートの CouchDB データベースにアクセスすることができるようになります。JavaScript ライブラリなのでクロスサイトスクリプティング等のセキュリティ制約を意識しながら使う必要があります。

今回、紹介するのはこんな環境で JavaScript を使ってデータベースにアクセスする、というものです。HTML ファイルがローカルディスク内にあるので、普通にアクセスするとクロスサイトスクリプティング制約にかかって使えませんが、その制約を回避する方法と合わせて紹介します:
2016031705


まずは操作先の CouchDB サーバー環境を用意します。外部からアクセスするので Access-Control-Allow-Origin ヘッダのカスタマイズも必要になります。この辺りの手順は以下2つのブログエントリを参照ください:


この CouchDB サーバーの IP アドレスは XX.XX.XX.XX、ポート番号はデフォルトの 5984 であるとして以下を記述します。まずは CouchDB サーバーの管理コンソールにアクセスして、この後リモートから操作するためのデータベースを1つ作成しておきましょう。ウェブブラウザで http://XX.XX.XX.XX:5984/_utils/ にアクセスして管理コンソール画面にアクセスし、"Create Database" と書かれた箇所をクリックします:
2016031701


作成するデータベースの名前を問われるので適当な名称(この例では mydb)を入力して "Create" ボタンをクリックします:
2016031702


指定した名称のデータベースが作成され、その内容が表示されます。この時点では作成直後なのでドキュメントは存在していないはずです。とりあえず元の画面に戻るために "Overview" と書かれた箇所をクリックします:
2016031703


もとの管理コンソールのデフォルト画面に戻ります。先程まではなかった mydb というデータベースが追加されていることがわかります。このデータベースにリモートからアクセスすることが今回の目的です:
2016031704


次にローカル側の準備です。まずなにはともあれ PouchDB ライブラリが必要なので、PouchDB のトップページから最新版の PouchDB をダウンロードします。この記事を書いている時点での最新バージョンは 5.3.0 で、ダウンロードファイル名は pouchdb-5.3.0.min.js でした:
2016031701


次にダウンロードした PouchDB ライブラリを使って、リモートの CouchDB サーバーのデータベースにアクセスするプログラムをローカルに作成します。具体的には以下の様な HTML ファイル(index.html)をダウンロードした pouchdb-5.3.0.min.js と同じディレクトリに作成します:
<html>
<head>
<script src="./pouchdb-5.3.0.min.js"></script>
<script>
var db = new PouchDB('http://XX.XX.XX.XX:5984/mydb');
db.info().then( function( info ){
  console.log( info );
});
</script>
</head>
<body>
</body>
</html>

ソース内の XX.XX.XX.XX は CouchDB サーバーのホスト名または IP アドレスです。また mydb はアクセス先のデータベース名称です。この HTML では同じディレクトリにある pouchdb-5.3.0.min.js を読み込み、http://XX.XX.XX.XX:5984/mydb にアクセスして、成功したらその情報をウェブコンソールに表示する、というものです。繰り返しになりますが、このソースファイルはローカル環境にあり、このようなファイル構成になっています。そして HTML ファイルの読み込みと同時にリモートの CouchDB サーバーにアクセスして mydb データベースの情報を表示する、という内容の処理が記述されています:
2016031706


ではこの index.html をダブルクリックしてデフォルトブラウザで開きます。注意点としてウェブサーバー上の HTML ファイルを開いているのではなく、ローカルディスク内の HTML ファイルを開いている(アドレスのプロトコル部分が file:/// で始まっていることに注目)ことを確認してください。そして F12 を押してウェブコンソールを表示すると、上記 HTML 内の JavaScript コードが実行されて XX.XX.XX.XX:5984 の mydb データベースに関する情報が出力されていることを確認してください。ローカル環境から JavaScript でリモート環境の CouchDB サーバーのデータベースの情報を取得することができました!
2016031707


もしもここで以下のようなメッセージが表示されてしまう場合は、クロスサイトスクリプティングの制約にかかって、JavaScript が実行できなくなっている可能性が高いです。その場合はこのページの上部にある「CouchDB の Access-Control-Allow-Origin ヘッダを設定する」のリンク先に書かれている設定が足りない(または間違っている)ので、この内容を確認した上で再度試してみてください:
2016031708


以前のブログエントリで CouchDB サーバーの導入手順(というかビルド手順)を紹介しました:
CentOS に CouchDB をインストールする


上記の方法で自由に使うことのできる CouchDB サーバー1ノードが手元の環境に出来上がります。が、PouchDB などの JavaScript ライブラリを使ってこのサーバー上の DB を操作しようとすると、まだ手順が足りないこともあります。

具体的には、素のままの設定状態で JavaScript を使って CouchDB の REST API を実行しようとすると、クロスサイトスクリプティングのセキュリティ制約にかかってしまい、実行時にエラーを起こしてしまうのです。

クロスサイトスクリプティング制約を回避するには、(CouchDB の)HTTP サーバーに Access-Control-Allow-Origin ヘッダを付与して、明示的に制約を回避できるアクセス元を指定して回避する必要があります。その方法を紹介します。


まずは上記サイトを参照して CouchDB サーバー環境を用意します。以下の手順では上記方法で導入した CouchDB サーバー環境を利用するものとします。

次に /usr/local/etc/couchdb/local.ini をテキストエディタで開き、以下の内容を加えます(この例では Access-Control-Allow-Origin に * を設定して、事実上アクセス元の制約なしにアクセスできるような設定にしています):
  :
  :
[httpd]
port = 5984
bind_address = 0.0.0.0
enable_cors = true

[cors]
credentials = true
origins = *

  :
  :

この状態で CouchDB を再起動すると、CouchDB の REST API はどのサーバーからでもアクセスできるようになります。
# /etc/init.d/couchdb restart


実際に PouchDB を使って外部からのアクセスが可能であることを示すコードは別のエントリで用意する予定です。

JavaScript(HTML5) の機能だけで、PC に付属したマイクから音声を入力(つまり PC の前で喋った音声データを取り込む)してみました。なお、この機能を使うには UserMedia に対応したブラウザが必要です。以下のコードは Windows 版の FireFox で動作確認をしています。

サンプルのコードはこんな感じになります:
<html>
<head>
<title>HTML5 Audio</title>
<script type="text/javascript"> 
//. ブラウザによる差異を吸収
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
 
//. バッファサイズ等
var audioContext = new AudioContext();
var bufferSize = 4096;
var cnt = 0;
 
//. 音声処理
function onAudioProcess( e ){
  //. 取得した音声データ
  var input = e.inputBuffer.getChannelData(0);

//. ↑この input に音声データが入っているので、これをストリーミングなどで処理すればよい。
//. 以下は実際にデータが入っていることを確認するためのサンプル処理
//. 音声データの最大・最小値を求める var mx = 0, mn = 0; for( var i = 0; i < bufferSize; i ++ ){ if( mx < input[i] ){ mx = input[i]; } if( mn > input[i] ){ mn = input[i]; } } //. 一度に取得した音声データの最大・最小値を求める(特に意味は無いが、データが取得できている確認) cnt ++; console.log( "[" + cnt + "] min = " + mn + ", max = " + mx ); } //. 音声処理開始 function initialize(){ navigator.getUserMedia( { audio: true }, function( stream ){ //. 音声処理 var javascriptnode = audioContext.createScriptProcessor( bufferSize, 1, 1 ); var mediastreamsource = audioContext.createMediaStreamSource( stream ); window.dotnsf_hack_for_mozzila = mediastreamsource; //. https://support.mozilla.org/en-US/questions/984179 mediastreamsource.connect( javascriptnode ); javascriptnode.onaudioprocess = onAudioProcess; javascriptnode.connect( audioContext.destination ); },function( e ){ console.log( e ); } ); } </script> </head> <body onload="initialize()"> </body> </html>

余談ですが、上記コードの赤字部分は今回一番迷った所。どうも FireFox のバグらしく、取得したメディアストリームがいつの間にか消えて(音声入力処理が止まって)しまう、という現象に遭遇しました。そのバグを回避するため、無理やり取得した値をグローバル変数化して消えないようにしています。バグの詳細についてはこちら:
https://support.mozilla.org/en-US/questions/984179


この HTML をマイク入力を ON にした状態で対象ブラウザで開くと、「PC のマイクをこのアプリと共有しますか?」という確認メッセージが表示されます。ここで「共有」を選択してください:
2016030301

共有すると画面に FireFox がマイクを利用中であること(知らないうちに音声を取得しているわけではないこと)を知らせるマークが表示されます。このマークが出ている間は PC のマイクで拾った音声をこのウェブページで共有していることを表しています:
2016030302


ここで F12 キーを押すとデバッグ画面が表示され、コンソールタブを見ると取得した音声データ1まとまり(4Kbyte)単位での最大値と最小値(簡単にいうと音の大きさ)を表示し続ける、というアプリになっています。
2016030303


実際には取得した音声データを保存(録音)して再生できるようにするとか、ストリーミングで処理してテキスト化するとか、・・・ という使い方になると思いますが、その入力を HTML で行う方法の紹介でした。

JavaScript の似て非なる2つの関数: setTimeout と setInterval。この2つの違いは分かりますか?

まず、どちらの関数も第一引数が実行する関数、第二引数が実行までの時間(ミリ秒)、という同じようなパラメータで使います:
<!-- setTimeout の場合 -->
<html>
<head>
<title>setTimeout</title>
<script>
var cnt = 0;
function func(){
  cnt ++;
  console.log( "cnt = " + cnt );
  setTimeout( "func()", 1000 );
}
</script>
</head>
<body load="func()">
</body>
</html>
<!-- setInterval の場合 -->
<html>
<head>
<title>setInterval</title>
<script>
var cnt = 0;
function func(){
  cnt ++;
  console.log( "cnt = " + cnt );
  setInterval( "func()", 1000 );
}
</script>
</head>
<body load="func()">
</body>
</html>

上記2つのページの違いは事実上一箇所です。上は <body> のロード時に setTimeout を、下は <body> のロード時に setInterval を実行しています(そして繰り返し呼ばれます)。この2つがどのような挙動の違いになるか、わかりますか?

説明しやすいのは上の setTimeout を使った場合です。こちらは最初のロード時にまず func() 関数が呼ばれ、 その中で setTimeout を使って1秒にもう一度 func() 関数が呼ばれ、・・・ という具合に、1秒おきに func() 関数が呼ばれるような処理になります:
2016202901


一方、ちょっとわかりにくいのは下の setInterval を使った場合です。こちらは最初のロード時にまず func() 関数が呼ばれ(ここまでは同じ)、その中で setInterval を使って1秒に func() 関数が呼ばれるよう指定されています。そして1秒後に func() 関数が呼ばれると、再度1秒毎に func() 関数を呼ぶよう指定されます。そのため更に1秒経過すると、最初に指定された1秒おきの func() と、2回目に指定された1秒おきの func() と、都合2回 func() 関数が(ほぼ同時に)呼び出されることになります。そして2つそれぞれの func() の中で更に1秒毎に func() 関数を呼ぶよう指定されます。ということは更に1秒経過すると、最初に指定された1秒おきの func() と、2回目に指定された1秒おきの func() と、3回目に指定された1秒おきの func() が2回、都合4回 func() 関数がほぼ同時に呼び出されることになります:
2016202902


この違い分かりますか?つまり setTimeout は次の1回の実行タイミングを指定しているのに対して、setInterval は何秒おきに実行するのかを指定している、ということになります。既に何秒おきに実行するのか設定されている状態で上書きしても、その前の指定がキャンセルされるわけではなく上書き(マルチスレッドのスレッドが1つ増える感じ)で実行タイミングがセットされるので、上記コードのようにループの中で何度も setInterval を実行すると、どんどん実行回数が増えていくことになる、という点に注意してください。

なお、setInterval を停止するには clearInterval 関数を実行します。


このページのトップヘ