slackに投稿されたメッセージを用いて、キーワード抽出・分析をしたい計画

## はじめに
イベント等のアイデア出しをするSlackチャンネルがあるのですが、
投稿されたメッセージを組み合わせて新たなアイデアを創造したいなあ。というわけで。

### slackに投稿されたメッセージを用いて、キーワード抽出・分析をしたい計画
以下の段階で完成させようと思います。
一：まずは投稿メッセージを取得しよう
二：キーワード抽出をしよう
三：分析・可視化しよう

~~API,エクスポート機能を使わない理由は、ワークスペース管理者へ連絡をするのが億劫だからです。~~


# 一：まずは投稿メッセージを取得しよう
## 完成したもの
↓をコピペしてプログラムを実行すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505609/b7c40ced-4c49-b1fd-3ac8-d01c5a5bf0c0.png)
<details><summary>こうなります。</summary><div>

```json:out
{
    "value": {
        "poster": {
            "value": {
                "name": "利用者A",
                "status": ":庭付き家:"
            }
        },
        "post": {
            "value": {
                "time": "15:16",
                "massage": "ああ @利用者A ああ  @利用者Aa @利用者A a"
            }
        },
        "reply": {
            "value": {
                "count": 0
            }
        },
        "reaction_array": [
            {
                "value": {
                    "name": ":スマイリー:",
                    "count": 1
                }
            },
            {
                "value": {
                    "name": ":okマーク:",
                    "count": 1
                }
            }
        ]
    }
}
{
    "value": {
        "poster": {
            "value": {
                "name": "利用者A",
                "status": ":庭付き家:"
            }
        },
        "post": {
            "value": {
                "time": "15:26",
                "massage": "これはテストです （編集済み） "
            }
        },
        "reply": {
            "value": {
                "count": 0
            }
        },
        "reaction_array": []
    }
}
{
    "value": {
        "poster": {
            "value": {
                "name": "利用者A",
                "status": ":庭付き家:"
            }
        },
        "post": {
            "value": {
                "time": "13:08",
                "massage": "ああ"
            }
        },
        "reply": {
            "value": {
                "count": 0
            }
        },
        "reaction_array": []
    }
}
```
</div></details>

## 実装

基本的にはslackアプリケーションにて全体コピー`Alt+A` → `Ctrl+C`をした結果を見て、
調整しただけです。
例えばこのような投稿をコピペすると以下のような文字列でした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505609/68659780-0b6d-283c-7af3-ebbf38a7379f.png)

```
利用者A
:庭付き家:  15:16
ああ
@利用者A
 ああ
 
@利用者A

a
@利用者A
 a
:スマイリー:
1
:okマーク:
1
```

改善点はたくさんあります。
- 出力JSON構造自体の最適化（poster内のvalueっていうやつが邪魔だなあと思っています）
- メッセージ内に3つ以上連続する改行があった場合への対処
- 十分なパターンでのテストを行っていないので、想定していない投稿の構造
etc...

が、メッセージ内容の抽出は実装できた気がするので良いです。
初めてpythonを書いた気がします。

<details><summary>ファイルの構造</summary><div>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505609/5b5c1dff-676d-678d-346c-9abae931041b.png)
</div></details>

<details><summary>プログラム全文</summary><div>

