ぴろログ

Output Driven

Slack のメッセージ履歴( JSON )を CSV に変換するツールを Go でつくった

情シス向けの問い合わせ用 Slack チャンネルがすごい勢いで流れていきます。(とりあえず聞いてみよう、が多いです。)対策の一環として自動応答するチャットボットを作成中で、ボットで返せそうな定型的な問い合わせを確認しようと当該チャンネルのメッセージ履歴をエクスポートしてみました。が、データが JSON 形式で見づらかったので、 CSV 形式に変換するツールを作成しました。

作成したツール

要件

作成にあたっての要件は以下の通りです。

  • Slack からエクスポートしたメッセージ履歴を、 JSON 形式から CSV 形式に変換してファイルに出力する
  • 必要最低限の情報のみ出力する
    • 対象はメッセージのみとする(添付ファイル等は対象外)
    • スレッド内でのやりとりは、どのメッセージ(どの問い合わせ)に対するものなのか識別できるようにする
    • メッセージの投稿者の名前を表示する
    • メッセージ内のメンション相手(ユーザ名)を表示する
  • 社内の誰もがサクッと使えるようにしたい
    • Windows / Mac の両方で使える
    • ツールの実行環境の準備は不要とする

「同じ事をしたい!」と別チームからも声があがったため、利用する PC の環境に左右されないよう Go でツールを作成しました。

github.com

使い方

Slackのエクスポート機能 でデータをエクスポートし、目的のチャンネル名のディレクトリにツールを配置・実行すると、同一ディレクトリ内に CSV ファイルを出力します。詳細は README を参照してください。

作成された CSV ファイルは Google スプレッドシートにインポートして扱っています。

f:id:pirox07:20200406013302p:plain
スプレッドシートにインポート

ピボットテーブル機能で整形すると、スレッド単位で集約できて文脈が追いやすくなります。

f:id:pirox07:20200406013315p:plain
ピボットテーブル

以下、ツールの作成中に調べたことやハマったことを備忘録として残しておきます。

Slack のエクスポートデータの構造

ワークスペースのデータをエクスポートすると以下のようにデータが出力されます。

 <Workspace Name> Slack export <term>
 ├ user.josn
 ├ channels.json
 ├ integration_logs.json
 ├ channel-01
 │  ├ yyyy-mm-dd.json
 │  └ yyyy-mm-dd.json
 └ channel-02
    └ yyyy-mm-dd.json
 
 ※ フリー / スタンダードプランの場合

Slack をプラスプラン以上で契約していると、 DM や プライベートチャンネル内のメッセージも取得できるようです。

slack.com

本ツールではメッセージ履歴である yyyy-mm-dd.json とユーザ情報の user.jsをインプットとして扱います。

各種ファイルについて丁寧に説明されている記事がありますので、紹介させていただきます。

qiita.com

取得対象のデータ

各ファイルから取得するデータは以下のとおりです。

  • yyyy-mm-dd.json
    • ts : タイムスタンプ( UnixTime )
    • thread_ts : スレッドの ID に相当( 親スレッドの ts の値)
    • user : メッセージを投稿したユーザの ID
    • text : 投稿したメッセージ
  • user.json
    • id : ユーザ ID
    • profile.real_name : ユーザ名(詳細は後述)
    • profile.display_name: ユーザ名(詳細は後述)

tsthread_tsprofile.real_nameprofile.display_name について補足します。

ts

メッセージの投稿時刻のタイムスタンプです。 UnixTime で表現されているので、わかりやすいよう JST に変換して CSV ファイルに出力します。

thred_ts

メッセージがスレッド形式となった場合に生成されます。 親メッセージの tsの値がスレッド内の全メッセージの thred_ts に割り当てられるため、どのスレッドに投稿されたメッセージであるかを識別することができます。

