PoltergeistのURL Blacklisting/Whitelistingを活用してAjaxスクレイピングを速くする

よくお魚の「脂ののりが良い」といいますが、実際例えば体脂肪率という形で計測するとどうなるのでしょうか。実はそれを測定するためにフィッシュアナライザ™という製品があり、人間用の体脂肪率計と同じ仕組みで非侵襲的な計測が可能なんだそうです。アジなどでは10%とかいうオーダーの数字とのことで、意外とスリム。

さて、Rubyを使ってWebスクレイピングをするときは、個人的にはNokogiriをよく使っています。
通常はNet::HTTPでとってきたHTMLをそのままNokogiriにぶち込めばOKなのですが、非同期での描画を行うAjaxのサイトの解析はそのままではできません。
そこで、PhantomJSのRubyフロントエンドであるPoltergeistをドライバとしてCapybaraを組み合わせると、深く考えることなく静的ページと同じ感覚で解析ができてしまいます。要は、見えないところでまるっとブラウザを動かしてしまって、DOMから仮想的なHTMLを生成させちゃってNokogiriにぶち込む…というようなアプローチです。

require 'nokogiri'
require 'capybara'
require 'capybara/poltergeist'

def wait_for_ajax(session)
  # https://robots.thoughtbot.com/automatically-wait-for-ajax-with-capybara
  Timeout.timeout(Capybara.default_wait_time) do
    return if session.evaluate_script('jQuery.active').blank?
    loop until session.evaluate_script('jQuery.active').zero?
  end
end

def access(url)
  Capybara.register_driver(:poltergeist) do |app|
    Capybara::Poltergeist::Driver.new(app, {
      js_errors: false  #JSに問題があったとき例外を吐かせない。スクレイピングの際は常にfalseがいいです。
    })
  end
  s = Capybara::Session.new(:poltergeist)
  s.visit(url)
  wait_for_ajax(s)
  s
end

def get_html(url)
  s = access(url)
  html = s.html
  s.reset!
  s.driver.quit
  html
end

#あとは普通にNokogiriにぶっこんでゴリゴリやってくだけ!
page = Nokogiri::HTML.parse(get_html(url))
page.css('body')

ちなみに、Capybara::Sessionのインスタンス(accessメソッドの戻り値)に対して、ちょうどfeature specで書くようなマッチャを使ってページに対する操作ができるので、例えば「ここをクリックすると出てくる情報を解析したい」という作業も簡単にできます。

s = access(url)
s.click('Submit')
s.find('.text')  #findを呼ぶとマッチする要素が現れるまで再描画を(デフォルトで2秒間)待ってくれます。
s.html #これをNokogiriにぶち込めばOK

後ろでブラウザがまるごと動いていることの弊害と言ってはあれですが、スクレイピングにあたっては必要ではない情報も非同期でゴリゴリ取ってくることになり、負荷や時間などが増大します。

Continue reading

Sidekiq::Schedulerで動的にタスクをぶち込む

お魚の中でもメバルやタチウオなどは夜行性のものとして有名で、特にタチウオなどは昼間沖の深めにいて夜には岸辺の浅いところにくるなど、かなり規則的なパターンで活動するのだそうです。

さて、rubyで野良アプリ書くときバッチ処理エンジンにはSidekiqを好んで使っています。
Sidekiqには日次処理などを簡単に行うため(*1)にSidekiq::Schedulerという拡張gemがあり(*2)、任意の定義済みワーカを例えばcronライクに繰り返し実行することができます。
基本的には各ワーカの実行条件やパラメータをYAMLの形で全て静的に記述し起動時に読み込むというスタイルなのですが、時には動的にワーカをスケジュールに突っ込みたいことがあります。

Sidekiq::Schedulerはもちろんそれもサポートしています。
ただmoove-it/sidekiq-schedulerの説明がいまいちだったので、備忘録も兼ねてまとめました(*3)。

初期化

まず、初期化の段階でSidekiq::Schedulerがタスクの動的な追加削除を受け付けられるよう設定しなければいけません。
Railsなどであればinitializerに次のようなコードを書くなり、

Sidekiq::Scheduler.dynamic = true

Sidekiqを直接走らせる場合には設定を書いたyamlを渡すなり、

% cat config/sidekiq.yml
 :dynamic: true
