2014年12月17日水曜日

preタグテスト

Techチームの遠藤です。

コンテンツのグローバル展開に向けて、いくつかの地点での画面表示の速度測定のためにphantomjsを使用し計測しました。
全部ではないですが初めの部分だけ記述します。

PhantomJSはまぁ仮想ブラウザを立ち上げてHPを操作していくものですね。
自動テストなどに使えるものです。

PhantomJS
http://phantomjs.org/

DownloadでそれぞれのOSにあったものをDLしてください

私はwindowsでやってるのでwindowsのみ記述します。
といっても大したことしてません。

windowsの場合はDLしてきたzipファイルを解凍し、任意の場所に配置してから環境変数にそのフォルダを指定すると使えるようになります。

さっそく使ってみます。

jsファイルを書いて、コマンドプロンプトで
phantomjs XXX.js

phantomjsコマンドと実行するファイルを指定するだけでできます。

まず手始めにgoogleへアクセスしてみます。

var page = require('webpage').create();
page.open('http://www.google.co.jp/', function () {
        page.render('sample.png');
        phantom.exit();
});

新しく作ったJSファイルに上記を記述して実行。

アクセスできました。
sample.pngというピクチャが保存されています。

↓こんな感じ



実際に使うにはどんどんページ遷移していきたいので、、、
下記ページを参考に遷移する先をtask化していきます。

var page = require('webpage').create();

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      var imagename = 'sample2_' + ('0' + task.id).slice(-2);
      page.render(imagename + '.png');

      next();
    }
  });
}

var tasklist = [
{
  id:1,
  path:'http://www.yahoo.co.jp/'
},{
  id:2,
  path:'http://www.google.co.jp/'
}];

next();
こんな感じに...


実行してみるとちゃんとアクセスされているようです。
sample2_01.png、sample2_02.pngと画像が保存されているので順にアクセスされてる模様です。

さて、意味もなくやってるわけじゃなくて仕事で使うので自社サイトのアクセスをするわけですが。。。
自社サイトでは認証があるのでそれを実装します。

var page = require('webpage').create();

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      var imagename = 'sample3_' + ('0' + task.id).slice(-2);
      page.render(imagename + '.png');
      if(task.operation == null){
        next();
      } else {
        task.operation(page);
      }
    }
  });
}

var tasklist = [
{
  id:1,
  path:'http://www.ub-speeda.com/',
  operation:
    function(page){
      page.evaluate(function() {
        //ここは各サイトで実装が異なります
        document.forms[0].username.value = 'dummy_username';
        document.forms[0].password.value = 'dummy_password';
        document.forms[0].submit();
      });

      //弊社サイトではログイン数管理をしており、それを超過した場合、古いログインを追い出すなどがあるため確認メッセージを出し、再度ログインボタンを押してもらう仕様のため、その対応です
      setTimeout(function() {
        if (page.url.indexOf('top/welcome') > 0) {
          page.evaluate(function() {
            document.forms[0].submit();
          });
          setTimeout(function() {
            next();
          }, 3000);
        } else {
          next();
        }
     }, 3000);
    }
},{
  id:2,
  path:'http://www.ub-speeda.com/company/companyinformation/cid/JPN3O1U73D3VNGWO'
}];

next();

operationという変数を作ってそこにログイン処理を放り込んでみました。
operationが実装されていれば実行する感じですね。
これを応用すればHP内の操作を追加していけるかと思います。
こちらは実際動作するのですがID/Pass等が必要なため試していただける方は少ないかと思います。(ゴメンナサイ

次にファイルのアクセスに関しての情報をファイルに記述します。
ここではページのロード時間しか記述しませんが、ちょこっと検索すればより詳細な情報を取得できるサンプルコードがあるところがあるので詳細はそちらを参考にしてください。

まず、ファイルに書きだす方法ですが公式を参考に。。。

var fs   = require('fs');
fs.write(ファイル名, 内容, 'w');

で書き出せます。

組み込んでみましょう。

var page = require('webpage').create();
var fs   = require('fs');

page.onLoadStarted = function() {
  page.startTime = new Date();
};

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      page.endTime = new Date();

      var logname = 'sample4_' + ('0' + task.id).slice(-2);
      store(logname, page.endTime, page.startTime);

      if(task.operation == null){
        next();
      } else {
        task.operation(page);
      }
    }
  });
}

