プログラム

Google docsのスプレッドシートに特定フィードの更新内容を追記する

業務でふと必要になって時々似た様なGoogle App Script書いているのですが、毎回書き始めるときに「えーっと、どうだったかな」となることが2回続いたのでエントリとして追加。

以下の様な形で書いてみた。そんなに大したことはやっていないのだけれども少し調整したところは、

  • フィード内のアイテムのプロパティで示されるリンク先URLの重複をシートに記録した中に一致したものが無いかの確認をする様にした
    • やり方としてはスプレッドシートの情報をまるっと取得して素朴にループさせてという愚直な方法にした(プレッドシート呼び出しのAPIを繰り返して叩かない)
  • リンク先のURLのバリデーションを入れる
    • RSSフィードが発行されていないページを外部のサービスを使ってRSS化したときに、更新対象以外の要素がフィードに含まれていたとき*1にそれを除外するため

ぐらい。
あとはSlackに通知させる必要があれば、sheet.appendRow([title, url, description]); と書いている辺りで併せて通知させれば良いかと思う。

下のスクリプトを追加して、定期的に呼び出す設定すればRSSが更新される度にシートの最終行に追記されていく様になる。

function parseNewsFeed() {
var sheet = SpreadsheetApp.getActiveSheet();
var sheetData = sheet.getDataRange().getValues();
var feedURL = "(走査対象のフィードのURL)";
var response = UrlFetchApp.fetch(feedURL);
var xml = XmlService.parse(response.getContentText());
var items = xml.getRootElement().getChildren('channel')[0].getChildren('item');
for(var i = 0; i < items.length; i++) {
var title = items[i].getChild("title").getText();
var url = items[i].getChild("link").getText();
var description = items[i].getChild("description").getText().replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'').replace(/(\s+)/g, '');
var urlValidate = /(フィード内のURLのバリデーション用正規表現を書く)/;
if (urlValidate.test(url)) {
if (!isExist(sheetData, 1, url)) {
sheet.appendRow([title, url, description]);
}
}
}
}
function isExist(sheetData, targetCol, value) {
for(var i=0; i<sheetData.length; i++) {
if (sheetData[i][targetCol] == value) {
return true;
}
}
return false;
}

*1:ページサイドにあるカテゴリなどの要素がフィードのアイテムとして更新されてしまうことがあるため

sepiaを利用したテスト中で一時的に機能を無効化する

Node.js向けのwebmockの作成を楽にしてくれるライブラリにsepiaがあります。RubyVCRみたく一度テストで利用する通信内容を記録してモック化され、2回目以降の通信はそのモックを介して行われる様になるというものです。

github.com

sepiaを基本的に使う様にしているのですが、一部分でNockを利用したテストを含んでいた場合に、Nockで書かれたテスト内のみsepiaの動きを止めたいと考えたときにどうすれば良いのかと調べたのでメモとして残しておきます。

得てしてこういうときは世の中に3000人くらい同じことをやろうとする人がいて、3人くらいが実際に手を動かし内1人くらいがどこかに記録を残してくれているに違いないという思想を持っているのでsepiaのリポジトリのissueを眺めているとまさにという内容があったのでそれに倣いました。

github.com

上と同内容のファイルをプロジェクトに置いて、

var foo = require('../../../lib/plugin/foo.js'),
sepia = require('../../util/sepia'),
path = require('path');
describe('......, function(){
it('should be ..... ', function(done){
sepia.fixtureDir(path.join(process.cwd(), 'test', 'sepia-fixtures'));
sepia.enable();
var args = {
token:'xxxxxx'
};
foo.output(args, "123", function(err, output){
err.should.be.false;
sepia.disable();
done();
});
});
});

といった具合にしてみたのだが、これがスマートなやり方なのかいささか不安な点もある。

LINE Notifyで天気を通知させる

LINE Notifyが公開されて少し触ってみたけれどとてもシンプルにLINEで通知を実装することができたので完全に個人用便利ツールとなりつつevacで天気通知させられる様にoutputとして実装してみた。

実装といってもとてもシンプルでAPIにLINE Notify上で取得されたトークンをPOSTするだけなのだけど。

たとえば東京地方の天気の情報をlivedoorRSSフィードから抜いて天気の情報のみをLINE通知させるには

in:
rss:
url: 'http://weather.livedoor.com/forecast/rss/area/130010.xml'
format: '__description__'
filter:
match:
regexp: '[0-9].*の天気は.*'
out:
line:
token: '(取得したLINE Notifyのトークン)'

