定期的にブラウザのタブを再読み込みしてスクリーンショットをSlackに投稿するシェルスクリプト

<!-- 定期的にブラウザのタブを再読み込みしてスクリーンショットをSlackに投稿するシェルスクリプト -->

[Linux Advent Calendar](http://qiita.com/advent-calendar/2016/linux)をご覧の皆様、初めまして。Piroといいます。普段はJavaScriptやRubyでのソフトウェア開発やFirefoxの法人向け技術サポートなどをしつつ、[日経Linux誌][日経Linux]で[シス管系女子][]という連載マンガを描かせて頂いております。

連載の方で書くには粒度の大きい話題だったので公開するのにいい場所はないかなあと思っていたらLinux Advent Calendarという名前を見かけて、まだ空きがあったので「これや！」と思って勇んでエントリーしたのですが、埋まってみると皆さん当たり前ですがカーネルの話中心で、そんな中で一人だけディストリビューションより上のレイヤの話でなんかほんとごめんなさい……シェルスクリプトアドベントカレンダーとかの存在を知ったときにはもう後の祭りでして……

そんな感じで空気まるで読めてない内容ですが、生暖かい目で見て頂けましたら幸いです。

## GUIアプリを制御するシェルスクリプト

[先日参加したイベント](http://geekwomenjapan.github.io/conference2016/)の懇親会で、「シス管系女子」の本をご覧になった方から「Google Analyticsのグラフを何分間隔とかでスクリーンショットとってSlackに流したいんだけど、本にはそういう話は書かれてなかった……」というご相談を頂きました。
![haa.png](https://qiita-image-store.s3.amazonaws.com/0/61387/938e0194-bbcd-428a-110c-098fff95af21.png)

本の中では主にSSHでLinuxのサーバーにリモート接続してコマンドで操作する時の話を取り扱っているため、GUIアプリの話はあまり、というか全然取り扱っていません。しかし、何かしら方法はありそうな気がします。

という話を[つぶやいた](https://twitter.com/piro_or/status/794175311940227074)ところ、[フォロワーの方にヒントを教えて頂けて](https://twitter.com/t2saras/status/794188238156009472)、最終的にそれらしいことを実現できる筋道が立ちました。なのでこの場を借りて、得られた知見を共有したいと思います。


### 基本的な考え方

「Google Analyticsのグラフを何分間隔とかでスクリーンショットとってSlackに流したい」という目標を一気に実現するのではなく、細かいステップに分けてそれぞれの実現可能性を考えてみます。

 1. Google Analyticsのグラフを表示する。
 2. ページを再読み込みする。
 3. スクリーンショットを画像ファイルとして保存する。
 4. 画像ファイルをSlackに投稿する。
 5. 一定時間後に同じ処理を繰り返す。

順番に見てみましょう。

### Google Analyticsのグラフを表示する

実際にGoogle Analyticsのグラフのページを表示してみた所、単純にページを再読み込みすれば最新の状態を表示できました。

ここを自動化する方法もありそうなのですが、とりあえず、最初の1回を表示するだけだったら手動でやってもいいかなと割り切って、今回は手動で行く事にします[^firststep]。

[^firststep]: この最初の1回の手間が辛い、という状況になってきたら初めて自動化を考えましょう。「8割くらいできてるならそれでよしとする」みたいなこういう割り切りも、時には大切です。

ただ、普段使いの環境で後ろでブラウザを動かしておくというわけにはいきません。VirtualBox等の仮想化ソリューションを使って仮想マシンの中にUbuntuを入れるなどして、「スクリーンショット取得用のブラウザとシェルスクリプトだけが動作するデスクトップ環境」を用意しておくことにしましょう。

これを踏まえて、ここから先の話は以下の前提で進めます。

 * 実行環境はUbuntu 16.04LTSを使う（最新の長期サポート安定版なので）
 * ブラウザはFirefoxを使う（Ubuntuで最初から入っているのがこれなので）

### ページを再読み込みする

「今開いているページを再読み込みする」というブラウザの機能を直接呼び出すのは、アドオンや拡張機能を開発する必要があるという事になって大変そうです。

でももっと単純に、位置を指定してツールバー上の更新/再読み込みボタンをクリックしたことにするとか、キーボードショートカットが入力されたことにするとかができるなら、それで用は足せそうな気がします。Firefoxなら、Ctrl-Rを入力できればそれでOKです。

日本語で「linux シェルスクリプト キーボードショートカット 自動入力」という感じに検索してみたのですが、シェルスクリプトの中で確認のため対話式の入力を求めてくるコマンドに「yes」と自動入力する方法や、キーボードショートカットを入力したら任意のスクリプトを実行するようにする方法は出てくるものの、GUIアプリを操作するシェルスクリプトについての情報はすぐには見つかりませんでした。

そこで探索範囲を広げて、英語で「linux shellscript keyboard shortcut input automatic」といったキーワードで検索してみた所、[まさにそういう質問をしている人がいました](http://unix.stackexchange.com/questions/14879/how-to-inject-keystrokes-via-a-shell-script "linux - How to inject keystrokes via a shell script? - Unix &amp; Linux Stack Exchange")。寄せられた回答を見てみると、`xdotool`というコマンドを使うといいようです。

ということで改めて`xdotool`で検索し直してみると、今度はそれらしい情報がたくさん出てきました。

このコマンドは、LinuxでGUI環境の根底にある「X Window System」という仕組み[^X Window System]に作用してGUIアプリのウィンドウを操作するための物で、任意のキーストロークを生成して送る事ができるようです。
![x.png](https://qiita-image-store.s3.amazonaws.com/0/61387/a44cdb03-6ca8-ca63-3669-1bc2aca6583d.png)

[^X Window System]: Fedora 25ではXからWaylandという別のWindow Systemに移行するとのことですし、他にも脱Xするディストリビューションが増えてくるようなので、この辺の話は将来的には通用しなくなってしまうかもしれません……

早速試してみましょう。Ubuntuでは`xdotool`は同名のパッケージでインストールできます。

~~~bash
$ sudo apt install xdotool █
~~~

このコマンドは、操作対象のウィンドウをIDで指定します。どのウィンドウがどのIDなのかは、サブコマンドの`xdotool search`で調べられます。Firefoxのウィンドウであれば、以下の要領です。

~~~bash
$ xdotool search --onlyvisible --name Firefox
1234567
$ █
~~~

このIDを指定してFirefoxのウィンドウを選択し、Ctrl-Rのキーボードショートカットを入力するには、以下のようにすればよいようです。

~~~bash
$ xdotool windowfocus --sync 1234567; xdotool key ctrl+r █
~~~

`xdotool windowfocus`がウィンドウを選択するサブコマンドで、`xdotool key`がキーストロークを入力するサブコマンドです。調べてみた所、`xdotool key`にはウィンドウを直接指定するためのオプションがあるようなのですが、実際にはどこかのレイヤで「バックグラウンドにあるウィンドウではキー入力を拾わない」といった処理が働いているのか、これはほとんど使えないようです。実際、フォーカスされていないウィンドウに直接キーストロークを送ってみても期待通りには動いてくれませんでした。なので、まずウィンドウにフォーカスして、その後で最前面のウィンドウに対してキーストロークを送るという形にしています。

最初のウィンドウIDを取得する部分をコマンド置換で自動化すると、シェルスクリプトに書く時は以下のようになります。

~~~bash
#!/bin/bash

id=$(xdotool search --onlyvisible --name Firefox)
xdotool windowfocus --sync $id
xdotool key ctrl+r
~~~

もうちょっと気を利かせて、ウィンドウのフォーカスをFirefoxに移した後、元のウィンドウにフォーカスを戻すようにすると、以下のようになります。（ここではさらに、現在アクティブなウィンドウのIDを取得する`xdotool getactivewindow`サブコマンドを使っています。）

~~~bash
#!/bin/bash

lastid=$(xdotool getactivewindow)
id=$(xdotool search --onlyvisible --name Firefox)
xdotool windowfocus --sync $id
xdotool key ctrl+r
# 最後にフォーカスを戻す
xdotool windowfocus --sync $lastid
~~~

ということで、無事に「ページを再読み込みする」を自動化できました。次のステップに進みます。

### スクリーンショットを画像ファイルとして保存する

「今開かれているウィンドウのスクリーンショットを取って、画像として保存する」。この方法を自分は知らなかったのですが、フォロワーの方に、[ImageMagick](http://imagemagick.org/)でできるということを教えて頂けました

ImageMagickは画像の加工をコマンド操作で行うために使われる伝統的なツール集で、これに含まれる`import`というコマンドを使うと、前述のX Window Systemの仕組み[^X Window System]を使ってウィンドウのスクリーンショットを画像ファイルとして保存できます。

ということでImageMagickもインストールします。

~~~bash
$ sudo apt install imagemagick █
~~~

`import`コマンドは、先程のX Window SystemのウィンドウIDを`-window ウィンドウのID`と指定して、保存先のファイルのパスを与えると、そのウィンドウのスクリーンショットを指定のパスに保存します。画像の形式はファイル名から自動的に決定してくれますので、「screenshot.png」と指定すればPNG形式になります。

早速試してみます。

~~~bash
$ import -window 1234567 /tmp/screenshot.png
$ ls /tmp/screenshot.png
screenshot.png
$ █
~~~

`import`コマンド自体は何もメッセージを出しませんが、指定したパスにはちゃんと画像が保存されていました。内容もバッチリです。

ということで、これを先程のシェルスクリプトに組み込みます。

~~~bash
#!/bin/bash

lastid=$(xdotool getactivewindow)
id=$(xdotool search --onlyvisible --name Firefox)
xdotool windowfocus --sync $id
xdotool key ctrl+r
xdotool windowfocus --sync $lastid

# ページの読み込みが終わるのを待ってからスクリーンショットを保存する
sleep 10
import -window $id /tmp/screenshot.png
~~~

ページの再読み込み直後にスクリーンショットを取ると読み込み中の真っ白の画面になってしまうので、ここでは`sleep`で10秒待つようにしています。何秒待てばいいかは環境次第なので、うまくいかない時は適宜変更してください。

### 画像ファイルをSlackに投稿する

Slackをコマンドで操作するソフトウェアはいくつかあるようですが、使い方が簡単そうだったので、ここでは[slackcat](https://github.com/rlister/slackcat)を使うことにします。以下のようにしてインストールできます。

~~~bash
$ sudo apt install ruby ruby-dev
$ sudo gem install slackcat █
~~~

slackcatから投稿するにはAPIトークンを発行する必要があります。[やり方の解説記事](http://qiita.com/rubytomato@github/items/6558bfdb37d982891c09)を見てAPIトークンを取得しておきましょう。

APIトークンを取得できたら、早速投稿してみます。slackcatの説明書きによると、以下のようにAPIトークン、チャンネル名、画像のパスを指定すればいいようです。

~~~bash
$ export SLACK_TOKEN=<APIキー>
$ export SLACK_CHANNEL=<チャンネル名>
$ slackcat --multipart /tmp/screenshot.png █
~~~

ということで、Slackへの画像の投稿も自動化できました。これもスクリプトに組み込みましょう。

~~~bash
#!/bin/bash

export SLACK_TOKEN=<APIキー>
export SLACK_CHANNEL=<チャンネル名>
tmp_file_path=/tmp/screenshot.png

lastid=$(xdotool getactivewindow)
id=$(xdotool search --onlyvisible --name Firefox)
xdotool windowfocus --sync $id
xdotool key ctrl+r
xdotool windowfocus --sync $lastid

sleep 10
import -window $id $tmp_file_path
slackcat -multipart $tmp_file_path
~~~

画像のパスを何回も書くのは煩雑なので、シェル変数で最初にまとめて定義しています。


### 一定時間後に同じ処理を繰り返す

Linuxで定期的に処理を実行するといえば`cron`なのですが、今回はブラウザを手動で起動しておく事にしたので、ブラウザを起動していようがいまいが動作してしまうcronjobだとちょっと都合が悪い気がします。

「正確に何時何分頃に処理が動いて欲しい」というわけでなく「だいたい何分間隔で処理が動いて欲しい」ということであれば、`while`と`true`の組み合わせで無限ループにしてその中で処理を繰り返すのが手っ取り早いでしょう。
![loop.png](https://qiita-image-store.s3.amazonaws.com/0/61387/ff27f4ee-d423-06a9-7ee5-b15f957281d4.png)

whileループの中が長くなると読みにくいので、まず「ページを再読み込みしてスクリーンショットを投稿する」という一連の処理を関数にすることにします。

~~~bash
#!/bin/bash
...

post_screenshot() {
  lastid=$(xdotool getactivewindow)
  id=$(xdotool search --onlyvisible --name Firefox)
  xdotool windowfocus --sync $id
  xdotool key ctrl+r
  xdotool windowfocus --sync $lastid

  sleep 10
  import -window $id $tmp_file_path
  slackcat -multipart $tmp_file_path
}
~~~

そして、「この関数を実行して、その後10分待つ」という処理を`while true`で無限に繰り返すようにします。

~~~bash
#!/bin/bash
...
post_screenshot() {
  ...
}

while true
do
  post_screenshot
  sleep 10m
done
~~~

`while true`の無限ループは自分で止めるまで動き続けますので、止めたい時はCtrl-Cで強制終了して下さい。


### できあがったスクリプト

ということで、最初にGoogle Analyticsのページを開く以外の事については無事一通りのことを自動で行えるようになりました。

まとめると、必要なソフトウェアは以下の手順でインストールできます。

~~~bash
$ sudo apt install xdotool imagemagick ruby ruby-dev
$ sudo gem install slackcat █
~~~

完成したスクリプト全体は以下の通りです。

~~~bash
#!/bin/bash

export SLACK_TOKEN=<APIキー>
export SLACK_CHANNEL=<チャンネル名>
target=Firefox
wait_after_reload=10
interval=10m
tmp_file_path=/tmp/screenshot.png

post_screenshot() {
  lastid=$(xdotool getactivewindow)
  id=$(xdotool search --onlyvisible --name $target)
  xdotool windowfocus --sync $id
  xdotool key ctrl+r
  xdotool windowfocus --sync $lastid

  sleep $wait_after_reload
  import -window $id $tmp_file_path
  slackcat -multipart $tmp_file_path
}

while true
do
  post_screenshot
  sleep $interval
done
~~~

後でブラウザを変えたくなった時や、投稿の間隔を変えたくなった時に分かりやすいように、これらも変数で最初に定義するようにしてみました。


## 発展：部分部分を差し替えてみる

ところで、「ブラウザで特定のページを開いて自動で再読み込みして……」という点に着目すると、Seleniumのようなブラウザ自動操作用のツールだったり、ヘッドレスのブラウザだったりを使った方が良いのでは？と思う方もいらっしゃるかもしれません。実際、自分は試したことがないですが、それらを使えばもっとスマートに目的を達成できるのかもしれません。

ただ、今回のように各手順ごとに分けて方法を確立しておくことにもメリットはあります。それは、*個々の手順を別の物に差し替えやすい*という事です。

例えば、「FirefoxでGoogle Analyticsのグラフを」という所を「Thunderbirdで表示したカレンダー」や「LibreOffice表示したワークシート」に変えるということもできます。

「Ctrl-Rでページを再読み込み」という所も、別のショートカットに変えたり、2つ3つとキーボード操作を重ねたりして、もっと複雑な操作にする事ができるかもしれません。

最後の「Slackに投稿」という部分も、別のサービスに変えられます。例えばTwitterであれば、[twurlというツールを使って画像付きのツイートを投稿できます](https://github.com/twitter/twurl/issues/58)。

必要なソフトウェアのインストール：

~~~bash
$ sudo apt install jq
$ sudo gem install twurl █
~~~

スクリプト内に書く、画像をツイートする処理：

~~~bash
upload_result="$(twurl -H upload.twitter.com -X POST "/1.1/media/upload.json" --file $tmp_file_path --file-field "media")"
media_id=$(echo "$upload_result" | jq .media_id)
twurl "/1.1/statuses/update.json" -d "media_ids=$media_id&status=New Screenshot at $(date +%Y-%m-%d-%H-%M-%S)"
~~~

（`twurl`は、使い始める前に初期設定が必要です。詳しくは[リポジトリの説明書き](https://github.com/twitter/twurl/)を参照してください。）

どうでしょう。色々と夢が広がってきませんか？

## まとめ：シェルスクリプトはこう考えよう

「長々語ってるけど、これLinuxの話じゃなくてシェルスクリプトの話じゃね？」と思った方、鋭い！ いや、でも冗談抜きでこういったシェルスクリプトにはLinuxの、というかUNIXの哲学が表れていると思うのです。

Linuxでは*多機能な万能のツールを使いこなすのではなく、単機能のコマンドをパイプラインやリダイレクトなどの仕組みで組み合わせて柔軟に対応するという考え方（UNIX哲学）*がメジャーです。各ツールも、そのような使い方を前提として他のツールと連携しやすいように開発されている場合が多いです。

「大きな目的を一発で実現する万能のツール」や「まさに自分のやりたい通りのことをやっている事例」を探しても、そのものズバリの答えが見つかる事は多くないです。しかし、やりたい事を細かいステップに分けて個々に探せば、部品ごとの答えが見つかる可能性は高まります。あるいは、他の人が書いたこのような記事の中の一部分だけを参考にすることもできるでしょう。

 * やりたい事を細かいステップに分ける。
 * 1つ1つのステップについて、実現する方法を調べる。
 * それを最後に1つにまとめ上げる。

皆さんもぜひ、このやり方で身近な問題を解決していきましょう！
![thumbsup.png](https://qiita-image-store.s3.amazonaws.com/0/61387/102129cc-ff07-40b7-f862-fc4bed52fb9f.png)


## 「シス管系女子」について

最後に[シス管系女子][]の事も改めてご紹介します。

自分は2011年から[日経Linux][]誌上で「シス管系女子」というケーススタディ形式の解説マンガ記事を連載させて頂いています。

「普通にマンガを読んでいたら、いつの間にか大事な事が頭に入っていって、いざ現場で課題に遭遇した時に『あっ、これゼミでやったやつだ！』とスラスラ進めるようになる。コマンド列をどっかのブログからコピペしなくても、自分でゼロから組み立てられるようになる。」

そんな内容を目指して、*「長く使える枯れた技術を、きちんと理解して身に付ける」*をモットーに制作しています。幸いなことに、コマンド操作に苦手意識のある新人の方への教材としてもご好評を頂けているようです。もし良かったら、お近くの大きめの書店で[シス管系女子][]の本や[掲載誌][日経Linux]を手に取ってチラ見して頂けましたら幸いです。

それ以外にも、[Twitterのみんとちゃんbotアカウント][みんとちゃんbot]でイラスト[^イラスト]や本編に入りきらなかった小ネタを流したり、Webサイトの方にも[連載や本では扱わなかったもっと基礎的な話の特別編](http://system-admin-girl.com/comic/#begins)を置いていたりします。あと、[「シス管系女子」をテーマにしたAdvent Calendar](http://www.adventar.org/calendars/1692)も公開中です。

[^本稿執筆時点]: 2016年12月3日現在。
[^イラスト]: [これまでのイラスト一覧](http://system-admin-girl.com/comic/#illust)

ということで、Linux Advent Calendarの3日目でした。4日目は[moriwakaさん](http://qiita.com/moriwaka)の記事です。お楽しみに！



[シス管系女子]:http://system-admin-girl.com/
[みんとちゃんbot]:https://twitter.com/sysadgirl_mint
[日経Linux]:http://itpro.nikkeibp.co.jp/linux/