function store(logname, endTime, startTime){
  page.render(logname + '.png');
  fs.write(logname + '.log', endTime - startTime , 'w');
}

var tasklist = [
{
  id:1,
  path:'http://www.ub-speeda.com/',
  operation:
    function(page){
      page.evaluate(function() {
        document.forms[0].username.value = 'dummy_username';
        document.forms[0].password.value = 'dummy_password';
        document.forms[0].submit();
      });

      setTimeout(function() {
        if (page.url.indexOf('top/welcome') > 0) {
          page.evaluate(function() {
            document.forms[0].submit();
          });
          setTimeout(function() {
            next();
          }, 3000);
        } else {
          next();
        }
     }, 3000);
    }
},{
  id:2,
  path:'http://www.ub-speeda.com/company/companyinformation/cid/JPN3O1U73D3VNGWO'
}];

next();

さて、これで
sample4_01.log
sample4_02.log
というファイルが出来ました。

中身は「endTime - startTime 」という内容が示すとおり殺伐とした数字だけです。
sample4_01.logの中身
---
219
---

sample4_02.logの中身
---
2448
---

これでページを開く時間がわかるようになりました。
実際にはこれをより詳細にHAR形式で出力するなどして、使えるものに仕上げていってます。
他にも、ダウンロードやajaxでの表示待ちなどを組み込みながら作ったりします。

こんな感じで日々仕事してます。。。

2014年12月15日月曜日


こんにちは。NewsPicks の開発を担当している文字(もんじ)です。本日 NewsPicks の Chrome 拡張をリリースしました。


幸いユーザーの皆様にもご好評頂いているようで嬉しいです。ということで今回は NewsPicks の Chrome 拡張を作った話をします。アジェンダは以下の通りです。
  1. NewsPicks の Chrome 拡張が提供する機能について
  2. なぜ Chrome 拡張を作ったのか?
  3. どうやって Chrome 拡張を作ったのか?
  4. まとめ

1. NewsPicks の Chrome 拡張が提供する機能について

Chrome 拡張が提供する機能については NewsPicks のブログをご覧下さい。主に 2 つの機能を提供しています。

現在開いているページを NewsPicks に Pick する機能




アドレスバー(Omnibox)を利用して NewsPicks 内の記事やコメントを検索する機能





2. なぜ Chrome 拡張を作ったのか?

NewsPicks は基本的にスマホファーストな方針で開発しており、Web 版は今年の夏にリリースされたばかりです。しかしヘビーユーザー ── 特に NewsPicks がターゲットとしているビジネスマン ── は、スマホ以外から NewsPicks を利用することも多いと考えられます。実際に 25 % のユーザーが Chrome から NewsPicks にアクセスしていますし、そもそも会社でスマホを使ってニュースを見ていたら印象が悪いでしょう。

また、過去記事を参照しながら長文のコメントを書いて下さるユーザーは、スマホではなく PC から書きたいと考えている方が多いとも感じていました。私自身も NewsPicks でコメントを書く場合は、スマホではなく PC を利用することが多いため、業務の隙間時間(ビルド時間)や休日の隙間時間に業務外で Chrome 拡張を開発することにしました。


3. どうやって Chrome 拡張を作ったのか?

さて本題の開発方法ですが、それほど特別なことはしていません。Chrome 拡張は必要なツールキットも揃っており、JavaScript と HTML で書けるため、実開発工数は 10-20 時間程度だと思います(気楽に作ることが出来るのが HTML + JavaScript の良いところですね)。ただ、私自身は Chrome 拡張を作ったことが無かったため、以下の順に調査をしました。
  1. Google のドキュメントを読む
  2. 今回開発しようとしている拡張機能に似た Extension のソースコードを読む
1. については、Google の API ドキュメントは良く整備されており、必要最低限の情報は十分に記載されていると感じました。ただ幾つか実際の使用例を見たかったので、2. のソースコードで知識を補完しました。ここでは具体的には以下を参考にしました。
前者は主に Pick 機能(ポップアップ)の実装について、後者はポップアップを常駐型のサイドバーにしようとしたときに読み込みました(Omnibox を操作する拡張のソースコードも幾つか読んだのですが、失念していまいました)。

