JavaScript
長期にログを取る用途ではなく、開発過程でどのページがどのタイミングでアクセスされたかをチェックするための簡易サーバとクライアントサイドからの呼び出しコードです。
簡易ですがサーバ側でログを取るので、Webブラウザの環境に依存しにくい動作を期待できます。次のようなケースのとき向け。
- JavaScriptコンソールやWebブラウザの通信ログでは余計な情報が多すぎる
- モバイル端末での挙動確認なのでWebブラウザの開発者ツール類が使用できない
起動させたサーバに対して ?view=<測定対象URL>
のようにしてGETアクセスすると、 <測定対象URL>
のみを console.log() で出力します。後はそのまま眺めるなり、もう一工夫してファイルに記録するなり、用途に応じてカスタマイズ。
サーバ側
node.js 実装 + Docker 起動。停止時は適宜 docker stop などで。
index.js:
const express = require('express')
const server = express()
server.get('/', (req, res) => {
const url = new URL(req.url, `${req.protocol}://${req.hostname}`)
console.log('pageview: ' + url.searchParams.get('view'))
res.setHeader('Access-Control-Allow-Origin', '*')
res.statusCode = 200
res.end()
})
server.listen(8080)
起動:
$ docker run -it -v${PWD}:/mnt/app -p 8080:8080 node node /mnt/app/index.js
クライアント側(Webブラウザ側)
jQueryやaxios等が使えるのであれば、そちらの方が色々便利かと思います。今回はターゲットWebブラウザがChromeだけなので、XMLHttpRequest決め打ちでよいだろうと判断。 なお、例ではRuby on Railsへ組み込んでいるので turbolinks:load イベントからの起動になっています。
<script>
document.addEventListener('turbolinks:load', event => {
const url = event.data.url;
const req = new XMLHttpRequest()
req.open('GET', 'http://' + window.location.hostname + ':8080/?view=' + url);
req.send();
});
</script>
発端
gulp-gh-pages を使おうとしたら、以下のような謎のエラー。
node_modules/gulp-gh-pages/node_modules/gift/lib/commit.js:145
ref1 = /^.+? (.*) (\d+) .*$/.exec(line), m = ref1[0], actor = ref1[1], epoch = ref1[2];
^
TypeError: Cannot read property '0' of null
エラー箇所のコードを読むと、gitのcommitログ解析でおかしくなっているように見えるのだけど、なんでcommitログの正規表現処理なんてやっているのかが不明だにゃー、と。
原因
ググると以下のページが見つかったので、取り敢えず gulp-gh-pages-will ではなくて手動で何とかする方法をとってみた。
gulpの「gulp-gh-pages」がエラー履くようになったときの対応
記事内の解説、およびgiftのissueとかを読んでみると、giftの過去バージョンに起因する問題で、gift自体では問題解決済みの版が出ている。しかし、gulp-gh-pagesのpackage.jsonが古いままなので、gulp-gh-pagesとしては問題解決できていない、ということらしい。
どうも gulp-gh-pages 本家もGitHub上では次バージョンのタグが出ていて、最新のpackage.jsonではgiftも最新版使うようになっているので、近々根本的な解決があることを期待したいところ。
なお、gulp-gh-pagesをGitHub版で使えばいいよね、と思ってやってみたところ、今度は gulp-gh-pages が依存する gulp に未リリース・GitHub版を要求され、仕方ないので gulp もGitHub版にしてみたところ、新たな謎エラーが出てしまったというオチがつきましたとさ。
解決手順
"gift": "^0.10.2"
を package.json に追記npm install
実行後、rm -rf node_modules/gulp-gh-pages/node_modules/gift
として gulp-gh-pagesのインストール結果内giftを削除(こうしないと v0.10.2 のほうを使ってくれない)
JavaScriptベースで一通りつついてみたので、実装方法の備忘録。秘密鍵扱うので、クライアントサイドJavaScriptはNGですな。基本的にはプログラミングガイド読めば分かるものです。コーディングはお仕事しますよ? 的な。
必要なもの
- アクセスキーID:
- Amazonアソシエイト・セントラル →ツール →Product Advertising API で取得可能
- 秘密キー:
- Amazonアソシエイト・セントラル →ツール →Product Advertising API で取得可能(認証情報生成時のみ表示されるようなので注意)
- アソシエイトID:
- Amazonアソシエイト・セントラルの右肩に出てるアレです
実装上のポイント
一先ず、ASINで指定した特定商品の情報取得を試しました。
- エンドポイントは固定。シグネチャ生成時にhostとpathに分割することになります。
- クエリは文字コード順でソートすることになるので、& で連結せずに扱ったほうが良さ気でした。ソートはArray.sort()のアルゴリズムで問題ない筈。
- 各所の値は
encodeURIComponent()
でエンコード。 - TimestampはISO形式の日付+時間ですが、ミリ秒以下があるとエラーになりますので
(new Date()).toISOString()
は使用できません。 - シグネチャはbase64 エンコードの HMAC_SHA256です。Node.jsの場合
crypto.createHmac()
が使用可能です。
うっかりハマる点として、各所に出てくるサンプルでのエンドポイントがアメリカ版ですが、日本のAmazonでアカウント登録している場合は日本のエンドポイントを使う必要があります。
処理の流れ
- タイムスタンプを生成します。UTC~メソッド群を使用して、ミリ秒を含まないISO形式の日付を生成。getUTCMonth()が0~11を返すことと、1桁の値は右側に0が必要になることに注意
-
クエリを項目ごとに分割・格納したArrayを組み立てます →query
- AWSAccessKeyId
- アクセスキーID
- AssociateTag
- アソシエイトID
- Operation
- ItemLookup (今回は商品情報取得なので)
- ItemId
- ASIN
- Timestamp
- タイムスタンプ
- エンドポイントURLをバラします。Node.jsの場合
url.parse()
でパースして、hostとpathを使用します。 - クエリをソートし、&で連結しておきます。 →sorted_query
- 'GET', エンドポイントのhost, エンドポイントのpath, sorted_queryを\nで連結し、
crypto.createHmac()
で処理した後digest('base64')
でBASE64形式で取り出し、更にencodeURIComponent()
でエンコードします。 →signature - エンドポイント + ? + sorted_query(queryでもいい筈) + & + signature にアクセスします。後はレスポンスを適宜調理。
リクエスト制限は「1秒に1回」とされているのですが、試行しているときにコード修正で明らかに数秒以上経過してても制限にかかってエラーになったので、もう少し余裕持ったほうが良さそう。
ところでPAAPIってやっぱり「パァイ」って読めばいいんですかね。
data() で初期値を定めるとき、new Date()の値をyyyy-mm-ddにしてやれば万事解決。←結論
細かく解説すると。
- Vue.jsではコンポーネントの data() で初期値セットを返す
- <input type="date" > はvalueとしてISO形式(yyyy-mm-dd)を与えないと初期状態として日付を表示してくれない(値があっても "yyyy/mm/dd" というユーザに大変お優しい表示となってしまう)
- <input type="date" > に new Date() の値をそのままセットしても上記理由によりまともに表示してくれない
解決パターン。
Google Shortener APIで短縮URLを生成する処理をちょっと試してみたのでメモ的に残しておく。
- APIキーは各自Googleアカウントで発行すること
- fieldsクエリは全部乗せになってます
- 正常に実行できると、アラートダイアログで結果が表示されます
- エラー処理? なにそれおいしい?
$(function() { var apikey = <各自APIキーを入力>; var url = 'https://twitter.com/mami_tuchino'; /* これが短縮したいURL */ $.post({ url: 'https://www.googleapis.com/urlshortener/v1/url?fields=analytics%2Ccreated%2Cid%2Ckind%2ClongUrl%2Cstatus&key=' + apikey, data: JSON.stringify({ longUrl: url }), dataType: 'JSON', contentType: 'application/json', success: function(json) { alert(json.id); }, error: function(error) { console.logt(error); } }); });
…これ、結局「Ical」と「Webcal」って何がどう違うんでしょうね…。何度調べてもよく分からん。ともあれ、扱うデータは「*.ics」なので「ICS形式」と呼んでおけば間違いなかろう、と。
某所で提案しようと下調べしていたのだけど、概ねめどが経ったところで募集状況を見に行くと既に終わっていたいつものパターンでしたとさ。まあフルタイム職が終盤に差し掛かってちょいと忙しかったしね…。というところで調べた成果の記録と供養など。
実行環境と使用ライブラリ
実行環境としてはNW.jsを使用して、Node.jsで実装しています。NW.jsはJavaScriptとHTMLとCSSでお手軽に書けるうえ、JavaScriptなのでWebアクセス系は楽々扱え、しかも実行バイナリはWindows、Mac OS X、Linuxと幅広く対応できるのがとても良いと思います。
コンパイラ系言語を使わなくて良いというより、クロスプラットフォームが簡単に実現できる、モノによってはWebアプリケーション化まで視野に入れることができるという点が気に入っています。
でも最近Go言語も気になっているのだけど、あっちとの比較はどんなもんだろう…?
もとい。
使用ライブラリは icalパッケージ。加えて、画面制御系としてjQueryも使います。
実装
icalパッケージでWeb上のICS形式読み取りとオブジェクト化は容易に実現できます。
問題はICSから読み取ったイベント情報が「繰り返し」だった場合。
例えばGoogleカレンダーで「2016年1月1日から12月30日まで、毎週金曜日に「今日は金曜日!」とセットした場合、ICSデータには「毎週金曜日に「今日は金曜日!」と追加されるのではなく、開始が1月1日、終了が12月30日、かつ「週ごとの繰り返しで金曜日のみ有効」というルールが付与されたイベントが1件だけ追加されます。
このようなイベントをicalパッケージで処理した場合、rruleプロパティ内に繰り返し情報が格納されます。さらに rrule.all() とすると具体的な日付に展開したArrayが返ってきますので、これを使用することで他のイベントと同じように日付として扱うことが可能になります。
(以下のCALENDAR_URLで指定しているカレンダーは、非公開(ICSのURLを知っている人のみ参照可)として実際に置いてあります。
var ICal = require( 'ical' );
var CALENDAR_URL =
'https://calendar.google.com/calendar/ical/u78uo80t8b784ojbbf265tpkv4%40group.calendar.google.com/private-8d07e17f65ddac761f8cf190f3e69fee/basic.ics';
ICal.fromURL( CALENDAR_URL, {}, function( error, data ) {
if( error ) {
console.log( error );
}
else {
var keys = Object.keys( data ).sort();
keys.forEach( function( key ) {
var friday = data[key];
if( friday.rrule ) {
friday.rrule.all().forEach( function( realDay ) {
console.log( [
friday.summary, realDay.toLocaleDateString()
].join( ' ' ) );
} );
}
} );
}
} );
なお、rruleがあるデータの場合なぜかstart, endにはtoLocaleDateString()等が使用可能なデータが入っていませんので注意。
最近クラウドワークスでのお仕事でいくつか Google Apps Script を使用したのだけど、なかなか使える良い子であると同時に癖もある奴なのでいつも使うパターンをメモするなど。
スクリプトの実行時間は1回あたり6分
仕様的には Quotas for Google Services を参照。
細かい処理をちょいちょいとこなす程度であれば気にする必要はないのだけど、大量のファイルとかを扱い始めるとハマる。というかハマッた。
対策としては「6分以内に終わらせる」もしくは「6分超えそうだったら一度中断し、改めて再開させる(中断・再開が可能なつくりにする)」の2パターン。ただこの制限値に引っかかるような処理をさせると結構動きがトロいのも分かるので、第3のパターンとして「Googleの外でやる(普通のサーバー上でGoogle APIを呼び出す)」を考えに入れておくべきなのかと思った。
処理を一度中断し、自動で再開させるのは Properties + Trigger 、ところにより JSON
いくつかググって、最終的には Google Apps Scriptで5分の壁(タイムアウト)を突破するを参考にしつつ自分なりのパターンを作ることにしました。
スクリプトの処理開始時刻を保存しておき、開始後何分経ったかを随時チェック
開始後6分を超過する前に十分な余裕をもって中断処理に入る
スクリプトの処理に必要なデータは Properties Service に格納する
ただし Properties はString value しか扱えないので、構造化データなどを格納したい場合は JSONフォーマットに変換して格納する
- Propertiesへ格納する際は JSON.stringify()
- Propertiesから復帰する際はJSON.parse()
なお当初Base64を使おうとしていたのは秘密だ(待て)
スクリプトを終了後自動的に再開させるのは Script Service の ClockTriggerBuilder クラス。
前述のBlogでも言及されていたし実際うまくいかなかったのだけど、Triggerで「after(durationMilliseconds)」があるくせにセットしてもちゃんと発動しない。…というか発動するのかもしれないけど「(plus or minus 15 minutes)」などとのんびりしたことを言われてしまっては使うわけにはいかんがな。せめて単位をsecondsにしてくれ…。
everyMinutes(n) で毎分起動→起動後に多重起動防止のためトリガーを削除 のパターンでいきませぅ。
ただ、ここはむしろトリガー追加+削除を繰り返すより、別途キューを用意しておき、毎分キュー確認→なにかあれば実行、何もなければ休眠、のほうがスマートな予感。
参考資料など。まー私が使ってるのはオライリーが販売しているPDF版ですがねハハハ(これはこれでよりによって「String」の項にしおりが設定されていないという痛恨のバグがある)。