みたいなのを書いてevacに渡すと

f:id:hideack:20161005095923p:plain

の様に通知させることができる。一日一回どこかで実行させれば週間天気予報を必ず流せるので自分にはとても便利*1

業務だとSlackに通知系を流すことが多いのですが、LINE Notifyだと例えば家庭内で共有したい情報とかを流せたりして便利そうな気がする。

*1:最近、これを連発していて本来は他の人にも便利であるのがベストとは思うのですが、何かしら小さく手を動かしたいと思うときなにより一番に自分が便利なものを作るというのがよいのかなと最近思ふ

nroongaを使ってデータストアして全文検索するまで

昨日の続き。

繰り返しになるが、解決したい課題と現状は、

  • 数ギガサイズのアクセスログがある
  • 上記に対して数万パターンの文字列それぞれが何回出現するかを数え上げたい

という状況。なので、このログファイルをngroongaを介してgroongaのデータベースを新規作成し、そこに格納してみる。

もちろん事前にGroongaのインストールが必要なのでインストールする。Macであればbrewでインストールすることができる。

$ brew install groonga

で、次に、

  • Groonga DB作成
  • DBへデータを格納するためのテーブル作成
    • 今回はシンプルに1テーブルにbodyという1カラムを作ってそこにアクセスログ1行分を格納する
  • 上記のテーブル中のカラムを全文検索するための語彙表(インデックス)用のテーブル作成
  • テキストファイルベースのログファイルを格納

という上記の一通りの作業は以下のコードで実現できる。

var nroonga = require('nroonga'),
byline = require('byline'),
fs = require('fs');
var db = new nroonga.Database('db/accesslog');
db.commandSync('table_create Logs TABLE_NO_KEY');
db.commandSync('column_create Logs body --type Text');
db.commandSync('table_create', {'name':'LogIndexes', 'flags':'TABLE_PAT_KEY',  'key_type':'ShortText', 'default_tokenizer':'TokenBigram', 'normalizer':'NormalizerAuto'});
db.commandSync('column_create', {'table':'LogIndexes', 'name':'body_index', 'flags':'COLUMN_INDEX|WITH_POSITION', 'type':'Logs',  'source':'body'});
var stream = fs.createReadStream('access.log', { encoding: 'utf8' });
stream = byline.createStream(stream);
stream.on('data', function(line) {
var log = JSON.stringify([
{'body':line}
]);
db.commandSync("load", {'table':'Logs', 'values': log});
});
stream.on('finish', function(){
console.log("Finish parse log file.");
db.close();
});

上記の格納が完了した後*1、実際に格納した情報にたいして全文検索するには以下の様に select のクエリを発行する。limit:0 を指定することでGroongaでは件数のみを取得できるので、例えば今回格納したログファイル中に「Windows」という文字列が何回出現したかということを得たい場合は以下の様な形になる。

var nroonga = require('nroonga');
var db = new nroonga.Database('db/accesslog');
var res = db.commandSync('select', {'table': 'Logs', 'match_columns':'body', 'query': 'Windows', 'limit':0});
console.log(res);
//=> [ [ [ 1869923 ], [ [Object], [Object] ] ] ] 

かなり気楽に全文検索できる状態を作れる。最初スクリプトgrepツールで解決しようと思ったが少し時間がかかりすぎなところがあったので対象を絞ることを考えていたりしたが自分が解決したかったことは解決できそう。

例えばサービスの方針等々を検討する際の数値集めが簡単に行える様にするためにログ基盤を整える等が最も正しい解決策だとは思うのだが、ひとまず何かしらログが残されていれば手元にこういった環境を作って調べてみるのも一つのやり方かなと思う。

参照

*1:ちなみにGroongaのDBとして作られたファイルのサイズは投入したアクセスログのサイズが1.8Gbyteに対して、4.3Gbyteになった。

google-search-rankというnpmを作った

以前紹介した google-search_rank | RubyGems.org | your community gem host のインスパイア。

再発明感あるけどGoogleのCustom search APIを操作するgoogle-searchというクライアントを使って任意の条件で検索をかけて検索順位を取っている。

該当APIで取得した検索結果の情報と検索順位がコールバックされる。

引数で検索ワード, 対象ドメイン, 最大検索結果何ページ目までを走査するかを指定する。

var searchrank = require('google-search-rank');
searchrank.key = '(API Key)';
searchrank.cx  = '(Custom search engine ID)';
searchrank.find("安住紳一郎の日曜天国", "hideack.hatenablog.com", 10, function(rank, result){
console.log("RANK => " + rank);
console.log(result);
});