% sidekiq -C config/sidekiq.yml

バッチ側だけでなく、フロント側やコンソールなど動的にタスクを突っ込む元となる全ロールでこの設定がなされている必要があります。

タスクの動的な追加

Reloading the schedulesに説明があるとおりですが、Sidekiq.set_scheduleメソッドを使って追加ができます。
第一引数の名前は一意であれば何でも構いません。名前を変えることで、同じワーカーを別のパラメータでスケジュールに突っ込むことができます。
第二引数のハッシュに指定できる項目は静的にyamlに書く際に指定できるものと同じです。キーはシンボルでも文字列でも大丈夫です。
また、スケジュールを突っ込んだあとは、reload_schedule!メソッドによってSidekiq::Schedulerに認識させる必要があります(*4)。

> Sidekiq.set_schedule('DynamicWorker', {every: '10s', class: 'DynamicWorker'})
> Sidekiq::Scheduler.reload_schedule!    # Don't forget!
2016-02-21T15:20:24.545Z 89314 TID-oxsqo8m4g INFO: Reloading Schedule
2016-02-21T15:20:24.546Z 89314 TID-oxsqo8m4g INFO: Loading Schedule
2016-02-21T15:20:24.546Z 89314 TID-oxsqo8m4g INFO: Scheduling DynamicWorker
2016-02-21T15:20:24.548Z 89314 TID-oxsqo8m4g INFO: Schedules Loaded

Railsなどでコンソールを叩いている場合、reloadしたさいに再読み込みされたタスク群のなかに新たなタスク(上記の場合DynamicWorker)が入っていればOKです。

これで、あとは勝手にキューイングしてくれるようになります。

...
2016-02-21T15:11:37.750Z 75551 TID-ouhmu4v8o INFO: queueing DynamicWorker (DynamicWorker)
2016-02-21T15:11:48.049Z 75551 TID-ouhmu4v8o INFO: queueing DynamicWorker (DynamicWorker)
2016-02-21T15:11:58.346Z 75551 TID-ouhmu4v8o INFO: queueing DynamicWorker (DynamicWorker)
...

で、Sidekiqが後ろで走っていれば勝手に実行してくれます。

...
worker.1 | 2016-02-21T15:14:59.644Z 75671 TID-ouxhncu88 DynamicWorker JID-a86e06a4ca000df092e1424c INFO: start
worker.1 | 2016-02-21T15:15:04.648Z 75671 TID-ouxhncu88 DynamicWorker JID-a86e06a4ca000df092e1424c INFO: done: 5.004 sec
worker.1 | 2016-02-21T15:15:09.661Z 75671 TID-ouxhncu88 DynamicWorker JID-2a7c2d743b88769da69d0a0d INFO: start
worker.1 | 2016-02-21T15:15:14.666Z 75671 TID-ouxhncu88 DynamicWorker JID-2a7c2d743b88769da69d0a0d INFO: done: 5.004 sec
...

タスクの動的な削除

上述の方法で動的に追加したタスクにかぎらず、Sidekiq::Schedulerが認識しているタスクはその名前を指定して削除することができます。

> Sidekiq.remove_schedule('DynamicWorker')
> Sidekiq::Scheduler.reload_schedule!    # Don't forget!
2016-02-21T15:21:40.522Z 89314 TID-oxsqo8m4g INFO: Reloading Schedule
2016-02-21T15:21:40.522Z 89314 TID-oxsqo8m4g INFO: Loading Schedule
2016-02-21T15:21:40.522Z 89314 TID-oxsqo8m4g INFO: Schedules Loaded  # "DynamicWorker" no longer exists!!

remove_scheduleは指定された名前のタスクが存在してもしなくてもtrueを返すので値を使わないよう要注意。

個人的にはset_schedule!/remove_schedule!なるメソッドもあってreloadを明示的に呼ばなくてもいいようになってると嬉しい人も多いかもなーなどと思っていました。ぼくのことです。

*1: ActiveJobに乗り移る気にならないのはこれがシンプルにできないからだったりします

*2: ちなみにResqueの場合Resque::Schedulerがあり、似たテイストで使えて便利です。どちらも後ろにrufus-schedulerを使っています。

*3: Readmeを改善するPR送ればいいだけの話なんですが。

