scanコマンドというcliツールを作った

scanコマンドというcliツールを作った。

GitHub - genya0407/scan

scanコマンドは、標準入力の各行に対して正規表現を適用し、ほしい部分を取り出すコマンドだ。使い方は以下の通り。

$ scan --help
Usage: scan [options] OUTPUT_FORMAT
    -p [PATTERN]                     specify regexp
    -d [DELIMITER]                   specify delimiter

使用例を見てもらったほうが早いだろう。

使用例

正規表現を適用する例

例えばこういうファイルがあったとする。

$ cat data.txt
hogehoge_nyan
hohho_nyan

これに対して、アンダースコアの左だけを取り出す正規表現を適用するには、以下のようにする。

$ cat data.txt | scan -p "(.+?)_.+" {1}
hogehoge
hohho

アンダースコアの左と右を、 , で繋いで出力したいときは以下のようにする。

$ cat data.txt | scan -p "(.+?)_(.+)" {1},{2}
hogehoge,nyan
hohho,nyan

複雑な正規表現を適用するときは、名前付きキャプチャも使用できる。

$ cat data.txt | scan -p "(?<name1>.+?)_(?<name2>.+)" {name1}:{name2}
hogehoge:nyan
hohho:nyan

正規表現エンジンはRuby標準のものをそのまま使っているので、正規表現の詳しい仕様についてはリファレンスをあたってください。

正規表現 (Ruby 2.7.0 リファレンスマニュアル)

区切り文字を指定する例

また、正規表現を使うのではなく、区切り文字を指定することもできる。

例えばこういうファイルがあったとする。

$ cat hoge.csv
aaa,bbb,ccc
xxx,yyy,zzz

このとき、区切り文字として , を指定して、左から3番目のフィールドを切り出すためには以下のようにする。

$ cat hoge.csv | scan -d , {3}
ccc
zzz

また、scanコマンドに何もオプションを指定しない場合、区切り文字として \s+ つまり「1つ以上連続する空白文字列」が指定されていると解釈される。

例えばこういうファイルがあるとする。

$ cat hoge.tsv
aaa     bbb     ccc
xxx     yyy     zzz

このファイルを、オプションを何も指定しないscanコマンドに食わせると、空白文字を区切りと解釈して左からn番目の文字列を取り出すことができる。

$ cat hoge.tsv | scan {2}
bbb
yyy

インストール方法

Ruby 2.6.3 以上が動く環境を前提として、以下のファイルをパスが通った場所に配置して、実行権限を付与すれば動く。依存ライブラリとかはなく、Rubyがあれば動きます。

scan/scan at master · genya0407/scan · GitHub

もう少しいい感じのインストール方法を模索していますが、ひとまずはこれで許してください。

なぜこのコマンドを作ったのか

仕事柄(?)、バグが出たときとかにサーバーのログを漁る必要にかられることがよくある。そういうときに正規表現は便利だ。

$ cat server.log | (アクセスしたユーザーのIDをいい感じに取り出す正規表現) | sort | uniq -c
100 user_id_111 # user_id_111 が100回もアクセスしているのがわかる
 10 user_id_222
..

しかし、正規表現をシュッと書いて値を抜き出す適用するツールが見当たらなかった*1。多分awkとかperlとかのワンライナー正規表現をかけるとは思うのだが、僕はawkperlも使い方がわからないし、言語ごとに存在すると思われる正規表現の方言を覚えるのもなんだかなあという気持ちだった。

僕が一番使えるプログラミング言語Rubyなので、一時期はRubyワンライナーを書くということをやっていた。

$ cat server.log | ruby -ne 'puts $_[/user_id: (.+)\s+/, 1]' | sort | uniq -c

これも割といい線行ってるとは思うが、正規表現で値を抜き出したいだけなのに ruby -ne とか puts とか $_ とか書くのはイケてない。

次に使ってたのはrargsというツールで、これは限りなく正解に近い。

