24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ハンズラボAdvent Calendar 2021

Day 4

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

Last updated at Posted at 2021-12-04

Introduction

きっかけ

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

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

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

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

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

結論

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

slack_result.png

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

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

slack_greeting.png

本日のお品書き

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

対象者

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

非対象者

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

自己紹介

smile.jpg

自己紹介ページ

環境情報

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ファイルを手に入れられる方は、下記の記事が参考になると思います。

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

Slackのアプリを作成します。

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

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

slack_toppage.png

権限周りは下記の通りに設定しております。参考記事の通りです。 ユーザー別で分析をしない場合は、users:read は不要かもしれません。

slack_appscope.png

トークンを発行します。

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

Slack APIを使用する。

参考記事を読み進めていくと、この辺りでつまづきました5
色々調査を進めるうちに、APIの名前が統合されたり、名前が変わっている部分も多かったです。さらにSlack APIはSDKを通じて操作する方が簡単そうと判断しました6

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

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

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

pip3 install slack-sdk

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

api_test.py
from slack_sdk.web import WebClient

client = WebClient()
response = client.api_test()
print(response)
python api_test.py 

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

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です。
トークンなどの値はそれぞれご自身の値をご利用ください。

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に格納します。

client = WebClient(token=SLACK_API_TOKEN)

次にconversations_historyというAPIを使って、特定のチャンネルから会話履歴を直近100件取得しています。
なお元々の記事の中では、channels:historyというAPIを用いているようですが、conversations_historyに統合されているようです。

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

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

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

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': ['ユーザー1', 'ユーザー2'], 'count': 2}, {'name': 'スタンプ名B', 'users': ['ユーザー1', 'ユーザー3', 'ユーザー4'], 'count': 3}]
上記のようになっているので、スタンプの種類と回数だけ抽出して、誰が押したのかという情報を取り除いています。

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()が便利です。

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

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

slack_test.png

# スタンプのランキングを作成する。
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を全て取得します。

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

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にチャンネル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
・集計対象は、リアクションに用いられたスタンプのみ。投稿内に使用されたスタンプは対象外。

import csv

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

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

slack_result.png

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のツイッター

大変だったこと

Slack APIの上限

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

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

11位をご覧ください。

slack_slackaddicted.png

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

参考記事

  1. 時間の指定や他の方法を駆使すれば、直近1000件以前を遡ることもできます。参考情報はこちら 2 3

  2. 個人ごとのチャンネルで、todoを書く人もいれば趣味や体調のことを投稿している人もいます。

  3. 管理者権限でない一般の権限という意味です。上手い言い方が思い浮かびませんでした。アイデア募集中です。

  4. 嘘です。強がりました。こちらの記事をもとに実装していましたが、Rubyがどうしても理解できず、途中から自己流で実装しています。大まかな流れは、上記の記事をもとにしています。

  5. もちろんRubyが読めないからです。本文では責任転嫁していますが、単純にRuby記事を読み進めていった自分に問題があると思います。皆さんは自己を過信しすぎないでください。

  6. SDKとかAPIという用語は、きちんと理解せずに使っています。認識違いがありましたら、ご指摘お願いいたします。

  7. 自分は結局CSVファイルを利用していません。記録用にCSVを作成していただけです。この辺はfuture workですね。

  8. 業務時間外なのでね。

  9. 嘘じゃないです。自分が創設者ではないです。ただの後継者です。

24
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?