読者です 読者をやめる 読者になる 読者になる

Google Apps ScriptでサーバレスのSlack botを作る(調査編)

 私が働いている会社では、社内のコミュニケーションツールとしてSlackを利用している。
slack.com

 Slackはメッセージやグループチャットを綺麗なUIで使えることに加え、Google DriveやDropbox、GitHub等の様々なサードパーティとのサービス連携が豊富であることから、利用事例にもある通り主に北米で多数の利用者を獲得している。

 また、Slackではbotと呼ばれる「プログラムで行動を決められるユーザー」を追加することで、特定の条件下で自動的にメッセージを投稿したりファイルをアップロードしたりすることができる。botについて詳しくはSlackのWebサイトを参照のこと。

 今回は、Google Apps Scriptを使ってWebサイトをスクレイピングし、その結果をSlackに投稿するbotを作成するためにいろいろ調査をした内容を書く。

Google Apps Script (GAS) とは

f:id:hqac:20160126232836p:plain

 Google Apps Scriptとは、Google Apps (Gmail、Googleカレンダー、Googleスプレッドシート等) で利用出来るマクロ機能で、Google Appsの操作をスクリプトで自動化することができる。

 Googleが様々なAPIを提供しており、例えばGoogleスプレッドシートの変更履歴をメールで通知したり特定のキーワードが含まれるメールをピックアップしてスプレッドシートに書き出したりといった操作を自分で記述することができる。

GASの良いところ

無料ですぐに実行できる
  1. Googleスプレッドシートを開く
  2. 「ツール」を開く
  3. 「スクリプトエディタ」を開く

 以上。面倒な開発環境の設定をしたり金を支払う必要は無く、その場ですぐに実行することができる。また、文法は広く利用されているJavaScriptをベースにしているので覚えやすい。

定時実行の設定が簡単にできる

 Linuxにはcronと呼ばれる、決められた時間にコマンドを実行するための仕組みがある。しかし、サーバを借り、初期設定をして、cron jobの設定ファイルを書き、シェルスクリプトの文法を覚え、プロセスがきちんと動いているか監視をする...使い始めるまでが大変だ。

 Google Apps Scriptなら、「トリガー」と呼ばれる仕組みで、GUIからスクリプトの実行日時を簡単に指定できる*1。例えばUrlFetchAppを使い、1分ごとに特定のURLにリクエストを送ってレスポンスコードをチェックし、200以外のコードが返って来たら特定のアドレスにメールを送るといった監視のようなことも簡単にできる。

Googleの様々なサービスを利用するためのAPIが揃っている

 膨大過ぎて書ききれないが、私の想像する限りGoogleのサービスでできそうなことは大体できる。メールの内容を取得したりスプレッドシートやサイトの操作を自動化したり文章を翻訳したり...

GASの悪いところ

Googleの都合で勝手にアップデートされる

 思いつく限りで唯一にして最大の弱点がこれ。過去にも幾つかサポート終了した機能がある。リリースノートを見る限りアップデートが頻繁に行われているので、仕事で使う場合は特に自分が利用している機能が廃止・仕様変更によって動作しなくなることがないか注意した方が良いだろう。

今回つくるもの

 日本取引所グループが公開しているTDnetの適時開示情報閲覧サービスをスクレイピングし、予め指定した会社の開示情報が公開された際にSlackの特定チャンネルへ社名開示情報の表題PDFのURLを投稿するbotを作る。なお、対象となる会社の指定はGoogleスプレッドシートで行うものとする。

f:id:hqac:20160214213331p:plain

 有価証券上場規程に基づき会社情報の開示を行う場合は、必ずTDnetを利用しなければならない*2ため、原則的に全ての適時開示情報は一度このWebサイトに掲載されることになる。

 適時開示情報として公開される情報は有価証券上場規程の第4章第2節以下に記載されており、業績予想の修正・決算短信・資本金の額の減少・子会社の異動・人員削減等、様々な情報を公開することが定められている。開示が求められる情報のリストはこちらをご参照いただきたい。

 ちなみに、有価証券報告書は適時開示に含まれていないが、これは適時開示制度が金融商品取引法に基づく法定開示制度の穴を埋める形で制度設計されていることによるものである。

 なお、TDnetを通じて公開された資料を適時開示情報閲覧サービスで見られる期間は、開示日を含めて31日間(土・日 祝日含む。)である。

アプリケーションの仕様

 GASでスクレイピングをするにはXMLServiceを、HTMLを取得したりSlackへ通知したりするためには、UrlFetchAppクラスを使うのが良いらしい。以下のようなロジックで作ればよさそうだ。

  1. fetch()を使ってGASから対象のWebサイトにHTTPリクエストを送る
  2. getContentText()を使ってHTTPResponseからHTMLを取り出す
  3. 取り出したHTMLから、XMLDocumentクラスのオブジェクトを作る
  4. getRootElement()を使ってXMLのルートノードを取得する
  5. 予め用意したスプレッドシートに記載されているコードと合致する会社をgetChildren()等で探し、以下の情報を取得する
    • 開示された時間
    • 社名
    • 開示情報のタイトル
    • 開示情報PDFへのリンク
  6. SpreadSheetクラスのappendrow()等を使って取得した情報をスプレッドシートに書き込む*3
  7. スプレッドシートに書き込まれた情報をgetSheetValues()等で取得してリストにまとめる
  8. JSON.stringfy()メソッドを使いJSONを生成する
  9. JSONを付してSlackのIncoming Webhooks(後述)にPOSTリクエストを送る