GitHub - lotabout/rargs: xargs + awk with pattern matching support. `ls *.bak | rargs -p '(.*)\.bak' mv {0} {1}`

rargsは、正規表現を指定して文字列を抜き出すことができ、抜き出した文字列を使ったコマンドを実行することができる。正規表現の代わりに区切り文字を指定することもできる。

$ ls *.bak | rargs -p '(.*)\.bak' mv {0} {1}
$ cat hoge.csv | rargs -d , echo {2}

しかし、rargsはコマンドを実行する都合上、入力される行の数だけプロセスを立ち上げる必要がある。環境にもよるが、プロセスの立ち上げはそこそこヘビーな処理で、業務で使っているMacBook Proではここがものすごく重かった*2。そのため、長めのログファイルをrargsに食わせると、ちょっと現実的ではないぐらい集計に時間がかかってしまう状態になっていた。

私が望む用途(=ログの集計)ではコマンドを実行する必要はない。そのため、rargsからコマンド実行の機能を削り、高速に正規表現を適用するだけのコマンドを作ればよいだろう、という発想に至った。

そして、今回説明したscanコマンドを作った。

まとめ

標準入力に正規表現をシュッと適用して、好きなフォーマットに整形して出力するコマンドである scan を作った。これは、ログの集計・整形などに使うことができ、実用的なレベルには高速であり、自身も便利に使っている。

*1:古参のエンジニアの皆さんはおすすめの正規表現ツールでも書いててください

*2:Instrumentsでプロファイルしたらposix_nspawnでめちゃめちゃ時間喰ってた。おそらくMacBook Proが悪いのではなく、セキュリティソフトかなにかが原因で遅いのではないかと疑っている。お家で使ってるThinkipad上のLinuxでは全然重くなかった。

画像入りzipを人物認識してエクセルに変換する「マイクロサービス」を作った

概要

例の建築家の同期が、動画に映る人の位置を1秒ごとに目視で認識するという虚無作業をしていたので、自動化するWebアプリ的なものを作りました。

github.com

使い方

まず、人物認識したい動画をお好みの間隔(1秒毎とか)で画像に切り出し、適当なフォルダにいれてzip圧縮します。 そして、今回作ったWebアプリを開きます。以下はWebアプリのスクリーンショットです。

f:id:threetea0407:20200524011510p:plain

GCPのアクセストークンを頑張って取得して、「アクセストークン」という入力欄にコピペします。そして、先程のzipファイルを選択します。最後に「変換」ボタンを押すと、以下のようなエクセルファイルがダウンロードされます。

f:id:threetea0407:20200524010137p:plain

左端の列が画像ファイルの名前を表し、その隣の2列は人物を囲う長方形の左上の点の座標を表し、その隣の2列は右下の点の座標を表しています。これによって、画像の中のどの位置に人間が写っているのかを知ることができます。

イメージとしては、以下のような写真を入れると2つの赤丸の座標が取れるという感じです。

f:id:threetea0407:20200524012615p:plain

どうやって実現しているのか

Cloud Vision APIというのが世の中にはあって、物体認識をしてくれます。

cloud.google.com

今回作ったWebアプリがやってるのは、

  • zipを解凍して画像を取り出し
  • 画像をJSONに埋め込んでAPIを叩き
  • 返ってきたデータを若干加工してCSVにする

という処理だけです。つまり、機械学習的なものを自分で実装したわけではありません。

工夫した点

動画から画像への変換をやらない

もともと同期の人がやりたかったことは、「ある街に定点カメラをおいて、その動画の中で人がどう動いているかを調べる」ということでした。

今回は、動画から画像への変換はユーザーにやってもらい、Webサービスは画像入りzipから人物を認識するだけ、というインターフェースを採用しました。

f:id:threetea0407:20200524021034p:plain
ユーザーが動画から画像を切り出す必要がある

しかしこれを例えば、ユーザーに動画をアップロードさせて、それをWebアプリ側で画像に変換し、その画像に対して人物認識をする、というようなインターフェースにすることも可能です。