ちなみにポップアップではなく常駐型のサイドバーにするのは途中で断念しました。理由は幾つかあるのですが、サイドバーを実装しようとすると content script もしくは executeScript と insertCSS によって表示しているページにスクリプトと CSS をインジェクトする必要があるのですが、後者が表示しているページの CSS とコンフリクトするためです。今回は手間を省くために CSS Framework を利用したかったのと、すべてのウェブページに対応しようと考えていたため、コンフリクトを回避するのはコストが高くつきそうだなぁと判断して取りやめた次第です。また、その他の理由としては、後述する Yeoman の generator との相性が悪かったというのもあります。JS ファイル内で依存スクリプト / CSS をインジェクトするため、grunt-usemin を使った Yeoman のビルドフローに乗せるのが面倒でした。

さて、幾つか下準備をしたあと、具体的な開発に入りました。最初は自分でちまちまビルドスクリプトを書こうと思っていたのですが、丁度 Yeoman の generator があることに気付いたので、こちらを使うことにしました。この generator を使うと extension のパッケージングまで含めたビルドフロー全般、また livereload を使ったデバッグ環境まで一式整えてくれるため、Chrome 拡張の開発に慣れていない人にとってはなかなか便利だと思います。ディレクトリ一式も良い感じに作ってくれるので、今回の拡張ではこの generator を使ってこんな感じの構成にしています。


具体的なソースコードについては特に複雑なことはしておらず、NewsPicks のサーバーが提供する REST API を叩いているだけです。あえて工夫した点を挙げると以下になるでしょうか。

まず popup については MVC で開発しました。今回はそれほど複雑な画面ではないので Backbone を利用していますが、フォームのモデルとのバインディングだけ面倒だったので backbone.stickit を利用しています。

backbone.stickit は Backbone にバインディング機能を提供してくれるライブラリです。これを Marionette と組み合わせて使う場合は、次のような Behavior を定義しておくと便利です。

class StickitBindingBehavior extends Backbone.Marionette.Behavior

  createBindings: ($root, attr="name", ignores={}) ->
    bindings = Backbone.$.extend true, {}, @options.bindings
    $root.find("[#{attr}]").each ->
      $el = $(@)
      attribute = $el.attr attr
      return if bindings[attribute]
      for ignore in ignores
        if _.isString ignore
          return if ignore is attribute
        else if _.isObject ignore
          return if ignore.test and ignore.test attribute
      selector = "[#{attr}='#{attribute}']"
      tag = $el.prop("tagName").toLowerCase()
      key = "#{tag}#{selector}"
      return if bindings[key]
      bindings[key] = "observe": attribute
    bindings

  onRender: ->
    @view.bindings = @createBindings @$el, @options.attr, @options.ignores
    @view.stickit()

  onDestroy: ->
    @view.unstickit()

Backbone.Marionette.Behaviors.behaviorsLookup = ->
  stickit: StickitBindingBehavior

これを使うと基本的には何も書かなくてもそれっぽくバインドしてくれるようになります。

class FormView extends Backbone.Marionette.ItemView

  template: "#form"
  behaviors:
    stickit: {}

ちなみに Backbone.Marionette + backbone.stickit + Browserify を使ったテンプレートを以前自分で作って github に公開しているので、良ければこちらをご参照下さい。
backbone.marionette.example

Omnibox については Chrome の extension と API を組み合わせているだけですが、操作感については「.」を打つことでページを切り替えられるような UI にしました。また、Chrome の Omnibox は一度に表示出来る候補リストのサイズが少ないため、サーバーの負荷を減らすためにサーバー側から取得した検索結果のバッファをページングし、終端まで辿り着いた段階で API を叩いてリモートから次のページを取得するようにしています。また、当然ですがキーボード入力は適当に間引いて負荷を減らしています。


4. まとめ

以上が今回開発した内容の概要になります。
Chrome 拡張の開発についてのまとめです。

  • Chrome の API ドキュメントは良く整備されており、Chrome extension のソースコードも GitHub などに沢山公開されているため、キャッチアップは比較的容易(但し GitHub に公開されている extension のコードは玉石混淆なため、安易に引用するのはオススメしません)
  • Yeoman の generator を使うと開発環境は何も考えずにセットアップできる
  • popup や omnibox の実装は、それほど Chrome 固有の知識を要求されるものではなく、HTML と JavaScript の知識があれば十分