```python
import json
from array import array
import re



# json encorder
class MyEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, FullyJson):
            return {'value': o.__dict__}
        if isinstance(o, Poster):
            return {'value': o.__dict__}
        if isinstance(o, Post):
            return {'value': o.__dict__}
        if isinstance(o, Reaction):
            return {'value': o.__dict__}
        if isinstance(o, Reply):
            return {'value': o.__dict__}
        return json.JSONEncoder.default(self, o)


# define fully json object
class FullyJson:
    # constructor
    def __init__(self, poster_name:str, poster_status:str, post_time:str, post_massage:str, reaction_arrray:array, reply_count:int):
        self.poster: Poster = Poster(poster_name, poster_status)
        self.post: Post = Post(post_time, post_massage)
        self.reply: Reply = Reply(reply_count)
        self.reaction_array = reaction_array


# define composition of json object
class Poster:
    # constructor
    def __init__(self, name:str, status:str):
        if type(name) is not str or type(status) is not str:
            raise TypeError('Error occured at Poster constructor')
        self.name = name
        self.status = status


class Post:
    # constructor
    def __init__(self, time:str, massage:str):
        if type(time) is not str or type(massage) is not str:
            raise TypeError('Error occured at Post constructor')
        self.time = time
        self.massage = massage

    def getMsg(self):
        print(self.massage)


class Reaction:
    # constructor
    def __init__(self, name:str, count:int):
        if type(name) is not str or type(count) is not int:
            raise TypeError('Error occured at Reaction constructor')
        self.name = name
        self.count = count


class Reply:
    # constructor
    def __init__(self, count:int):
        if type(count) is not int:
            raise TypeError('Error occured at Reply constructor')
        self.count = count


# is the number decimal
def isascnum(s):
    return True if s.isdecimal() and s.isascii() else False


# main method start
print("slack_text_to_json/main method is starting...")

# get text data
fully_data:str = open('./src/tmp.txt', 'r', encoding="utf-8").read()

# Leading Row Molding
fully_data = '\n\n\n' + fully_data
fully_data = re.sub("^\n+", "\n", fully_data)

# Remove excessive blank lines of all rows(3 or more blanks remove)
someblanks_removed_data_tmp=re.sub('\n\n\n+', '\n=====\n', fully_data)
someblanks_removed_data=re.sub('\n+', '\n', someblanks_removed_data_tmp)

# Forming to JSON format
MAX_MENTION_LENGTH = 20
LINE_OF_POSTER_NAME:int = 1
LINE_OF_POSTER_STATUS_AND_TIME:int = 2
LINE_OF_BEGINNING_MASSAGE_:int = 3

#Break down it into posts.
posting_data:array=[]
contain_non_posting_data:array = someblanks_removed_data.split('=====')
for data_may_be_posting in contain_non_posting_data:
    each_lines_of_post:str = data_may_be_posting.split('\n')
    # contain mm:ss on the line
    contain_time:bool = re.search('[0-9]{2}:[0-9]{2}',each_lines_of_post[LINE_OF_POSTER_STATUS_AND_TIME]) is not None
    if(contain_time):
        posting_data.append(data_may_be_posting)

ret_arr:array = []
for a_post in posting_data:
    # define roop meta data
    roop_count:int = 0
    be_skip:bool = False # this flg user to skip non-post data like '~が参加しました'
    reaction_flg:bool = False # this flg use to get the next line reaction_count
    #define class member
    reaction_array:array = []
    reaction:Reaction = ("",0)
    poster_name:str = ''
    poster_status:str = ''
    post_time:str = ''
    post_massage:str = ''
    reaction_name:str = ''
    reaction_count:int = 0
    reply_count:int = 0
    
    for line in a_post.split('\n'):
        if(roop_count == 0): # cuz blank line
            pass
        elif(roop_count == LINE_OF_POSTER_NAME):
            poster_name = line
        elif(roop_count == LINE_OF_POSTER_STATUS_AND_TIME):
            if(line[0] == ':'):
                poster_status = line.split()[0]
                post_time = line.split()[1]
            else:
                post_time = line.split()[0]
        else:
            if(len(line) < 1): # cuz blank line
                pass
            #if the line is only :something: then the line is reaction-line
            elif(line[0] == ':' and line[-1:] == ':' and line.count(':') < 3):
                reaction_name = line
                reaction_flg = True
            elif(reaction_flg):
                reaction_flg = False
                if(isascnum(line)):
                    reaction_count = int(line)
                    reaction_array.append(Reaction(reaction_name,reaction_count))
            else:
                if(line[0]=='@' and len(line) < MAX_MENTION_LENGTH):
                    post_massage += ' '+line
                elif(' に追加されました。' in line or 'チャンネルのトピックを設定しました: ' in line or ' に参加しました。' in line):
                    be_skip = True
                else:
                    post_massage += line
        roop_count += 1
    if(be_skip):
        pass
    elif(len(post_massage) == 0):
        pass
    else:
        # encoded data store in array for return
        fully_json:FullyJson = FullyJson(poster_name, poster_status, post_time, post_massage, reaction_array, reply_count)
        ret_arr.append(fully_json)
        js = json.dumps(fully_json, cls=MyEncoder, ensure_ascii=False)
        print(js)
print("...slack_text_to_json/main method is ending")
```
</div></details>


# 二：キーワード抽出をしよう