{
    "messages": [
        {
            "type": "message",
            "user": "U061F7AUR",
            "text": "island",
            "thread_ts": "1482960137.003543",
            "reply_count": 3,
            "subscribed": true,
            "last_read": "1484678597.521003",
            "unread_count": 0,
            "ts": "1482960137.003543"
        },
        {
            "type": "message",
            "user": "U061F7AUR",
            "text": "one island",
            "thread_ts": "1482960137.003543",
            "parent_user_id": "U061F7AUR",
            "ts": "1483037603.017503"
        },
        {
            "type": "message",
            "user": "U061F7AUR",
            "text": "two island",
            "thread_ts": "1482960137.003543",
            "parent_user_id": "U061F7AUR",
            "ts": "1483051909.018632"
        },
        ...

conversations.replies method | Slack

Slack の公式ヘルプでも、thread_tsをスレッド ID と表現しています。

If you’re replying to a threaded message, you’ll pass the thread_ts ID of the message you’re replying to.

Basic Usage — Slack Developer Kit for Python

ユーザ名に関する情報

yyyy-mm-dd.json 内ではユーザメンションが <@user_id> と表現されるため、 user.json を使って ID からユーザ名への変換処理を行います。 ユーザ名に関するキーが複数あって何を採用すべきか悩みましたが、「 realname / display_name 」という形式に変換することにしました。

キー
name アカウント登録時のメールアドレスから、ドメイン部分を除外した文字列
real_name プロフィールの [氏名] 欄に登録した文字列( Unicode )
profile.real_name real_name と同一
profile.display_name real_name と同一だが、非ラテン文字は除外される
profile.real_name_normalized プロフィールの [表示] 欄に登録した文字列( Unicode )
profile.display_name_normalize profile.display_name と同一だが、非ラテン文字は除外される
profile.first_name real_name 内のファーストネーム(スペースがデリミタ)
profile.last_name real_name 内のファミリーネーム(スペースがデリミタ)

users.info

~_normalize は非ラテン文字(アルファベット)以外は除外されると公式 に説明がありますが、手元の環境で試してみるとひらがな・カタカナ・漢字も除外されずに出力されました。(カタカナは半角表示になりました。)

Go で JSON をパースする

JSON 形式のデータ取り込む場合、 Go では構造体を定義してパースします。 JSON のキーにマッチするよう構造体のフィールドを定義します。

JSON 形式のデータをペーストすると、 Go の構造体に変換してくれるツールです。
※業務の本番データ等、機密性の高いデータを使わないようご注意ください。

mholt.github.io

users.json の中身をペーストしてみました。

f:id:pirox07:20200405141527p:plain

取り込むデータに含まれる全ての JSON のキーを定義する必要はないため、ツール内では扱いたい対象のみを定義しています。

    type User struct {
      ID      string `json:"id"`
      Profile struct {
          RealName    string `json:"real_name"`
          DisplayName string `json:"display_name"`
      } `json:"profile"`
    }

Go のクロスコンパイル

go build コマンドでプログラム実行に必要なライブラリを含める形でコンパイルし、実行ファイルを生成します。

その際に環境変数の値を変更することで、 Mac 上で Windows 用の実行ファイルにビルドすることができます。具体的にには GOOS ( OS の種類)と GOARCH (アーキテクトの種類) を使います。

今回は make コマンドでビルドしており、 Makefile で以下のように環境変数の値を変更して x86 ベース ( 64bit )アーキテクチャの Widows および macOSで動作する実行ファイルを用意しました。

# Makefile

   build:
        # macOS
        GOOS=darwin GOARCH=amd64 go build -o ./bin/darwin64/parseSlackMessage ./main.go
    
        # Windows
        GOOS=windows GOARCH=amd64 go build -o ./bin/windows64/parseSlackMessage.exe ./main.go

GOOSGOARCH に関する情報はこちらで確認できます。

golang.org

Windows での time.LoadLication 実行エラーへの対応

ts の値を Unixtime から JST へ変換するために time.LoadLocation 関数を使ったのですが、 Windows 環境での実行時にエラーとなりました。

    # loc, err := time.LoadLocation("Asia/Tokyo") 
    Error:The system cannot find the path specified.

Github で Issue がたっていますが、処理中に参照する $GOROOT/lib/time/zoneinfo.zip が( Go をインストールしていない) Windows 環境には存在しないことによるエラーでした。

github.com

Go をインストールすることで実行可能(ファイルが配置される)となるようですが、利用者にその手間を強いるのではなく、外部パッケージをインポートして対応する方が良さそうです。

zoneinfo.zip is a mere 366776 bytes, a small number amongst friends. I think of a typical go binary as a 10MB +/- 5MB affair, and I would happily increase that by 366KB (3% of the mean, but small compared to the standard deviation) to have my binaries be portable to windows.

こちらの記事でも紹介されていた tz パッケージをインストールすることで解決しました。

qiita.com

github.com

    import (
      ...
    
      "4d63.com/tz"
    )
    
    ...
    
    # loc, err := time.LoadLocation("Asia/Tokyo") 
    loc, err := tz.LoadLocation("Asia/Tokyo")

まとめ

  • Slack のエクスポートデータ(チャンネル内のメッセージ履歴)を、 JSON 形式から CSV 形式に変換するツールをつくった
  • Go を使うことで、実行環境の構築が不要でマルチプラットフォーム( Windows , macOS )で動作するようにできた

Go といえばテストですが、テストコードを書かずに作ってしまったので、後追いでテストコードも整備したいと思います。