「Chrome 拡張の開発」と言うとなんとなく敷居が高く感じてしまいますが、それほど難しい概念があるわけでもないので、皆さんも気楽に開発してみると楽しいのではないでしょうか。


NewsPicks では一緒に開発してくれるエンジニアを募集しています!様々なバックグラウンドを持つエンジニアや編集部の皆と世界一の経済メディアをつくりましょう!興味の在る方は是非 Wantedly などでお気軽にオフィスまでお越し下さい!


2014年12月12日金曜日

test js


NewsPicksの開発をしている板倉です。


NewsPicksではニュースを見る画面とは別に、
どの記事がどれくらい読まれているかという画面の開発を進めています。
直感的にわかる画面がほしいということで、 D3.jsを使って画面を開発することになりました。
D3.jsを使うにあたって勉強するつもりで何か作ろうと思い書いたのが今回のエントリーになります。

今回の開発環境

Mac OS X(10.10)
D3.js(3.4.13)

D3.jsについて

まずは、D3について少しだけ。
Githubの人気リポジトリに入っている人気のJavaScriptライブラリです。(2014/11 時点)


APIリファレンスを見ると、たくさんの機能があるのがわかります。

地図を書いてみよう

APIリファレンスを見ていて気になったのがGeography
ということで日本地図を書いてみました。




参考にしたサイト

地図上に何か表示してみよう


地図を描いただけだと面白くないので、地震のデータを使って地図上に表示してみました。
まずは、データの取得。
以下のサイトで日本の緯度経度、地震の大きさと期間を入力してデータをダウンロードしました。

USGS


ダウンロードしたデータを0.1秒ずつずらして描画してみました。
円をそのままにしておくと画面が円だらけになるので、描画して2秒後に消しています。

d3.csv('geo/eq.csv', function(d) {
    // データが降順だったので反転
    d = d.reverse();
    // 大きさ
    var rScale = d3.scale.linear().domain(d3.extent(d, function(e){
         return e.mag;
    })).range([3, 81]);
    // 深さ
    var colorScale = d3.scale.linear().domain(d3.extent(d, function(e){
         return e.depth;
    })).range(['yellow', 'red']);
    g.selectAll('circle')
         .data(d)
         .enter().append('circle')
         .attr('fill', function(d) { 
              return colorScale(d.depth);
         })
         .attr('fill-opacity', 0.5)
         .attr('stroke-width', '1')
         .attr('stroke', function(d) { 
              return colorScale(d.depth);
         })
         .attr('cx', function(d){
             return projection([Number(d.longitude), Number(d.latitude)])[0];
         })
         .attr('cy', function(d){
             return projection([Number(d.longitude), Number(d.latitude)])[1];
         })
         .transition().delay(function(d, i) { 
              return i * 100;
         })
        .attr('r', function(d) {
             return rScale(d.mag)
         })
        .each('start', function(d){
             dateText.text(df(new Date(d.time)));
             mag.text('M ' + d.mag);
             depth.text('Depth: ' + d.depth + ' km');
         })
        .transition().delay(function(d, i) {
             return (i * 100) + 2000
         })
        .each('end', function(d){
             d3.select(this).remove();
         });
})


実装したものはこちら

数字が並んでるデータを見るだけだと気付きにくいことも
図形で表現してみると新しい発見がありますね。

Uzabaseではデータの視覚化を進めるエンジニアを募集しております。
興味をお持ちいただいた方はWantedlyなどからご連絡ください!
SPEEDA 開発チームの緒方です。

最近話題に上ることも多い Docker ですが、UZABASE でもチームによっては積極的に使っています。
(現在は主に開発用途。)

他のコンテナ型仮想化技術と比較した場合の Docker の良さとして、
  • Docker Hub など、レジストリに登録されているイメージを利用できる
  • Dockerfile を使用したイメージ構築の自動化
などが挙げられると思います。

Docker Hub をざっと見渡しただけでも、Ubuntu や CentOS など環境だけを提供するものから redis や MySQL などそのままアプリケーションが実行できるものまで様々なイメージを見つけることができ、その良さを垣間見ることができます。