とすると

RANK => 70
{ kind: 'customsearch#result',
title: '「安住紳一郎の日曜天国」というラジオ番組を毎週聞いている話 ...',
htmlTitle: '「<b>安住紳一郎の日曜天国</b>」というラジオ番組を毎週聞いている話 ...',
link: 'http://hideack.hatenablog.com/entry/2016/04/20/225328',
displayLink: 'hideack.hatenablog.com',
snippet: '2016年4月20日 ... 安住紳一郎の日曜天国 (通称 "にちてん")という番組があって自分は大好きなので毎週\n日曜日の朝10時から聞いているのですが、twitterで まもなくTBSラジオで安住紳一郎の\n日曜天国のお時間。今日はインスタントラーメン研究家 大山即席斎 ...',
htmlSnippet: '2016年4月20日 <b>...</b> <b>安住紳一郎の日曜天国</b> (通称 &quot;にちてん&quot;)という番組があって自分は大好きなので毎週<br>\n日曜日の朝10時から聞いているのですが、twitter��� まもなくTBSラジオで<b>安住紳一郎の</b><br>\n<b>日曜天国</b>のお時間。今日はインスタントラーメン研究家 大山即席斎&nbsp;...',
cacheId: 'Nckg46Amc5IJ',
formattedUrl: 'hideack.hatenablog.com/entry/2016/04/20/225328',
htmlFormattedUrl: 'hideack.hatenablog.com/entry/2016/04/20/225328',
(snip)

といった具合。あぁ、前回調べた時より大分順位下がったな。などといったことが知ることができて便利。

www.npmjs.com

FIFOの作り方を考える

FIFO(First In, First Out) を利用したときにソフトウェアだとArrayにメソッドとして実装されていたり、またKVSに実装されていたりして、あまり中身意識をすることが無いかもしれないけど、定期的に振り返って実装を考えると頭の体操になって楽しい。といってもそんなに難しいものじゃない。

雑に書いてしまえば以下の様な形になる。*1

FIFOの深さを256(8bit)、Push/Popされる値の大きさを32bitで考えることにしてみる。型をきっちり指定したいのでGoで。

package main
import "fmt"
var pushAddr, popAddr uint8
var stack[256] int
func push(value int) {
stack[pushAddr] = value
pushAddr++
}
func pop() (popValue int){
if popAddr >= pushAddr {
panic("NO stack values.")
}
popValue = stack[popAddr]
popAddr++
return
}
func main() {
push(1)
push(2)
push(3)
fmt.Println(pop())
fmt.Println(pop())
}

上を実行してみると

$ go run fifo.go
1
2

最初にPushしたものが最初にPopされている。非常にシンプル。FIFOのどこのアドレスまでPushされているか管理する変数と、逆にどこまでPopされているかを管理する変数があればよいだけである。

こうすることでアドレスの加算だけで表現できる。上の例だと pushAddr は符号なし8bitの変数なので255までアドレスの値が到達すると更にpushされたときは0に戻るのでその点も気にすることがない。いくらか雑に書いているのでFIFOがあふれる処理とかを書いていないので厳密ではないがおおよそこういった中身になっているということを知って使うだけでも少し考え方が変わったりするかもしれない。

最近できるだけ原理的なところも知ろうとする様にしている。とはいっても、端折ってしまうことも正直多いのだけど概略だけでも抑えておく意味合いでも。

*1:一つの実装の考え方なので他の実装方法もあると思う

npmのtunnel-sshを利用してSSHポートフォワーディングする

SSHでポートフォワードするのをNode.jsだけで行うことを試してみたのでその際のメモ。

下の例では、local(127.0.0.1)の13306番にアクセスするとSSH接続先のホストから接続できる mysql***.heteml.jp の3306番にフォワードしている。

npmで公開されている tunnel-ssh を利用するとシンプルにできる。

$ npm install tunnel-ssh --save

SSH接続するための設定とポートフォワードする先の設定、ローカルで接続待ちをするポート番号を指定する。

// tunnel.js
var tunnel = require('tunnel-ssh');
var config = {
username: 'ssh-hideack',
port: 2222,
host: 'ssh***.heteml.jp',
privateKey:require('fs').readFileSync('/Users/hideack/.ssh/id_rsa'),
passphrase: '******',
dstPort: 3306,
dstHost: 'mysql***.heteml.jp',
localHost: 'localhost',
localPort: 13306
};
var server = tunnel(config, function (error, result) {
if (error) {
console.log(error);
process.exit();
}
console.log('connected');
// これ以降にMySQLクライアント等でlocalhostの13306番に接続する処理を書けばよい
});

というメモエントリでした。

greenkeeper.io を利用してプロジェクトが依存するnpmを最新の状態で保つ

プロジェクトで依存しているnpmを最新に保つのにgreenkeeperというサービスを見つけたので試してみました。

まずはcliツールをインストール。このサービスのカッコイイのがOAuthの部分を除いてコマンドラインで完了するというところ。

$ npm install -g greenkeeper

まずはgreenkeeperにログインします。コマンドラインでログインを指定。

$ greenkeeper login

そうするとGithubOAuth認証用にブラウザが立ち上がって認証を求められます。

f:id:hideack:20151002210341p:plain

その手続が済むとgreenkeeperを利用可能な状態になっているので、greenkeeperでpackage.jsonで管理されているnpmパッケージを最新に保ちたいプロジェクトのルートでgreenkeeperを有効にします。

☁  remp-api [master] greenkeeper enable
🌴  info enable The GitHub slug is: remp-team/remp-api

そうすると該当リポジトリ内にある package.json の監視が始まり、最新のnpmパッケージがあれば自動的にプルリクエストが作成されます。

f:id:hideack:20151002210518p:plain

github側で連携していれば当たり前ですが自動的にSlackに流れてきて, CI走って便利感ある。

f:id:hideack:20151002210650p:plain

例によってプライベートリポジトリだと課金が必要なのですがパブリックなリポジトリであれば無料で利用できる様なのでぜひお試しください。


過去作成したloopbackアプリのloopback-component-explorerへの対応

過去に作成したloopbackアプリの場合、slcコマンドでscafoldすると loopback-explorer と呼ばれるloopbackで作成したAPICRUDするUIが含まれた形でアプリが作成されますが、今回この loopback-explorer に大きな変更が加えられて単純に npm update するとアプリの起動が行えなくなるのでその際の対応方法のメモです。

loopback(2.22.0)以降だとslcコマンドでscafoldされた際に新しいAPI explorerに対応しているので問題はないので、既にアプリを作っている人向けの内容です。

package.jsonの修正

これまで利用されていた loopback-explorer を削除。新しく同じ役割をするコンポーネントとして追加された loopback-component-explorer を追加します。

$ npm install loopback-component-explorer --save
$ npm uninstall loopback-explorer --save

修正後のpackage.jsonは以下の様な形になります。

  "dependencies": {
(snip)
"loopback-component-passport": "^1.5.0",
+   "loopback-component-explorer": "^2.1.0",
"loopback-connector-mongodb": "^1.13.0",
(snip)
},
- "optionalDependencies": {
-   "loopback-explorer": "^2.0.1"
- },

component-config.json を追加

loopback-component-explorer をlooopbackアプリにマウントさせてやるために server/component-config.json を新規に追加します。

explorer呼び出すパスをJSON内で記述します。

{
"loopback-component-explorer": {
"mountPath": "/explorer"
}
}

以上の操作で http://localhost:5000/explorer/ へアクセスすることで従来通りのAPI Explorerを利用することができます。

参照


全く蛇足ですが、StrongloopのロゴにIBMのクレジットが入っていた。

slack-winstonでログをSlack通知する

Node.js でloopbackを使ってAPIを書くことをここ最近ずっとしていて、winstonでアプリケーションのログを取る様にする際にした際、ついでに一定レベル以上のログ(エラーログ)の場合は内容をSlackにも通知できないかなと調べると由ななwinstonのプラグインを見つけたので使ってみた。

github.com

お約束。npmコマンドでインストール。

$ npm install slack-winston --save

以下の様な形で利用できます。

var winston = require('winston');
var slackWinston = require('slack-winston').Slack;
var options = {
domain: 'remp',
token: '******',
channel: '#remp',
username: 'REMP API winston',
message: '[{{level}}] {{message}}',
level: 'warn'
}
winston.add(slackWinston, options)

なお、token の部分には、Slackの"Incoming WebHooks"の設定で得られるWebhook URLの末尾の文字列を渡します。

あとは、winstonでログ出力をする要領で記述を行うと

winston.warn("Winston test");

winstonのログは出力されつつ...

f:id:hideack:20150816114012p:plain

Slackの指定したチャンネルにも通知されます。

f:id:hideack:20150816114005p:plain

先の例に書いた通り、メッセージ内容や通知レベルも個別で指定できるので便利。