キーワード抽出には下記2通りを試しました。
=>[YAKE!](https://github.com/LIAAD/yake)
=>[pke](https://github.com/boudinfl/pke) (python keyphrase extraction)
結論から言うと、pkeの方が相応しいワードを抽出できました。

### YAKE!　について
YAKE!のアルゴリズムは、以下を大きく評価するようです

- 文頭に近い位置にある
- 何度も表示する
- 共起する単語が少ない
- 複数の文章に出現する

### pke.MultipartieRank　について
MultipartieRankでは、複数のトピックが存在することが前提で、
- トピック達から、重要なトピックを選ぶ
- 選ばれたトピックから先頭に近いフレーズ候補を抽出する。(出現回数を基に抽出するものもある)

要は、大事な話の最初の方にある単語は大事。
と認識しました。


自然言語処理の入門にも立っていない私は、"フレーズ","トピック"などの定義がイマイチぴんと来なかったので、名言は避けます。
[Keyphrase Extraction],[Graph base ~]などで検索してみてください。

#### キーワード抽出の前処理
日本語は英文と違い、空白やカンマ等で単語を分割しないので、
キーワード分析するために、単語ごと分割する必要があります。
=>[MeCab](https://taku910.github.io/mecab/)を使いました。


こんな感じに分かち書きしてくれます

`日本語は英文と違い、空白やカンマ...`
↓
`日本_語_は_英文_と_違い_、_空白_や_カンマ...`


## 完成したもの
#### 使用した文章
前回まで使用していたデータは、文章自体に意味が含まれていなかったので、対象とする文章を変更しました。 

（公開して良いチャットデータを探し中）。。。
#### YAKE!

<details><summary>プログラム</summary><div>


```python:以下をJSONデータ数ループ
kw_extractor = KeywordExtractor(lan="ja", n=3, top=10)
keywords = kw_extractor.extract_keywords(JSONのmassage.value)
print(keywords)
```
</div></details>



<details><summary>出力</summary><div>

（公開して良いチャットデータを探し中）
</div></details>

#### pke

<details><summary>プログラム</summary><div>

```python
import pke
import separatingWords

extractor = pke.unsupervised.MultipartiteRank()

separated_words_massage_array = separatingWords.separete()
for separated_words_massage in separated_words_massage_array:
    extractor.load_document(input=separated_words_massage, language='ja',　normalization=None)
    extractor.candidate_selection(pos={'NOUN', 'PROPN', 'ADJ', 'NUM'})
    extractor.candidate_weighting(threshold=0.74, method='average', alpha=1.1)
    print(extractor.get_n_best(3))
```
</div></details>

<details><summary>出力</summary><div>

（公開して良いチャットデータを探し中）
</div></details>

## 考察
今回のキーワード抽出で、精度が低かったのは、以下３つの要因があるのではないかと考えます。

#### ・ 短文はキーワード抽出の難易度が高い
短文の場合、各単語の出現回数が1回ずつというケースが頻出します。
文章ごとの関連や、各単語の出現回数、繋がりを基に重要さを決定するアルゴリズムでは、正しいキーワード抽出が出ないのではないか。
加えて、日本語の短文では句読点を打たなくても容易に文が成り立つので、機械的にはどこまでが文なのか判別がつかないのではないか。

#### ・ 主語が先頭に出ない文の解析は難しい
英語も日本語も重要な単語が必ずしも先頭に来るわけではない。
特に日本語で、重要な単語が文頭に出ていない文は、分かち書きをしたところで、英語をベースとしたキーワード抽出処理では高い精度は出せないのではないか。
##### 例
日：skype, discordと同様に、VRでの会話は今後の標準になるだろう。
英：Like skype and discord, VR conversations will become the standard for the future.

または
日：VRでの会話は、skype,discordと同様に、今後の標準になるだろう。
英：Conversations in VR will be the standard for the future, just like skype,discord.

どの文もキーワードは"VR", "会話"であってほしい。
上側の文について、英語ではLike(たぶん前置詞)が文頭に来るが、日本語ではskype(名詞)が文頭に来るため、skype(名詞)が主語だと認識されても仕方ないのかもしれない。

#### ・ 日本語は必ずしも主語が明記されるわけではない。
英語は構造上、主語が明記される。
しかし日本語では主語が明記されない場合がある。
(参考：[主題優勢言語_wikipedia](https://ja.wikipedia.org/wiki/%E4%B8%BB%E9%A1%8C%E5%84%AA%E5%8B%A2%E8%A8%80%E8%AA%9E))
##### 例
日：私はケンジ君と遊んだ。サッカーとバスケットボールをした。
英：I played with Kenji. We played soccer and basketball.


# 三：分析・可視化しよう