Slackを愛する者として、社内のスタンプ利用ランキングを知りたい。

# Introduction

## きっかけ
在宅勤務も多くなってきた昨今、チャットやテキストでのコミュニケーションにはスタンプなどを用いて、感情表現する機会が多くなっています。

現職のSlackには、前職と比較して多くのスタンプが使われています。いわゆるカスタム絵文字というものですが、非常にユニークなものも多く毎日新たな発見があります。そこで今回は、SlackのAPIを用いて、どんなスタンプが使われているのかを調査してみました。

なお個人が特定されるような公開できないスタンプ等がある場合は、加工いたします。あらかじめご了承ください。

続編も執筆いたしました！

https://qiita.com/Senple/items/47e45f5e93ba225ddad1


過去に、似たようなことをやっている方が居たみたいです。Slack好きの集まる社風ですね。

https://qiita.com/bakuwarorin/items/1c4ff06c52555b80abea


## 結論
上位10位までの結果はこのようになりました。

![slack_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/3e7d3101-a406-1377-197b-a2b0303b90ae.png)


下記の条件を調査対象としています。
・全ての公開チャンネル：855件。(アーカイブされたものも含む。2021年12月2日時点)
・それぞれのチャンネルの直近1000件の投稿。(Slack APIの上限が1000件のため)[^1]
・集計対象は、リアクションに用いられたスタンプのみ。投稿内に使用されたスタンプは対象外。

調査結果をざっとみた感じですと、挨拶や労いなどのスタンプが多いようです。現職では、それぞれの分報[^2]にて出勤や退勤の際に挨拶を添えて投稿する慣習があります。その投稿に対するリアクションスタンプが上位を占めております。面白いですね。

![slack_greeting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/f79c0ad8-711a-ccc3-c495-96461873165a.png)



## 本日のお品書き
・ SlackのWorkspaceにアプリをインストールする。
・ Slack APIでごっそりとチャット履歴を取得する。
・ ~~色々遊んでみる。~~ Pythonを用いて可視化してみる。

### 対象者
・データ分析初心者の方
・1を聞いて10を理解できるエンジニア
・Slack APIと初めましての方
・**Macユーザー**の人
・Slack権限が一般の方[^3]

### 非対象者
・データ分析ガチ勢
・説明下手な筆者を攻撃しようとするエンジニア
・Slack APIを熟知している方
・**Windowsユーザー**の人
・Slackの管理者権限を持っている方

## 自己紹介
![smile.jpg](https://qiita-image-store.s3.amazonaws.com/0/262742/38a82ae5-98e6-8d78-b461-3d14024af15d.jpeg)

自己紹介ページ

https://qiita.com/Senple/items/b47374e68e4d8d26d0cb

### 環境情報

```
macOS Big Sur ver:11.6
Python 3.8.5
slack-sdk==3.12.0
```

Cf.) Python2系とかPython3系の意味が分からない人へ！
Pythonには2系と3系があって、最近始めた人ならほとんどPython3系だと思います。一応バージョンを確認する方法を記載しておきます。

MacOSに入っているターミナル(Terminal)でPythonのインタラクティブモードを起動すればバージョン情報が表示されます。

```
$ Python
>Python 3.8.5 (default, Sep  4 2020, 02:22:02) 
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.>>>
```
インタラクティブモードを辞めるには`exit()`を入力しましょう！


# Let's Start

今回の記事は、vscodeで実装しています。ただ、どちらのエディタでも問題ないと思います。知らんけど。

## SlackのWorkspaceにアプリをインストールする。
Slackの権限が、管理者と一般によって実装難易度が大きく異なります。
管理者の場合は、Slack APIを用いずともCSVファイル等を用いて分析することができるようです。

管理者権限で、CSVファイルを手に入れられる方は、下記の記事が参考になると思います。

https://techblog.securesky-tech.com/entry/2019/01/08/

自分は一般の権限しか持っていないため、下記の記事の手順ですすめました。下記の記事はRubyで書かれていますが、Slack APIの権限周りは開発言語に依存していないため、問題はない[^4]です。

https://qiita.com/H-Asakawa/items/e56ecaa7ef08062dc8bb#1-user%E5%90%8D%E3%81%8Bchannel%E5%90%8D%E3%82%92%E5%85%A5%E5%8A%9B