さて、その便利な Docker のイメージですが、実体がどのようになっているかご存知でしょうか。
Ubuntu や CentOS が丸ごと入っているくらいだからさぞかし難解なものだろうと思われる方もいらっしゃるかもしれませんが、作成の手順はとても単純です。

ということで、試しに作ってみることにしました。

とりあえず簡単なものということで、
  • フルスクラッチから作る
  • busybox を動かすだけの最小のイメージ
ということを目標とします。
イメージの作り方にも色々な方法があるのですが、今回はイメージのファイル構成を含んだアーカイブを作成して docker import するというやり方でいきます。

アーカイブの最終的なファイル構成は次のようになります。
bin/
bin/busybox (※busybox-x86_64 へのシンボリックリンク)
bin/busybox-x86_64
話がややこしくならないように、busybox はスタティックリンク版を利用します。
(ダイナミックリンクしているバイナリを実行するためにはライブラリの配置や /etc/ld.so.conf の設定などが必要です。)

以下手順です。

環境:
Docker 1.3.2 / Debian sid (Linux 3.16.0-4-amd64 #1 SMP Debian 3.16.7-2 (2014-11-06) x86_64 GNU/Linux)
$ mkdir -p busybox-static/bin
$ cd busybox-static/bin
$ wget -P . http://www.busybox.net/downloads/binaries/latest/busybox-x86_64
$ chmod +x busybox-x86_64
$ ln -s busybox-x86_64 busybox
$ cd ..
$ tar zcf busybox-static.tar.gz bin
これで、busybox を含んだ bin ディレクトリだけの最小の Docker イメージができました。
作成したアーカイブを docker import します。
$ cat busybox-static.tar.gz | docker import - busybox-static
うまくいったようです。
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
busybox-static      latest              85e373c46d80        27 seconds ago      973.2 kB
では、run してみます。
$ docker run busybox-static /bin/busybox ls -a
.
..
.dockerenv
.dockerinit
bin
dev
etc
proc
sys
これも成功です。目標達成!

ところで、アーカイブには bin ディレクトリしか含まれていなかったはずですが、いくつかファイルやディレクトリが追加されています。
これらは、デバイスなどホストのリソースを利用できるように、Docker が勝手に作ってコンテナに追加してくれているものです。

このような感じで、単純なイメージであれば非常に簡単に作ることができます。
Docker コンテナ内でプログラムを実行するには何が必要であるか、コンテナからホストのリソースはどのように見えているかなど勉強になることも多いので、一度スクラッチからのイメージ作成をやってみても面白いかと思います。

test scala

技術チームインターンの中村です。

内製化されたシステムを抱えた会社にいると,エンジニア以外の方のためにドメイン特化言語を構築するようなこともあるかと思います。 uzabaseの場合,アナリストがSPEEDAに載せる業界概要の記事を効率良く書けるようになるために,Markdownに似た軽量マークアップ言語が作られました。

作る言語が構文木が不要なほど小規模ならば,文字用ユーティリティだけで十分に言語実装が可能かと思います。 一方で,言語が大規模であったり効率の良いコンパイルが求められたりするのであれば,LexやYaccのようなパーサージェネレータが必要になるかもしれません。

 今回はその間くらい,つまり単純な文字列処理では足りないものの,パーサージェネレータを使うほどでもないくらいの言語を構築するときに便利なScalaのパーサーコンビネータについて紹介します。

受け取った入力の結果を返す関数としてパーサーを組合せることで,目的のパーサーを高階関数(コンビネータ)として構築したものがパーサーコンビネータです。 パーサーは文法構造の連続,繰り返し,選択などを実現する関数で組み合わされます。 このとき,言語が関数あるいはメソッドの中置記法をサポートするなら,パーサーコンビネータの定義はEBNFの生成規則と似たものになります。 たとえば,論理式のEBNFとこれに従う論理式をパースするパーサーコンビネータの対応は以下のようになります。

EBNF
expr ::= termA {"|" termA}
termA ::= termN {"&" termN}
termN ::= factor | {"!" factor}
factor ::= "TRUE" | "FALSE" | "(" expr ")"
|は選択,{}は0以上の繰り返し

パーサーコンビネータ
def expr: Parser[Any] = termA~rep("|"~termA)
def termA: Parser[Any] = termN~rep("&"~termN)
def termN: Parser[Any] = factor | "!"~factor
def factor: Parser[Any] = "TRUE" | "FALSE" | "("~expr~")"
|は選択,~は連続,rep()は0回以上の繰り返し
  
|, ~, repをメソッドとして別のParserを引数にとり,より複雑な文字列を受理するようなParserを返す,この繰り返しによって論理式を受理するパーサを構築しています。 上の定義は宣言的なので,どのような計算がパース過程で行われるのか定義から読み取ることができません。 そこで,アトミックなパーサと,パーサ同士を逐次的に繋げた場合の計算がどのような行われるか見て行きたいと思います。

最小のパーサはsuccess, failureというメソッドです。 これらは入力をまったく消費しません。 2つのメソッドは以下のように定義されています。

def success[T](v: T) = Parser{ in => Success(v, in) }

def failure(msg: String) = Parser { in => Failure(msg, in) }

ヘルパーメソッド
def Parser[T](f: Input => ParseResult[T]): Parser[T] = new Parser[T]{
  def apply(in: Input) = f(in)
}
  
Parserというのが,|や~をメソッドに持つパーサーのクラスです。 Inputは,パーサーが生の文字列だけではなく,トークンのストリームも読めるようにするために,パース対象を抽象化するクラスです。 Success, FailureはParseResultの子クラスとして,それぞれ成功,失敗したパース結果と残りの入力をまとめるクラスです。これらのクラスの宣言は以下のように定義されています。

abstract class Parser[+T] extends (Input => ParseResult[T]) {..}

case class Success[+T](result: T, override val next: Input) extends ParseResult[T] {
  ..
}

case class Failure(override val msg: String, override val next: Input) 
  extends NoSuccess(msg, next) {..}
  
success, failureの次に仕事をするパーサが以下のelemです。 これは,入力を適用すると,入力の先頭がeであれば,resultにe, nextに2番目以降の入力もつSuccessを返します。 また,先頭がeでなければFailureを返します。

def elem(e: Elem): Parser[Elem] = accept(e)
  
ここまでで,Parserが入力(Input)を受けつけ,ヘルパーメソッドのfにinputを適用した結果(result)と入力の残り(next)を返す関数であるようなイメージを持って頂けたと思います。
次に逐次的にパーサ同士をつなげるメソッド~を見ていきます。 ~は以下のように定義されています。

def ~[U](q: => Parser[U]): Parser[~[T, U]] = { lazy val = p = q
  (for {
    a<- this
    b<-p
  } yield new ~(a, b)).named("~")
}
  
new ~(a, b)は繋げられた2つのパーサーの結果をひとつにまとめるためケースクラス~のインスタンスです。 また,for式に必要なflatMap, mapの定義は以下になります。

def flatMap[U](f: T => Parser[U]): Parser[U] = Parser{ in => 
  this(in) flatMapWithNext(f) 
}

def map[U](f: T => U): Parser[U]  = Parser{ in => this(in) map(f)}
  
上の定義で使われるSuccessにおけるflatMapWithNextとmapは以下のようになります。

def flatMapWithNext[U](f: T => Input => ParserResult[U]): ParseResult[U] =
   f(result)(next)

def map[U](f: T => U) = Success(f(result), next)
  
そして,これらを用いてfor式を地道に展開していくと例えば次のように書き直せます。

new Parser[T~U] {
  def apply(in: Input) = Parser.this(in) match {
    case Success(r, n) => p(n) match {
      case Success(r1, n1) => Success(new ~(r, r1), n1)
      case Failure(msg, next) => Failure(msg, next)
    }
    case Failure(msg, next) => Failure(msg, next)
  }
}
  
最初のパーサーが返すParseResultにある残りの入力が次のパーサに渡されることで,計算が進めるようです。 上の定義を見ると,はじめのパーサが失敗すれば計算は終了。 成功すれば,次のパーサpに残りの入力(n)を渡す。 そこで失敗すれば後続する計算がなされず全体の計算が終了。 成功すれば両パーサの計算結果としてケースクラスの~が返されています。

 ここまでで,パーサーコンビネータの実体が引数をパース対象として受付け,関数適用の結果と残りのパース対象の組を返す関数のように振舞うことがなんとなく伝わったかと思います。

 さらに詳しく知りたい方はこちらを参考にパーサーコンビネータそのものを自作してみるとよいかもしれません。 リンク先の資料ではパーサコンビネータがモナド値であることを踏まえた上で実装方法を紹介しています。 事実,ScalaのParserやParseResultはモナド値となっています。
資料ではParserが次のように定義されています。

newtype Parser a = Parser (String -> [(a,String)])
  
InputがString, ParseResultが[(a, String)]とそれぞれ対応しています。 空リストがFailureを意味します。 最後に上のpdfの実装をScalaで試すときに,モナド則の確認やfor式の記述に最低限必要な定義を残しておくので,ご参考にしていただければと思います。

trait Parsers {
  def Parser[A](f: String => List[(A, String)]): Parser[A] = new Parser[A]{
    def apply(input: String) = f(input)
  }

  def success[A](v: A): Parser[A] = Parser{ input => List((v, input)) }

  def failure[A]: Parser[A] = Parser { input => List() }

  trait Parser[A] extends (String => List[(A, String)]) {
    def apply(input: String): List[(A, String)]

    def flatMap[B](f: A => Parser[B]) = Parser{ (input: String) =>
      for {
        (a, input2) <- this(input)
        b <- f(a)(input2)
      } yield b
    }

    def withFilter(pred: A => Boolean): Parser[A] = Parser { input: String =>
      for {
        elm <- this(input) if pred(elm._1)
      } yield elm
    }

    def map[B](f: A => B) = Parser {input =>
      for {
        a <- this(input)
      } yield(f(a._1), a._2)
    }
  }
}
  
付録, for式の展開過程

this.flatMap {
  a => p.map(b => new ~(a, b))
}

Parser {
  in => Parser.this(in) match {
    case Success(r, n) => ((a: T) => p.map(b => new ~(a, b)))(r)(n)
    case Failure(msg, next) => Failure(msg, next)
  }
}

Parser { in =>
  Parser.this(in) match {
    case Success(r, n) => Parser { in => p(in).map(b => new ~(r, b)) }(n)
    case Failure(msg, next) => Failure(msg, next)
  }
}

Parser { in =>
  Parser.this(in) match {
    case Success(r, n) => p(n) match {
      case Success(r1, n1) => Success(new ~(r, r1), n1)
      case Failure(msg, next) => Failure(msg, next)
    }
    case Failure(msg, next) => Failure(msg, next)
  }
}
  
Techチームの遠藤です。

コンテンツのグローバル展開に向けて、いくつかの地点での画面表示の速度測定のためにphantomjsを使用し計測しました。
全部ではないですが初めの部分だけ記述します。

PhantomJSはまぁ仮想ブラウザを立ち上げてHPを操作していくものですね。
自動テストなどに使えるものです。

PhantomJS
http://phantomjs.org/

DownloadでそれぞれのOSにあったものをDLしてください

私はwindowsでやってるのでwindowsのみ記述します。
といっても大したことしてません。

windowsの場合はDLしてきたzipファイルを解凍し、任意の場所に配置してから環境変数にそのフォルダを指定すると使えるようになります。

さっそく使ってみます。

jsファイルを書いて、コマンドプロンプトで
phantomjs XXX.js

phantomjsコマンドと実行するファイルを指定するだけでできます。

まず手始めにgoogleへアクセスしてみます。

var page = require('webpage').create();
page.open('http://www.google.co.jp/', function () {
        page.render('sample.png');
        phantom.exit();
});

新しく作ったJSファイルに上記を記述して実行。

アクセスできました。
sample.pngというピクチャが保存されています。

↓こんな感じ



実際に使うにはどんどんページ遷移していきたいので、、、
下記ページを参考に遷移する先をtask化していきます。

var page = require('webpage').create();

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      var imagename = 'sample2_' + ('0' + task.id).slice(-2);
      page.render(imagename + '.png');

      next();
    }
  });
}

