カスタマーコンサルティングチームの谷口 翔です。
今回はTeamsのWebhookを用いてTreasure Data CDPに蓄積されたデータを可視化、Teamsに投稿することで定期的なチェックを行う体制を構築していきたいと思います。少し古いデータですが、Teamsはグローバルではエンタープライズ企業でのシェアが65%(2019年9月)に達しています。この記事を読まれている方のお勤め先でも導入されているところが多いのではないでしょうか。
今回は以下のステップで進めていきます。
- TeamsでWebhookの登録を行う
- 環境変数への登録を行う
- 投稿コードを記述する
対象者
- Treasure Data CDPでデータの蓄積がある程度完了している
- クエリの実行結果を都度ダウンロードし、Excelやスプレッドシートでチャートや集計表を作成していて工数がかかっている
- BIツールの導入・運用はコスト面で時期尚早だと感じている
- 社内コミュニケーションにTeamsを導入している
Incoming Webhookとは
外部のアプリケーションからTeams内にメッセージを投稿する際に使用する機能です。Slack SDKと同様にPythonから扱いやすくするpymsteamsも提供されていますが、メンテナンスがあまりされておらずWebhookを直接使った場合に対してできることが制限されているため今回は使用しません。また後ほど記載をしますが画像投稿には一部制限があるため注意してください。
詳細はリファレンスサイトを参照してください。
環境変数とは
環境変数とは、シェルから起動されたすべてのプロセスに有効な変数のことです。これだとなんのことかイメージが湧きづらいので今回の記事と関係する点を挙げると、Treasure Data CDPに外部から接続するためのAPI KeyやSlackに接続するためのTokenなどが関係してきます。
上記のような漏洩することが大きな問題になるキー等について、プログラムに直接記述するのではなく環境変数に保存し必要に応じて呼び出すことでセキュリティを高めることができます。今回の実行環境はMac OSのため、Terminalから~/.zshrcにSlack Tokenを書き込んでいます。
実行環境について
- Mac OS 11.5.2
- Python 3.9
- pytd 1.4.0
- pandas 1.3.4
- matplotlib 3.4.3
- seaborn 0.11.2
- slack-sdk 3.13.0
使用するデータについて
使用するデータについては前回の記事と同じものを使用しています。
Incoming Webhookの登録
環境により表示される文言が異なることがあるので注意してください。
- Teamsを起動しアプリの検索から『webhook』を検索し選択します
- 『チームに追加』を選択する
- 『チームまたはチャネルの名前を入力してください』とあるテキストボックスに登録したいチャネルの名前を入力します(例:daily-opportunity等)
- 入力完了後『コネクタを設定』を選択します
- チャネルのアイコンを変更する場合はイメージをアップロードしてください。そうでない場合はそのまま作成を選択してください。
- 作成完了すると、URLが発行されるのでそちらを使用します。外部に漏れないように保管してください。
環境変数への書き込み
先ほどのURLを環境変数に書き込みます。今回はTEAMS_WEBHOOKで書き込みました。環境変数への書き込みについては前回の記事を参照してください。
Base64への変換
Webhookを使用する場合直接画像を投稿することができません。そのため、Base64という形式に画像を変換してテキストで送付を行う必要があります。
画像の圧縮
Teamsで15KB以上の画像をWebhookで投稿すると表示されないため、ファイルの圧縮が必要になります。主に解像度と品質を落として圧縮をおこないますが、圧縮しすぎると視認性が落ちるため場合によってはOneDriveにアップロードするなどの代替手段も検討してください。今回は[Python]画像ファイルを指定サイズ以下まで縮小するの記事の方法を拝借して圧縮をおこないます。
画像の圧縮
関数resize_img_file()内にある14000がファイルサイズを示しています(ここではファイルサイズが14KB以上の場合圧縮をするように指定しています)。また、240については解像度の上限を示しており、解像度の幅が240px以上の場合縮小するように指定しています。base64.b64encodeで圧縮した画像をBase64形式に変換し、requests.postで作成したWebhookのURLに投稿しています。
import pytd import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import os from datetime import datetime from PIL import Image, ImageFilter import imghdr import requests import base64 def resize_img_file(file_path): is_resize = False # ファイルが画像ファイルかどうかを確認し、画像ファイルではない場合リサイズ処理は行わない img_type = imghdr.what(file_path) print(img_type) if img_type is None: return is_resize # ファイルサイズ確認 if int(os.path.getsize(file_path)) >= 14000: is_resize = True img = Image.open(file_path) width = 240 height = img.height * (240 / img.width) resize = img.resize((int(width), int(height))) resize.save(file_path) # 画質のデフォルトは75 quality = 75 while True: if int(os.path.getsize(file_path)) < 14000: break img = Image.open(file_path) img.save(file_path, quality=quality, optimize=False) if quality >= 5: quality -= 10 else: break return is_resize # 指定フォルダ配下にある画像ファイルを処理 def recursive_resize_img_file(file_path): if os.path.isdir(file_path): files = os.listdir(file_path) for file in files: recursive_resize_img_file(os.path.join(file_path, file)) else: resize_img_file(file_path) if __name__ == '__main__': now = datetime.now().strftime("%Y%m%d%H%M%S") client = pytd.Client(apikey=os.environ['SAMPLE_API_KEY'], endpoint='https://api.treasuredata.com/', database='sample_datasets', default_engine='presto') res = client.query('WITH t1 AS ( SELECT symbol ,TD_TIME_FORMAT(time,'yyyy','EST') as year ,MAX(time) as max FROM nasdaq WHERE symbol IN ('MSFT','ADBE') GROUP BY 1,2 ) SELECT nasdaq.symbol ,TD_TIME_FORMAT(nasdaq.time,'yyyy','EST') as year ,nasdaq.close FROM nasdaq INNER JOIN t1 ON nasdaq.symbol = t1.symbol AND nasdaq.time = t1.max ORDER BY 2,1 ') df = pd.DataFrame(**res) filepath = '/Users/kakeru.taniguchi/Pictures/' + now + '_chart.png' webhook = os.environ['TEAMS_WEBHOOK'] sns.lineplot(x="year", y="close", hue="symbol", data=df) plt.xticks(rotation=90) plt.savefig(filepath) plt.show() recursive_resize_img_file(filepath) with open(filepath, "rb") as file: base64image = base64.b64encode(file.read()).decode("ascii") image = "![]" + "(" + f"data:image/png;base64,{base64image}" + ")" data = { "title": "<タイトル>", "text": image } headers = {"Content-type": "multipart/form-data"} requests.post(webhook, json=data, headers=headers)
上記の設定に誤りがなければ、以下の様にTeamsに画像が投稿されていると思います。
おわりに
普段から使用しているコミュニケーションツール(今回の場合はTeams)に自動で投稿されることで、作成者・閲覧者双方の負担を減らしつつ可視化結果を共有できることが確認できたかと思います。日々の変化を簡単に確認できることで、施策効果の確認や改善に繋げられるのではと思います。ただし前述しましたがTeamsでは画像投稿に制限があるので、ExcelをOneDriveに格納してリンクを送るなど代替手段も検討ください。
この記事が少しでも参考になりましたら幸いです。
商標
Excelは、米国Microsoft Corporationの米国およびその他の国における登録商標または商標です。
Teamsは、米国Microsoft Corporationの米国およびその他の国における登録商標または商標です。
Slackは、Slack Technologies, Inc.の登録商標です。
参考書籍
小久保 奈都弥(2020)『データ分析者のためのPythonデータビジュアライゼーション入門 コードと連動してわかる可視化手法』翔泳社.
参考サイト
こはた. “[Python] 画像ファイルを指定サイズ以下まで縮小する”. こはた. 2020-12-27.