*4: Sidekiq.reload_schedule!というメソッドもありますがSidekiq::Scheduler.reload_schedule!を呼ぶ必要があります。落とし穴です。はまったのはぼくだけですかすみません。

[翻訳]自動テストをすべき5つの理由

最近まで知らなかったのですが、さかな検定(通称ととけん)という検定試験があり、すでに6回実施されているのだそうです。ご推察の通り、さかなクンさんが一枚噛んでいらっしゃるようです。

さて、さいきん仕事でテストを全く書かないチームにジョインしたのですが、やはりこのままではつらい未来しか見えないため、なんとかテストを書くチームに変えていこうとテストをバリバリ書く先輩と一緒に活動を始めつつあります。

ところがいざテストを書くモチベーションを話そうとしてみると、19歳でTDDに出会い初配属部署も「テストは呼吸であり」「テストは義務教育であり」「テスト書けて初めて法の下の平等と基本的人権が主張できる」環境で過ごしてきた今になって言葉に集めるのも骨が折れそうだったので、ちょうど見つけた短めでわかりやすい英語記事を紹介することにしました。
今回はそれを和訳したものを紹介します。

この度翻訳した記事はこちら。
5 advantages of automated testing vs. manual testing.

Continue reading

2015年現地参戦したJリーグスタジアム

日本の自治体(市区町村)は全部で1936あるようですが(*1)、名前に「魚」の文字が入っている自治体数は意外にも4つと少なく、そのうち3つが新潟糸魚川市・魚沼市・南魚沼市、1つが富山県魚津市と、北越に集中しています。魚へんの漢字(*2)を地名に含む自治体に広げても、鯖江市や鰺ヶ沢町などを加えて、東北と北越の9自治体のみでした。

さて、数年前からJリーグを見るようになりまして、今年は地元のレノファ山口FCがJ3リーグ参入を果たしたのをきっかけにいよいよ本格的に観るようになりました。
生観戦の魅力は計り知れないものがあり、またスタジアムそのものも興味の対象だったりするため、今年足を運んだスタジアムについてざっくりとまとめてみました。
(ゴール裏の開放されない試合を除き)全てゴール裏、いわゆるコアサポとして観戦しています。

succer

Continue reading

嫁の顔忘れてもTimecop.returnは忘れないでねという話

夜釣りでは初心者向けの狙い目といえばアナゴなんだそうです。岸壁で釣れちゃうんだとか。ウナギはお高いのでアナゴで一杯、とかあるんだろうか。

さて、Rubyでいろいろ開発していて時間(時刻・期間…)が絡むテストをするときTimecopが便利で広く使われます。
travisjeffery/timecop – GitHub

# 現在時刻を変える
> Timecop.travel(Date.parse('1998/12/1'))
=> 1998-12-01 00:00:00 +0900
> Date.today
=> Tue, 01 Dec 1998

# 時間の進行を止める
> Timecop.freeze(Time.parse('1998/12/1 0:00:00'))
=> 1998-12-01 00:00:00 +0900
> sleep(10)
> Time.now
=> 1998-12-01 00:00:00 +0900  # it's still 0:00:00

# 時間の進行を早める
> Timecop.travel(Time.parse('1998/12/1 0:00:00'))
> Timecop.scale(3600)
> sleep(2)
> Time.now
=> 1998-12-01 02:00:19 +0900  # it took 2 hours although only slept for 2 seconds

便利ですね。
時間をいじくるので、

  • タイムゾーン周りをミスってないか
  • DBMSのタイムスタンプやredisのexpireのようにruby処理系の時間を使わないものでは効かない

こんなかんじで注意が必要なのは当然なのですが、一番しんどいのはTimecopで変えた時間を戻し忘れたときの挙動不審なかんじでしょうか。
テストでTimecopを使う場合、このテのミスをするとたいてい別のテストケースに影響しますね。順序依存で効いてくるし煽りを食ったテストケース自体は何も悪く無いことがほとんどなので、心神耗弱状態になります。

  • なぜかCapybaraのCookieが取れなくなっている
  • redisではうまくいくのにredis_mockでテストすると想定外の挙動になる
  • Timeoutが狂う

こんなことで時間をムダにするのはあほらしいので、Timecop.returnを呼ぶのは絶対に忘れないようにしましょう。