f:id:threetea0407:20200524021105p:plain
動画を直接変換できる

後者のほうがユーザーの仕事が減り、一見すると便利なように見えます。しかし、このインターフェースには問題があります。

例えば、今までは1秒ごとに画像を切り出していたけど、5秒ごとに切り出すようにしたい、というような要望が出てくることは容易に想像されます。そのたびにソースコードに手を入れてパラメータを修正するのは不毛です。そのようなパラメータをWebアプリ上で設定可能にすることも考えられますが、その場合は設定項目が無数に増えていくことになるでしょう*1

また、一般にこの手の物体検出は、画像の中における物体の相対的な大きさによって、検出精度が大きく変わるようです*2

f:id:threetea0407:20200524114452p:plain
右奥に写った小さな人物は認識されない

f:id:threetea0407:20200524114522p:plain
右奥を拡大すると認識されるようになる

Jörg MöllerによるPixabayからの画像

そのため、「大きな画像に沢山の人が小さく写っている」というような画像を対象にするときは、画像を適当なサイズに分割して検出にかけた上で、その結果を統合する処理が必要になります。その場合、どのようなアルゴリズムで画像を分割するか、またどのようなアルゴリズムで結果を統合するか、ということを考える必要があります。

このように「動画を画像に変換する」という領域は、要求が変化しやすい上に、真面目に考えるとかなり複雑です。

こうしたことを考え、動画から画像への変換方法に自由度を持たせ、かつアプリケーションの複雑度を下げるため、現行のような「画像をzipで固めてアップロードする」というインターフェースを採用しました。これによって、「動画を画像に変換する」という処理はこのソフトウェアの対象領域外となり、実装も使い方も非常に簡単になりました。

また、「動画を1秒ごとに画像に切り出す」というような処理は、それ用のソフトウェアがすでに存在します。そのため、このようなインターフェースを採用しても、ユーザーの手間はそれほど変わりません。

Excel形式で出す

Excelで開けるというのはかなり重要です*3。というのも、同期の人が使っているビジュアルプログラミング環境であるGrasshopperに、「Excelからコピペでデータを貼り付けられるブロック」というのがあるからです*4。これにより、認識した人物の座標をGrasshopper上で処理することができます。

また、Excelで開ける形式であるため、表計算によるデータの加工が可能です。例えば、画像ファイルに含まれる連番の番号を文字列処理で取り出して、「この人物は動画の何秒目に写っていたのか」というデータを取り出すことができます。プログラミングに不慣れな人にとっては、表計算のほうが敷居が低いため、これも便利な点です。

同期の人がGrasshopper上で処理した例が以下です。これは、街並みの3Dモデル上に動画上の人物の位置を投影し、三次元空間上での人間の動きを分析したものです。

GCPのアクセストークンを入力させる

これはCloud Vision APIの利用料金を、利用者である同期に支払わせるための仕組みです。Cloud Vision APIは非常に廉価に利用できますが、それでも60分の動画を1秒ごとに切り出した画像を全部物体検出する、みたいな処理をすると、無視できない料金が発生します。その料金を僕が負担するのは筋違いなので、何らかの手段で同期の人に料金を支払わせる必要があります。定期的に現金ないしはLINE Payとかでお金をやり取りしても良いですが煩雑です。

そこで考え出したのが「アクセストークンを入力させる」というやり方で、これによってAPIの利用料金は同期の人のクレジットカードに直接請求されるため、私とのお金のやり取りが発生せずに済みます。なお、アクセストークンはサーバー上で保存しておらず、変換処理を行うたびに入力する必要があります*5

技術的に特筆すべきこと

Steepを使ってみた

Rubyに型をつける技術というのが最近できつつあるのですが、それを使ってみました。

github.com

上のレポジトリから例をコピーしてきますが、以下のような型定義ファイルをRubyスクリプトとは別に書いて、テストみたいな感じで型チェックを回すという感じになります。