var tasklist = [
{
  id:1,
  path:'http://www.yahoo.co.jp/'
},{
  id:2,
  path:'http://www.google.co.jp/'
}];

next();
こんな感じに...


実行してみるとちゃんとアクセスされているようです。
sample2_01.png、sample2_02.pngと画像が保存されているので順にアクセスされてる模様です。

さて、意味もなくやってるわけじゃなくて仕事で使うので自社サイトのアクセスをするわけですが。。。
自社サイトでは認証があるのでそれを実装します。

var page = require('webpage').create();

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      var imagename = 'sample3_' + ('0' + task.id).slice(-2);
      page.render(imagename + '.png');
      if(task.operation == null){
        next();
      } else {
        task.operation(page);
      }
    }
  });
}

var tasklist = [
{
  id:1,
  path:'http://www.ub-speeda.com/',
  operation:
    function(page){
      page.evaluate(function() {
        //ここは各サイトで実装が異なります
        document.forms[0].username.value = 'dummy_username';
        document.forms[0].password.value = 'dummy_password';
        document.forms[0].submit();
      });

      //弊社サイトではログイン数管理をしており、それを超過した場合、古いログインを追い出すなどがあるため確認メッセージを出し、再度ログインボタンを押してもらう仕様のため、その対応です
      setTimeout(function() {
        if (page.url.indexOf('top/welcome') > 0) {
          page.evaluate(function() {
            document.forms[0].submit();
          });
          setTimeout(function() {
            next();
          }, 3000);
        } else {
          next();
        }
     }, 3000);
    }
},{
  id:2,
  path:'http://www.ub-speeda.com/company/companyinformation/cid/JPN3O1U73D3VNGWO'
}];