> Timecop.travel(Time.parse('1998/12/1 0:00:00'))
> Timecop.return
> Date.today
=> Sun, 20 Dec 2015

「忘れないでね」「気をつけてね」はものごとの解決策としては最悪な思考停止なので、
Rails/RSpecをお使いなら、spec/spec_helper.rbにて

  config.after(:each) do
    Timecop.return
  end

なる設定をしておくべきです。
また、別の安全策として、Timecopのsafe_modeといい、Timecopを使うときはblockを与えることを強制し、block末尾でreturnが自動で行われるようにする仕組みがあります。
blockを渡さずに呼ぶと例外が飛ぶので、すぐに気づきます。

> Timecop.safe_mode = true
> Timecop.travel(Time.parse('1998/12/1 0:00:00'))
Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed.
...
> Timecop.travel(Time.parse('1998/12/1 0:00:00')) do
>   Date.today
> end
=> Tue, 01 Dec 1998
> Date.today
=> Sun, 20 Dec 2015

Rails/RSpecのみで使うなら、spec_helperのafterでケアするのがシンプルにして安全十分かと。
テスト以外でも使うなら、safe_modeにするようinitializerなどで設定しておくほうが安全そうです。

全部Githubに書いてあるんですけども、1ヶ月近くこれにハマってしまった自分が本当にあほらしかったので忘れないように…。

第29回関東CV勉強会でSelective Search for Object Recognitionを紹介してきました

初ガツオの季節です。都会なので目に青葉は映りません。山などないのでホトトギスもいません。テッペン原稿カケタカ!

さて、仕事がらComputer Vision的な活動はぜんぜんできていなかったこの1年でしたが、年明けたぐらいから気持ち的にも余裕ができてきたので、ちょいちょいと遊んでいました。
そして先日開催された関東CV勉強会で、かなりひさしぶりに外で専門分野のお話をさせていただく機会をいただき、検出界隈では有名と思われる手法Selective Search(*1)(*2)について紹介してきたので、簡単に振り返ってみたいと思います。

勉強会情報

Continue reading

ndarrayとPySideのQImageとの相互変換

魚類はあまり変態しない(生まれた姿のまま大きくなる)ことが一般的で、カレイなどのように左右対称形から非対称になる…など、昆虫や両生類のそれに比べると控えめな程度にとどまるようです。

さてnumpyと愉快な仲間たちにおいて画像はndarrayとして表現されていて、それを表示するのに使うmatplotlibはちょっとしたGUI機能(widgetsモジュール)をもっています。
ただこれコード的にも絵的にもクセが強くごく単純なデモ以上のことは難しいので、何かしたいときはQtのPythonラッパーであるPySideを使っています。

んで、そのPySideで画像はQImageで扱われるので、裏っかわでndarrayで処理をしているならそれをQImageとの間で相互変換できる必要があります。

そのやり方はこちら。