class Person
  @name: String
  @contacts: Array[Email | Phone]

  def initialize: (name: String) -> untyped
  def name: -> String
  def contacts: -> Array[Email | Phone]
  def guess_country: -> (String | nil)
end

今回書いた型定義ファイルは以下のような感じです(抜粋)。

interface _BoxLocalizer
  def localize: (images: ::Array[Boxboxbox::BinaryImage]) -> ::Array[Boxboxbox::Box]
end

class Boxboxbox::BoxLocalizer::GoogleVisionApiOnline
  @access_token: String
  @max_results: Integer
  @min_percentage: Float
  @max_retry: Integer
  @logger: Logger

  def initialize: (access_token: String, max_results: Integer, min_percentage: Float, max_retry: Integer, ?logger: Logger) -> untyped

  def localize: (images: ::Array[Boxboxbox::BinaryImage]) -> ::Array[Boxboxbox::Box]

  (略)
end

_BoxLocalizer というインターフェースがあって、その実装として Boxboxbox::BoxLocalizer::GoogleVisionApiOnline というのがある、という気持ちです*6。ここでI/Fを切ったのは、Cloud Vision APIを叩くときに叩き方がいくつかあるし、他の物体検出系のサービスもあるので、そこを差し替えられるようにしたかったからです。

github.com

Steepを使ってみた感想は以下です

  • クラスの階層を全て明示した形で書かなきゃいけないので若干見づらい(型定義のネストができない)
  • private methodも型定義書かなきゃいけないのでつらい
  • 標準ライブラリも型定義が存在しないものがあり、自分で書く必要があったりした
  • 型を書いてるとき、Rubyスクリプトファイルと独立した全然関係ないファイルを書くことになるので、地味に虚無な気持ちになる(慣れの問題かも)
  • Rubyのダックタイピングをかなりいい感じに型に落とし込めてるという印象
  • 型がないライブラリを使うときに、自分で適当にシュッと型をつけてしまえるのは結構体験が良かった
  • 型がないライブラリの実装から型定義ファイルを自動生成するやつというのがあって、大体のケースはそれを使うとなんとなく型がついてくれるんだけど、たまになんか無理なやつがあってつらい (concurrent-rubyというgemは無理だった)

Steepを使う際にはruby-jpの#typeチャンネルの皆さんに大変お世話になりました。ありがとうございました。

今後の課題的な

同期の人の要件を満たすソフトウェアは作れたので一旦は満足なんですが、一般に公開したら誰かしら使いたがる人はいる気がしていて、公開したいという気持ちがあります。ただ、いろいろ気になるところがあって公開を見送っています。

例えば現状の実装だと、アップロードされてきたzipファイルはそのまま/tmpディレクトリ以下に書き込まれるようになっており、大容量のファイルが送られてきたり、利用者が増えてきたりすると問題になりそうです。/tmpディレクトリに置くのをやめてS3に上げるのも考えられるんですが、そうなるとファイルサイズに対して従量で料金がかかってくるわけで、テロされてAWS破産みたいな話もあり得るわけです。

あとは、GCPのアクセストークンをサーバーに送信させてるわけですが、これって大丈夫なんだっけみたいな話はあって、僕個人との信頼関係がない人が、ここにアクセストークンを書き込みたいとは思わないでしょう。そもそも、Webでこれをやる必要は一切なくて、Windowsのデスクトップアプリとして公開すれば良いはずです。今回サーバーサイドアプリケーションとして実装したのは、単純に僕にデスクトップアプリを作る技術がないからです。

ただ、デスクトップアプリとして公開する場合も、GCPのアクセストークンの取得手続きは利用者に踏んでもらう必要があり、ユーザーに一定程度難易度の高いタスクを要求することになります。そう考えると、サーバーでその処理をまるっと隠蔽してあげるというのはありうる選択肢で、ユーザーに対して適切に料金を課す手段が見つかってないことが問題なのだと考えることもできます。

というわけで、今後あり得る展開としては

  1. フリーのデスクトップアプリにして一般公開する(アクセストークンは自分で取得させる)
  2. ユーザーに料金を課すスキームを確立し、一般公開する(アクセストークンの取得をさせない)
  3. 需要がないためこれ以上何もしない。現実は非情である。