### Slackのアプリを作成します。
Slackの公式でアプリを作成して、そのアプリを通じてSlackの各種APIを操作します。
そのためまずはアプリを作成してください。

https://api.slack.com/

`Create an app` をクリックして、アプリを作成してください。

![slack_toppage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/f2640fcd-c90e-d633-a616-aadd136757cd.png)


権限周りは下記の通りに設定しております。[参考記事](https://qiita.com/H-Asakawa/items/e56ecaa7ef08062dc8bb#1-user%E5%90%8D%E3%81%8Bchannel%E5%90%8D%E3%82%92%E5%85%A5%E5%8A%9B)の通りです。 ユーザー別で分析をしない場合は、`users:read` は不要かもしれません。

![slack_appscope.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/0653d3a8-3959-17db-2554-3d77b000b1ea.png)

### トークンを発行します。

トークン(token)が発行されたら、完了です。
自分はWorkspaceにインストールしたら、 `xxxx-****-your-token` みたいな長いトークンが発行されました。アプリを通じてSlack APIを操作するにはこちらのトークンが必須のようです。


## Slack APIを使用する。
参考記事を読み進めていくと、[この辺り](https://qiita.com/H-Asakawa/items/e56ecaa7ef08062dc8bb#21-%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E3%81%A7%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9Fuser%E5%90%8D%E3%82%92userid%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86)でつまづきました[^5]。
色々調査を進めるうちに、APIの名前が統合されたり、名前が変わっている部分も多かったです。さらにSlack APIはSDKを通じて操作する方が簡単そうと判断しました[^6]。

結論としては、Slack SDKを用いてSlack APIを操作することに決めました。

### SlackのSDKをインストールする。

下記の公式SDKの記事がわかりやすかったです。結論から述べると、Slack SDKはpip3でインストールできるようです。

https://qiita.com/seratch/items/8f93fd0bf815b0b1d557#%E3%81%A8%E3%81%AB%E3%81%8B%E3%81%8F%E3%81%AF%E3%81%98%E3%82%81%E3%82%88%E3%81%86


```Bash:
pip3 install slack-sdk
```

インストールしたパッケージが使えているか確認するときは、下記のコードで実行できます。


```Python:api_test.py
from slack_sdk.web import WebClient

client = WebClient()
response = client.api_test()
print(response)
```

```Bash:
python api_test.py 
```

実行すると、`{'ok': True, 'args': {}}`との結果が返ってきます。
OKみたいですね！とりあえず安心です。


## Pythonを用いて可視化してみる。

早速ソースコードの紹介です。

```Python:test.py
from collections import Counter

from slack_sdk.web import WebClient


# トークン
SLACK_API_TOKEN = "xxxx-****-your-token"
# 調査対象のチャンネル
CHANNELS_ID = "your_channels_id"
# 集計結果を送信するチャンネル
POST_ID = "your_post_id"

client = WebClient(token=SLACK_API_TOKEN)

#特定の会話を、APIの上限まで取得する。
response = client.conversations_history(channel=CHANNELS_ID)

all_reaction_list = []
# 会話履歴から、一つの投稿ごとに処理する。
for num, post in  enumerate(response['messages']):
    # リアクションのあったものだけを取り出す。
    if 'reactions' in post.keys():
        all_reaction_list.append(post['reactions'])

sticker_list = []
# 抽出したリアクションから、スタンプの数をカウントする。
for reaction_list in all_reaction_list:
    for reaction_dict in reaction_list:

        max_count = reaction_dict['count']
        count = 1
        while count <= max_count:
            sticker_list.append(reaction_dict['name'])
            count += 1

# スタンプのランキングを作成する。
sticker_rank = Counter(sticker_list).most_common(10)

# スタンプのリストを取得する。
for rank, sticker_row in enumerate(sticker_rank):
    # 自分のDMへ送信する。
    rank_string = str(rank+1) + "位は、:" + sticker_row[0] + ":です。回数は" + str(sticker_row[1]) + "回です。"
    client.chat_postMessage(channel=POST_ID, text=rank_string)
```

特定のチャンネル(自分の分報など)の直近100件の投稿に紐づくリアクションを取得しています。
やっていることを大雑把に解説すると、以下のようなことをしています。
1. 特定のチャンネルの会話履歴を投稿単位に分割する。
2. 投稿に紐づくリアクションのみを抽出してスタンプだけを抽出する。
3. スタンプを多い順に並べて、集計結果をチャンネルに送信する。


この記事の冒頭にあったような処理をするために、まずは単純な構造部分を解説していきます。

>下記の条件を調査対象としています。
・全ての公開チャンネル：855件。(アーカイブされたものも含む。2021年12月2日時点)
・それぞれのチャンネルの直近1000件の投稿。(Slack APIの上限が1000件のため)[^1]
・集計対象は、リアクションに用いられたスタンプのみ。投稿内に使用されたスタンプは対象外。

下記にある通りimportしているものは、スタンプの集計に利用する `collections` と
Slack APIを操るための`slack_sdk.web`です。
トークンなどの値はそれぞれご自身の値をご利用ください。

```Python:
from collections import Counter

from slack_sdk.web import WebClient

# トークン
SLACK_API_TOKEN = "xxxx-****-your-token"
# 調査対象のチャンネル
CHANNELS_ID = "your_channels_id"
# 集計結果を送信するチャンネル
POST_ID = "your_post_id"
```

APIを叩く際に、毎回トークンの情報を入れるのは面倒なので、事前に`client`に格納します。

```Python:
client = WebClient(token=SLACK_API_TOKEN)
```


次に`conversations_history`というAPIを使って、特定のチャンネルから会話履歴を直近100件取得しています。
なお[元々の記事](https://qiita.com/H-Asakawa/items/e56ecaa7ef08062dc8bb#3-%E5%AF%BE%E8%B1%A1%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E3%81%8B%E3%82%89reactions%E5%8F%8D%E5%BF%9C%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97)の中では、`channels:history`というAPIを用いているようですが、`conversations_history`に統合されているようです。

`conversations_history`に含まれる引数はこちらをご覧ください。引数を調整することで、冒頭のように直近1000件を取得することもできます。

https://api.slack.com/methods/conversations.history


```Python:
#特定の会話を、直近100件まで取得する。
response = client.conversations_history(channel=CHANNELS_ID)
```

投稿の中に含まれるスタンプや絵文字を取り除いたり、業務連絡やBotが投稿したようなリアクションのない投稿も含まれています。事前にスタンプの押されなかった投稿を除外します。

```Python:
all_reaction_list = []
# 会話履歴から、一つの投稿ごとに処理する。
for num, post in  enumerate(response['messages']):
    # リアクションのあったものだけを取り出す。
    if 'reactions' in post.keys():
        all_reaction_list.append(post['reactions'])
```

スタンプの押された投稿から、スタンプの情報だけをリストにまとめます。post['reactions']の中身はどのスタンプが何回押されたのか、押したのは誰かという情報が含まれています。
`[{'name': 'スタンプ名A', 'users': ['ユーザー１', 'ユーザー２'], 'count': 2}, {'name': 'スタンプ名B', 'users': ['ユーザー１', 'ユーザー3', 'ユーザー4'], 'count': 3}]`
上記のようになっているので、スタンプの種類と回数だけ抽出して、誰が押したのかという情報を取り除いています。

```Python:
sticker_list = []
# 抽出したリアクションから、スタンプの数をカウントする。
for reaction_list in all_reaction_list:
    for reaction_dict in reaction_list:

        max_count = reaction_dict['count']
        count = 1
        while count <= max_count:
            sticker_list.append(reaction_dict['name'])
            count += 1
```

投稿毎のスタンプの種類と回数を取得できたので、全ての投稿をまとめて、上位10件のみ取得しています。
こういう時は、`Counter().most_common()`が便利です。

https://docs.python.org/3/library/collections.html#collections.Counter

あとは、Slackの`postMessage()`というAPIを使用しています。

https://api.slack.com/methods/chat.postMessage

Slackへ送信することで実際のスタンプの形で表示させることができます。

![slack_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/53f157ca-c441-2a2e-dd14-c144e2989c1f.png)


```Python:
# スタンプのランキングを作成する。
sticker_rank = Counter(sticker_list).most_common(10)

# スタンプのリストを取得する。
for rank, sticker_row in enumerate(sticker_rank):
    # 自分のDMへ送信する。
    rank_string = str(rank+1) + "位は、:" + sticker_row[0] + ":です。回数は" + str(sticker_row[1]) + "回です。"
    client.chat_postMessage(channel=POST_ID, text=rank_string)
```

これで、単純な構造部分の解説が終了です。
わかりづらいところがありましたら、コメントなどをお願いいたします。

### 全てのチャンネルの会話履歴を取得する。

上記のソースコードの中では、特定のチャンネルということで、IDを事前に定義していました。
`CHANNELS_ID = "your_channels_id"`
しかし社内のスタンプ利用ランキングを知るには、特定のチャンネルではなく全てのチャンネルから取得する必要があります。

そこで、公開チャンネルのIDを全て取得します。

https://api.slack.com/methods/conversations.list

チャンネルの名前とIDを取得します。

```Python:
CHANNELS_LIST = client.conversations_list(limit=1000)

public_list = []
for index, row in enumerate(CHANNELS_LIST['channels']):
    row_dict = {
        'name' : row['name'],
        'id' : row['id']
    }
    public_list.append(row_dict)
```

作成した `CHANNELS_LIST` をもとに、先ほども利用した[会話履歴API](https://api.slack.com/methods/conversations.history)にチャンネルIDを渡します。limitはデフォルト値が100件なので、最大の1000を設定しています。
```response = client.conversations_history(channel=CHANNELS_ID, limit=1000)```



### CSVにアップロードする。

実際に作成したランキングをCSVファイルとして、保存しておくと便利です。
チャンネル数は855件もあると毎回毎回実行するのは骨が折れます。大体15分以上の時間がかかりました。[^7]
>下記の条件を調査対象としています。
・全ての公開チャンネル：855件。(アーカイブされたものも含む。2021年12月2日時点)
・それぞれのチャンネルの直近1000件の投稿。(Slack APIの上限が1000件のため)[^1]
・集計対象は、リアクションに用いられたスタンプのみ。投稿内に使用されたスタンプは対象外。

```Python:
import csv

# e. 絵文字のリストを取得する。
with open('emoji.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(sticker_rank)
```

以上のような内容を全てまとめて下記のようなプログラムを動かしていました。
実行結果は、冒頭でも紹介したこちらの画像の通りになりました。

![slack_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/3e7d3101-a406-1377-197b-a2b0303b90ae.png)

```Python:main.py
from collections import Counter
import csv
from datetime import datetime as dt
import time

from slack_sdk.web import WebClient


# トークン
SLACK_API_TOKEN = "xxxx-****-your-token"
# 集計結果を送信するチャンネル
POST_ID = "your_post_id"

START_TIME = dt.now()
client = WebClient(token=SLACK_API_TOKEN)
CHANNELS_LIST = client.conversations_list(limit=1000)

def _get_public_list(CHANNELS_LIST:list):
    """
    Workspaceの全ての公開チャンネルの名称とIDを取得する。

    Parameters
    ----------
    CHANNELS_LIST : list
        ワークスペースに含まれる全ての公開リスト

    Returns
    -------
    _public_list :list
        公開チャンネルの名称とIDを入れたリスト
    """
    _public_list = []
    for i, j in enumerate(CHANNELS_LIST['channels']):
        row_dict = {
            'name' : j['name'],
            'id' : j['id']
        }
        _public_list.append(row_dict)

    return _public_list

def _get_sticker_list(CHANNELS_ID:str):
    """
    任意のチャンネルの会話履歴を取得する。

    Parameters
    ----------
    CHANNELS_ID : string
        会話履歴を取得するChannelのID

    Returns
    -------
    None
    """
    #特定の会話を、APIの上限まで取得する。
    response = client.conversations_history(channel=CHANNELS_ID, limit=1000)

    # 会話履歴から、一つの投稿ごとに処理する。
    for num, post in  enumerate(response['messages']):

        # リアクションのあったものだけを取り出す。
        if 'reactions' in post.keys():
            all_reaction_list.append(post['reactions'])

# 公開チャンネルを格納するリスト
public_list = _get_public_list(CHANNELS_LIST)

# それぞれの投稿についたリアクションをリストに貯めておく。
all_reaction_list = []
for index, row in enumerate(public_list):
    print(index, row)
    if index % 7 == 0:
        time.sleep(1)
    try:
        _get_sticker_list(row['id'])
    except Exception as e:
        print(e, len(all_reaction_list))
        time.sleep(3)
        pass

sticker_list = []
user_list = []

# 抽出したリアクションから、スタンプの数をカウントする。
for reaction_list in all_reaction_list:
    for reaction_dict in reaction_list:

        max_count = reaction_dict['count']
        user_list.append(reaction_dict['users'])

        count = 1
        while count <= max_count:
            sticker_list.append(reaction_dict['name'])
            count += 1

# 上位100件を取得する。
# スタンプのランキングを作成する。
sticker_rank = Counter(sticker_list).most_common(100)

# e. スタンプのリストを取得する。
with open('emoji.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(sticker_rank)

# スタンプのリストを取得する。
for rank, sticker_row in enumerate(sticker_rank):
    # print(index, i)
    # SlackへPostする数、かつterminalに表示するのも10個。
    if rank >= 10:
        break
    # terminal用
    rank_string = str(rank+1) + "位は、:" + sticker_row[0] + ":です。回数は" + str(sticker_row[1]) + "回です。"
    print(rank_string)
    
    # 自分のDMへ送信する。
    client.chat_postMessage(channel=POST_ID, text=rank_string)

END_TIME = dt.now()
print("開始時間：", START_TIME, ". 終了時間:", END_TIME)
time_log = f"開始時間: {START_TIME},  \n終了時間: {END_TIME}"
client.chat_postMessage(channel=POST_ID, text=time_log)
```


# To be Continued
## 終わりに
今後もTwitterでこのようなデータの可視化を発信しています。興味があったらご覧ください。 ~~大抵はくだらないことです。~~

[Sempleのツイッター](https://twitter.com/Semple_smile)

## 大変だったこと
### Slack APIの上限
Slack APIに上限があるらしくて、チャンネル数の多い企業のSlackを分析する場合には、途中でSleepしたり、例外処理を準備しておかないと止まります。

```Bash
slack_sdk.errors.SlackApiError: The request to the Slack API failed. (url: https://www.slack.com/api/conversations.history)
The server responded with: {'ok': False, 'error': 'ratelimited'}
```

自分はエラーを吐いたPCを前にやけ酒していました。[^8]

チャンネル数や投稿に含まれるリアクションの数にも依存すると思います。当然プログラムの実装内容や処理速度等も関係します。効率良いプログラムを描けるようになりたいです。

### times_sendaの悲しき現実
今回の記事を執筆する中で、自分の分報にはどんなスタンプが押されているのかを上位からいくつか調査した結果、愉快なスタンプが入っていました。

![slack_mytruth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/2340ada3-587b-8622-ab13-f098cce646d8.png)

11位をご覧ください。

![slack_slackaddicted.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262742/ffd05eab-91cf-3071-37c4-7680c3d98785.png)

Slack廃人スタンプです。
どなたが作成したものかは知りませんが、自分が入社する前からあったようです[^9]。なかなかランクインするようなスタンプではないと思うので、多分プログラムのミスだと思います。Slack廃人だなんて、決してそんな事実はありません。


## 参考記事

https://api.slack.com/methods/users.list

https://api.slack.com/methods/reactions.list

https://docs.python.org/ja/3/library/time.html?highlight=sleep#time.sleep

https://www.pythontutorial.net/python-basics/python-write-csv-file/


[^1]: 時間の指定や他の方法を駆使すれば、直近1000件以前を遡ることもできます。参考情報は[こちら](https://qiita.com/Senple/items/0e4626307b4559d26bc5#comment-28264ad7a867128d44f6)
[^2]: 個人ごとのチャンネルで、todoを書く人もいれば趣味や体調のことを投稿している人もいます。
[^3]: 管理者権限でない一般の権限という意味です。上手い言い方が思い浮かびませんでした。アイデア募集中です。
[^4]: 嘘です。強がりました。こちらの記事をもとに実装していましたが、Rubyがどうしても理解できず、途中から自己流で実装しています。大まかな流れは、上記の記事をもとにしています。
[^5]: もちろんRubyが読めないからです。本文では責任転嫁していますが、単純にRuby記事を読み進めていった自分に問題があると思います。皆さんは自己を過信しすぎないでください。
[^6]: SDKとかAPIという用語は、きちんと理解せずに使っています。認識違いがありましたら、ご指摘お願いいたします。
[^7]: 自分は結局CSVファイルを利用していません。記録用にCSVを作成していただけです。この辺はfuture workですね。
[^8]: 業務時間外なのでね。
[^9]: 嘘じゃないです。自分が創設者ではないです。ただの後継者です。