(参考: python – QImage to Numpy Array using PySide – Stack Overflow

画素毎にループしてコピーするとかイケてないことしなくて済むから簡単だね!
QImageに持ってきてしまえばQPixmapにしてQLabelに突っ込んで表示するなりQPainterでお絵かきするなり、自由です。

2行N列のndarrayを一発で(1行目≦2行目)にするnumpy芸

各国における国民1人当たりの魚の年間消費量をランク付けすると、1位はモルディブなんだそうです。土地柄を考えると妥当な結果と言えそうで、6位の日本の2倍以上です。

Numpyにて、行同士の大小関係を強制したい、つまり「2行N列のndarrayがあるんだけど、各列ごとに、常に1行目の値が2行目よりも小さい値を取る必要がある」「そうでない列の値を強制的に上下交換させたい」という状況に遭遇しました。
生ループ回してひとつひとつチェックしては…とやってもいいんですが、それをPython側でやっていてはパフォーマンス上どうかなということで、Numpyのインデクス芸・スライス芸・ブロードキャスト芸あたりを駆使してやってみました。

もちろん、N行2列のndarrayの列間の大小関係を統一するのも同様に可能。

インデクシングなどの詳細の説明はここではしません。
日本語であればこのあたりがわかりやすそう(まだ見てない)。

性能やいかに?

Benchmarker.pyを使って、実行時間を比べてみました。
比較対象は、まぁ何も考えずに書くとそうなりますわな、というような雰囲気のコード(だとおもう)。
#ところで標準の時間計測モジュールtimeitが最高に嫌いなんだけどあれなんであんなふうになってしまったのかな。。。

こちらが実行結果。
proposedが本記事で紹介した黒魔術、conventionalがシンプルな方法。

##                             real    (total    = user    + sys)
col-proposed                 3.6327    3.6200    2.8100    0.8100
row-proposed                 3.6977    3.6900    2.8700    0.8200
col-conventional            49.9127   49.8600   49.7600    0.1000
row-conventional            51.3075   51.2600   51.1600    0.1000

だいたい14倍高速ですね。
メモリ上では行オーダーで格納される都合上、row-*とcol-*の間で速度差が出るのかなと思いましたが、たかだか2行/2列なので、関係ない模様(σが未知なのでなんともいえませんが。。)。

Pythonの高速化の鉄則は『生Python書くな』に尽きますが、それが改めて示された形になりましたとさ。

ただこのようなコード、後から読んだり他人に読ませたりするのはとても困難なので、あまり多用しないようにしましょう。

matplotlibやskimageのimshowとかでウィンドウが全く表示されないならbackendを変えてみよう

サメはいわゆる魚類(分類学的には魚類というのは無いんだそうですね)に含まれますが、普通に「さかな」と言ってイメージするような魚を指す場合、「硬骨魚綱」と呼ぶとだいたい一致するんだそうです。サメやエイは「軟骨魚綱」になります。ただし、タツノオトシゴも硬骨魚綱に含まれます。

さて、matplotlibやskimageなどを使っていて、どんな描画結果をウィンドウに出そうとしてもでないとき。
X周りはちゃんとしているのに、例えばこんなコードを実行しても、ウィンドウが表示されない場合。

import skimage.io
img = skimage.io.imread('/path/to/image.png')
skimage.io.imshow(img)

Render backendを疑ってみてください。
matplotlibが何をベースに描画を行っているかにより、絵の質や出力方式や出力先が変わります。
現在のRender backendを調べてみましょう。

>>> print matplotlib.get_backend()
agg

無印のAGGは、画面への出力には対応していません。
このようなbackendsを、”noninteractive backends”と呼んでいます。

Here is a summary of the matplotlib renderers (there is an eponymous backed for each; these are non-interactive backends, capable of writing to a file):
Usage — Matplotlib 1.4.3 documentation

ということで、画面への出力にも使えるもの”interactive backends”をrender backendにしてしまいましょう。
backendの変更はスクリプト内でもできますが、ホントに最初にやらないといけないので面倒です。
そこで、デフォルト設定を変更してしまいます。

matplotlibの設定ファイルを探し出します。例えばpyenvを使っていたら下記のようなかんじで出力されます。

>>> matplotlib.matplotlib_fname()
u'/your/home/.pyenv/versions/2.7.9/lib/python2.7/site-packages/matplotlib-1.4.2-py2.7-linux-x86_64.egg/matplotlib/mpl-data/matplotlibrc'

で、このファイルを書き換えます。

 - backend      : agg
 + backend      : Qt4Agg

interactive backendであれば何でもOKです。Aggがついてるやつがおすすめです。
また依存ライブラリは事前に入れる必要があります。
例えばQt4Aggの場合はPyQtやPySideが、GTKAggの場合はPyGTKのインストールが必要です。

調べればすぐなんですけども、backendにはinteractiveとnoninteractiveがあり、デフォルトになっている無印AggがGUIをサポートしていないというのがハマりポイントでした。
pyenv環境にpipで入れたわけですが、配布しているパッケージによっては最初からinteractive backendがデフォルトになっていることもあるようです。

Pythonのargparseからzsh補完スクリプトを吐いてくれるgenzshcompでQoLが上がった

タツノオトシゴって、あの見た目なのですが、実は魚類だったんです。なんとなく甲殻類と思っていた。最大の種は30cmを超えるそうで、あの姿でその大きさというのを想像するとちょっとこわい。。

Pythonでは、かなり強力なコマンド引数パーサとしてargparseが使えます。
いろんなオプションをもつコマンドを作ったら、引数の入力も面倒。
ということで、自分の作ったPythonスクリプト専用の補完がzshで効くようにしたいなーと思っていたら、genzshcompというパッケージがありました。

Continue reading