という感じです。雑にユーザーに課金させるシステムをどなたかご存知でしたら教えてください。よろしくおねがいします。

*1:しかもそれらの設定項目のうち実際に使われるのは極少数であると予想します

*2:この場合、画質の粗さは問題とならず、純粋に画像全体のなかで物体が相対的にどのぐらいの大きさで写っているのかというのが重要っぽい

*3:Excelではなく正確にはCSVファイルを出力している

*4:一般的に、Excelからコピペでデータを貼り付けられるソフトウェアは結構多い

*5:実際にはブラウザの機能で自動入力されるようです

*6:duck typingなので、明示的にinterfaceを実装しなくても、interfaceを満たしていれば実装したことになる

ブログを移転し、ブログを集約するページを作った

ブログを移転した

ブログを移転しました。

https://genya0407.github.io/

Atomフィードもあるので、僕のブログを継続的に読みたい人がいたらRSSリーダーに登録してください*1

https://genya0407.github.io/feed.xml

この新しいブログは、JekyllとGitHub Pagesで作られています。 静的サイトであり広告などもないので、かなり高速にページを表示できています。 PageSpeed Insightsでトップページのスコアを算出すると100点でした。

f:id:threetea0407:20190526214433p:plain
新しいブログのスコア

すでにいくつか記事も書いていますが、記事の作成に関しても不便を感じたことはないです*2

移転の理由

ブログを移転した理由ですが、一番大きかったのははてなブログの表示が遅いということです。 遅いと言っても十分実用可能なレベルの遅さだとは思いますが、動作がキビキビしているとは言えません*3。 気にならない人が大半だとは思いますが、僕は結構気になっていました。

実際、PageSpeed Insightsでスコアを算出しても、33点という低いスコアになります。

f:id:threetea0407:20190526214523p:plain
このブログのスコア

ブログを集約するページ

そういうわけでブログを新しく作り、今のところは満足しているわけなんですが、将来的にはまた別の理由が発生してブログを移転したくなるかもしれません。移転するというか、はてなブログにははてなブログの、Qiita には Qiita の、JekyllにはJekyllのいいところがあるので、使い分けていきたいという気持ちがあります。

しかし、こういうことを繰り返すと、自分の書いた記事が色んな所に分散することになってしまいます。僕は自分の書いた記事の一覧をどこかに持っておきたいので、これは好ましくありません 。

そういうわけで、僕が書いた記事を集約するページを作りました

genya0407's articles

裏ではRSS/Atomのフィードがいくつか登録してあって、これを15分毎にクロールしてページを更新するようになっています。 現状ではこのブログと、新しく作ったブログと、Qiitaのフィードが登録してあって、これらのブログサイトの記事が一覧で表示されます。 新しいブログを開設したとしても、そのブログのフィードを登録すればこのページに表示されるので、記事の分散を防ぐことができます。

まとめ

はてなブログが遅いということに不満を抱き、Jekyllで高速なブログを開設しました。 またそれに合わせて、僕の書いた記事がブログをまたいで一覧表示されるページを作りました。

*1:そういえば2019年の現在にRSSリーダー使ってる人ってどのぐらいいるんですかね。僕は一応使ってますが、周囲の人が使ってる印象があんまりない。

*2:markdownで記事を書いてgithubにpushするとすぐに反映される

*3:これははてな社の努力不足とかそういう話ではなくて、広告を表示したりしているのでその分遅くなるのは仕方がない。

CAMPHOR-についてここらでひとこと言っておくか

京都のIT系学生コミュニティ「CAMPHOR-(カンファー)」に私が出入りするようになったのは2016年の冬なので,足掛け4年ほどCAMPHOR-に関わっていたことになります.

この記事では,CAMPHOR-について説明した後,私がCAMPHOR-に感じた魅力を語ります.

※この記事はポエムです

続きを読む