スプレッドシートを準備する

 最初に、Googleスプレッドシートで下記2枚のシートを作成する。
 1枚目のシートの情報を基にスクレイピングを行い、2枚目のシートに結果を出力することにした。

1枚目"targets":
  • code: 証券コード
  • company_name: codeに対応する会社名

f:id:hqac:20160123125506p:plain

2枚目"log":
  • title: 開示情報のタイトル
  • link_to: 東証のリリースPDFへのリンク
  • d_datetime: 適時開示情報閲覧サービスに開示された日付と時間
  • post_status: "Done" / "Not yet"

f:id:hqac:20160123125516p:plain

取得できるHTMLの内容

 スプレッドシートを作成したら[ツール] > [スクリプトエディタ]からGoogle Apps Scriptを書く。まずは、適時開示情報閲覧サービスからどのようなHTMLが取得できるかを確認する。

function test() {
  var response = UrlFetchApp.fetch("https://www.release.tdnet.info/inbs/I_main_00.html");
  Logger.log(response.getContentText()); 
}

メニューの[表示] > [ログ]からLogger.log()で出力したログを確認できる。
f:id:hqac:20160213095715p:plain

iframeについて

 休日は情報が開示されないので、もし休日に試す場合はURLを下記のように書き換えるとよい。
 ”https://www.release.tdnet.info/inbs/I_list_001_20160210.html

 適時開示情報閲覧サービスではiframeを使って日付ごとに別のURLからHTMLを取得して表示しており、日付部分を変更することで過去分のHTMLを取得することができる。なお、指定する年月日は平日にすること(20160210の部分を自分が取得したい年月日に変更すると良い)。下記の画像のiframe部分を見ると、別の場所からHTMLを取得していることが分かる。
f:id:hqac:20160211104536p:plain

Slackの設定

 次に、Slackで投稿先のチャンネルを作成し、Incoming WebhooksというAPIの設定を行う。Incoming Webhooksを使うと、Slackが発行するURLへPOSTした際にSlackの特定のチャンネルもしくはユーザーにメッセージを投稿することができる。
 設定はこちらから通知したいチャンネルを選ぶだけ。発行されたWebhook URLは投稿に必要なのでメモしておくこと。
f:id:hqac:20160211114434p:plain

問題発生

 XmlServiceを使おうとしたら下記のエラーが表示された。残念なことに、どうやらXmlServiceはHTMLには使えないらしい。HTMLにはlinkなど閉じなくてもいいタグがあるが、これはXMLでは許されていない書き方で、これが原因でHTMLをXmlServiceを使ってパースすることは諦めざるを得なかった。
f:id:hqac:20160213144156p:plain

Kimono

 問題を調べる過程でStackOverflowというWebサイトを見てKimonoという便利なWebサービスがあることを知る。
f:id:hqac:20160213145621p:plain
Ericさんありがとう。

 Kimonoはブラウザの拡張機能として追加することで使えるようになり、webサイトのスクレイピングしたい要素をクリックするだけでその要素を抽出してJSON、RSSまたはCSVとして取得することができるAPIを備えている。詳しくは下記のQiitaの記事が参考になる。
kimonoの使い方 基礎編 - Qiita

f:id:hqac:20160214203439p:plain

 言葉で説明するのは難しいが、下記のチュートリアル動画を見れば使い方は直感的にわかるだろう。
vimeo.com

 これを使えばHTMLのタグを辿らなくても良さそうだが、問題が1つある。Kimonoではクローリングの間隔を指定できるのだが、最短でも15分となってしまうのだ。

 クローリングの間隔は2016年2月14日現在で7種類あり、手動、15分、30分、1時間、1日、1週間、1ヶ月となっている。そのため、間隔を15分より短くしたい場合は、Kimonoは選択肢から外れることになる。

 なお、Kimonoの無料プランではクロールの結果が1回分しか保存されないため、GAS + Kimonoでスクレイピングする際はUrlFetchAppと時間主導型トリガーで定期的にAPIにアクセスしてスプレッドシートに結果を保存するとよい。(参考記事)。

正規表現との出会い

  • XmlService → validなXMLにしか使えない(そして一般的なHTMLはvalidでない)
  • Kimono → 無料だとクローリングの間隔等に制約がある

 XmlServiceもKimonoも不十分ということで、別の方法を探すことにした。

 そして、HTMLファイルを取得した後にどうやって目的のタグと中身を抜きだせばよいかを調べていたところ、正規表現(regular expression)を使うという方法があることを知った。

 正規表現はメタ文字を使って文字列のパターンを記述する式で、指定したパターンを持つ文字列を抽出することができる。これを使えば、HTMLから目的のタグを抽出することができそうだ。

 ただし、+や\や*や^等の記号がたくさん出てくるので何が書かれているのか全くわからない。これは勉強する必要がある。
正規表現サンプル集
f:id:hqac:20160214211920p:plain

 記事が長くなってしまったので、正規表現の勉強とGASでの実装についてはまた次回。
-

*1:「スプレッドシートが編集されたとき」という指定も可能

*2:有価証券上場規程第414条 会社情報の開示の方法

*3:直接Slackに投稿せず一度スプレッドシートに書き込むのは、通知されたかどうかのステータスをスプレッドシートで管理するため