next();

operationという変数を作ってそこにログイン処理を放り込んでみました。
operationが実装されていれば実行する感じですね。
これを応用すればHP内の操作を追加していけるかと思います。
こちらは実際動作するのですがID/Pass等が必要なため試していただける方は少ないかと思います。(ゴメンナサイ

次にファイルのアクセスに関しての情報をファイルに記述します。
ここではページのロード時間しか記述しませんが、ちょこっと検索すればより詳細な情報を取得できるサンプルコードがあるところがあるので詳細はそちらを参考にしてください。

まず、ファイルに書きだす方法ですが公式を参考に。。。

var fs   = require('fs');
fs.write(ファイル名, 内容, 'w');

で書き出せます。

組み込んでみましょう。

var page = require('webpage').create();
var fs   = require('fs');

page.onLoadStarted = function() {
  page.startTime = new Date();
};

function next(){
  if (tasklist.length == 0) {
    phantom.exit();
  }
  var task = tasklist.shift();

  page.address = task.path;
  page.resources = [];

  page.open(page.address, function(status) {
    if (status !== 'success') {
      console.log('FAIL to load the address');
      phantom.exit(1);
    } else {
      page.endTime = new Date();

      var logname = 'sample4_' + ('0' + task.id).slice(-2);
      store(logname, page.endTime, page.startTime);

      if(task.operation == null){
        next();
      } else {
        task.operation(page);
      }
    }
  });
}

function store(logname, endTime, startTime){
  page.render(logname + '.png');
  fs.write(logname + '.log', endTime - startTime , 'w');
}

var tasklist = [
{
  id:1,
  path:'http://www.ub-speeda.com/',
  operation:
    function(page){
      page.evaluate(function() {
        document.forms[0].username.value = 'dummy_username';
        document.forms[0].password.value = 'dummy_password';
        document.forms[0].submit();
      });

      setTimeout(function() {
        if (page.url.indexOf('top/welcome') > 0) {
          page.evaluate(function() {
            document.forms[0].submit();
          });
          setTimeout(function() {
            next();
          }, 3000);
        } else {
          next();
        }
     }, 3000);
    }
},{
  id:2,
  path:'http://www.ub-speeda.com/company/companyinformation/cid/JPN3O1U73D3VNGWO'
}];

next();

さて、これで
sample4_01.log
sample4_02.log
というファイルが出来ました。

中身は「endTime - startTime 」という内容が示すとおり殺伐とした数字だけです。
sample4_01.logの中身
---
219
---

sample4_02.logの中身
---
2448
---

これでページを開く時間がわかるようになりました。
実際にはこれをより詳細にHAR形式で出力するなどして、使えるものに仕上げていってます。
他にも、ダウンロードやajaxでの表示待ちなどを組み込みながら作ったりします。

こんな感じで日々仕事してます。。。