様々な記事の単語をカウントするプログラムを作ってみた(mecab python)

# 記事の最頻出単語を調べてみよう
[mecabをpythonで使う](https://qiita.com/Sak1361/items/47e9ec464ccc770cd65c) より、pythonで形態素解析ができる環境が出来たので、mecabとpythonを用いて記事や文章ファイルに出てくる単語をカウントするプログラムを作ってみました。この記事では作成したプログラムの簡単な解説です。
用途？結果を出したらへぇと思えたらそれで良いのです。

## 大まかな流れ
記事データから余分な記号を取り除く→mecabで形態素解析→単語の集計→グラフとかで結果を表示

こんな感じでやっていきます。


## デモコード
gitにあげていますので[こちら](https://github.com/Sak1361/diet_polarize/blob/master/count_word.py)を参照してください。
この記事では上記のコードを解説していきたいと思います。コードの訂正やツッコミは随時お待ちしております。

# デモコードの解説
分割しながら解説していきたいと思います。

## mecabのバグ対策
```demo_init.py
def __init__(self):
    self.s = 0
    self.e = 200000
    self.stops = 2000000
    self.tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
```
バグについては[mecabのバグ？](https://qiita.com/Sak1361/items/47e9ec464ccc770cd65c#mecab%E3%81%AE%E3%83%90%E3%82%B0)参照。
いい案が思いつかなかったので単純に文字数を200万字で区切ってmecabに投げるようにしています。

## URLやHTMLタグを取り除く処理

```demo_re.py
def re_def(self,filepass):
    with codecs.open(filepass, 'r', encoding='utf-8', errors='ignore')as f:
    #with open(filepass, 'r')as f:
        l = ""
        re_half = re.compile(r'[!-~]')  # 半角記号,数字,英字
        re_full = re.compile(r'[︰-＠]')  # 全角記号
        re_full2 = re.compile(r'[、・’〜：＜＞＿｜「」｛｝【】『』〈〉“”○〔〕…――――◇]')  # 全角で取り除けなかったやつ
        re_comma = re.compile(r'[。]')  # 読点のみ
        re_url = re.compile(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+')
        re_tag = re.compile(r"<[^>]*?>")    #HTMLタグ
        re_n = re.compile(r'\n')  # 改行文字
        re_space = re.compile(r'[\s+]')  #１以上の空白文字
        re_num = re.compile(r"[0-9]")
        start_time = time.time()
        for line in f:
            if re_num.match(line):
                line = mojimoji.han_to_zen(line, ascii=False)
            line = re_half.sub("", line)
            line = re_full.sub("", line)
            line = re_url.sub("", line)
            line = re_tag.sub("",line)
            line = re_n.sub("", line)
            line = re_space.sub("", line)
            line = re_full2.sub(" ", line)
            line = re_comma.sub("\n",line)  #読点で改行しておく
            l += line
    end_time = time.time() - start_time
    print("無駄処理時間",end_time)
    return l
```
形態素解析前の処理として、URLやHTMLタグ等を取り除きます。

まず、ファイルを開く際にバイトコードが入っていたりして文句を言われないように``codecs``を用いてエラーを無視するようにしています。

次にタグ等の処理です。処理には正規表現を扱う``re``を使用します。
複数回呼び出すことになるので事前にコンパイルしておきました。
取り除く内容としては

- HTMLタグ
- URL
- 半角文字全般
- 全角記号
- 余分な空白・改行
 です。

半角文字を全て取り除くので英単語等をこの時点で除外してしまうことになりますが・・・目をつぶりました
またうまく取り除けなかった全角記号はアナログに打ち込んでいます。いい感じの正規表現ありましたらご教授くださいm(_ _)m

ただ、数字だけ多少気に食わなかったので``mojimoji``を使って半角から全角に直して対象から外しています。

``time``モジュールについてはどれくらい時間かかってんのかなぁくらいで入れてるだけですﾊｲ

## ストップワードの取得
自然言語処理の前処理として、ストップワードを取り除く作業があります。
今後色々使いそうなので書いてみました。
ストップワード集として[Slothlib](http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt)を使用します。逐一開いてDLしてくる作業がめんどくさいのでスクレイピングしてくるようししています。

```slothlib.py
def sloth_words(self):    #slothwordのlist化
    if os.path.exists("sloth_words.txt"):
        text = ""
        with open("sloth_words.txt",'r') as f:
            for l in f:
                text += l
        soup = json.loads(text,encoding='utf-8')
        return soup
    ###sloth_words###
    sloth = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    slothl_file = urllib.request.urlopen(sloth)
    soup = BeautifulSoup(slothl_file, 'html.parser')
    soup = str(soup).split()
    ###sloth_singleword###
    sloth_1 = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/OneLetterJp.txt'
    slothl_file2 = urllib.request.urlopen(sloth_1)
    soup2 = BeautifulSoup(slothl_file2, 'html.parser')
    soup2 = str(soup2).split()
    soup.extend(soup2)  #1つにまとめる
    ###毎回呼ぶの面倒だからファイル作る
    with open("sloth_words.txt","w") as f:
        text_dic = json.dumps(soup,ensure_ascii=False, indent=2 )
        f.write(text_dic)
    return soup

```
``urllib.request``で取ってきて``BeautifulSoup``でタグを消してます。
一応、一文字のみの[OneLetterJp](http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/OneLetterJp.txt)も同時に取得しています。また、次回以降このスクレイピング処理を省きたいのでファイルを作成して、呼び出せるようにしています。

## 形態素解析用の関数

```morphological.py
def morphologial(self, all_words):
    wakati_data = []
    while True:
        w = all_words[self.s:self.e]
        wakati_data.extend(self.tagger.parse(w).split("\n"))
        if self.e > self.stops or self.e > len(all_words):
            break
        self.s = self.e
        self.e += 200000
    return wakati_data
```
[mecabのバグ](#mecabのバグ対策)より、200万字ごとに分けて形態素解析しています。この関数を、カウントする関数内で繰り返し呼び出していきます。yieldで返しても良かったのですが作成時の知識不足で使ってません。気が向いたら作り直しますﾊｲ

## 単語の形態素解析処理
長いので区切りながら見ていきます。

```counting1.py
def counting(self,all_words):
    dicts = {}  # 単語をカウントする辞書
    print("総文字数:{0}\t({1}万字)".format(len(all_words),len(all_words)/10000))
    mem = 0 #一定単語以上か判別
    sloths = self.sloth_words() #slothのlist
    if len(all_words) > 2000000:
        mem = 1
```
総文字数をこの時点で出力して確認できるようにしてます。もし200万字を超えるなら変数を1にして繰り返し処理するようにします。

```counting2.py
while True:
    word_list = []
    wakati = self.morphologial(all_words) #分かち書きアンド形態素解析
    for addlist in wakati:
        addlist = re.split('[\t,]', addlist)  # 空白と","で分割
        if addlist == [] or addlist[0] == 'EOS' or addlist[0] == '' or addlist[0] == 'ー' or addlist[0] == '*':
            pass
        elif addlist[1] == '名詞':  #名詞のみカウント
            if addlist[2] == '一般' or addlist[2] == '固有名詞' :#and not addlist[3] == '人名':
                    word_list.append(addlist)  #listごとに区切るのでappendで。extendだとつながる
        else:
            pass
```
変数`wakati`で[形態素解析](#形態素解析)の関数を呼び出して解析させます。
`mecab`には追加辞書を適用し、オプションなしで解析していますので
[ここのスクショ](https://qiita.com/Sak1361/items/47e9ec464ccc770cd65c#%E5%B0%8E%E5%85%A5%E7%A2%BA%E8%AA%8D)
<img width="653" alt="mecab.png" src="https://qiita-image-store.s3.amazonaws.com/0/339669/261e179b-d160-ce0f-2b41-5bcb1867d5a2.png">
こんな感じで分かち書き＋品詞＋読みのように出力されます。これを`split`でlist化して扱いやすくしています。

for文内のif・else文ですが、ここで取得する単語の品詞を分けています。このプログラムだと**名詞**に限定しています。さらにそこから名詞の一般形と固有名詞形に絞って変数`word_list`に格納します。

## 単語のカウント

```counting3.py
for count in word_list:
    if count[0] not in dicts:
        dicts.setdefault(count[0], 1)
    else:
        dicts[count[0]] += 1
```
単語を形態素解析し、かつ欲しい品詞に分類するとこまで終わったのであとはカウントするだけです。
dict型を使用してカウントしています。dict内に存在しなければ新しく追加し、既存であればカウントを＋１しています。

## 文字数が200万を超えた場合の処理
```over.py
###文字数オーバー時###
if mem:
    if len(all_words) < self.stops:
        del wakati, addlist, word_list
        break
    else:
        del addlist
        print("{}万字まで終わったよ".format(self.stops/10000))
        self.stops += 2000000
        self.s = self.e
        self.e += 200000
else:
    break
```
コンストラクタの文字数の始点・終点を入れ替えて終わるまで今までの処理を繰り返します。そもそも文字数が200万以下の場合は繰り返し処理は行いません。

## ストップワードの除去

```re_sloths.py
for key in list(dicts): #ストップワード除去
    if key in sloths:
        del dicts[key]
```

別の関数で取得したストップワードを最後に取り除きます。
dict型のキーのみでfor文を回す際は`dict.keys()`よりもlist化する方が良いみたいです。

# カウントした単語をグラフで表示させる

```plot.py
def plot(self,countedwords):
    counts = {}
    total = sum(countedwords.values())
    c = 1
    show = 20 #何件表示する？
    for k, v in sorted(countedwords.items(), key=lambda x: x[1], reverse=True):  # 辞書を降順に入れる
        counts.update( {str(k):int(v)} )
        c += 1
        if c > show:
            break
    plt.figure(figsize=(15, 5)) #これでラベルがかぶらないくらい大きく
    plt.title('頻繁に発言したワードベスト{0} 総単語数{1} 単語の種類数{2}'.format(show,total,len(countedwords)), size=16)
    plt.bar(range(len(counts)), list(counts.values()), align='center')
    plt.xticks(range(len(counts)), list(counts.keys()))
    # 棒グラフ内に数値を書く
    for x, y in zip(range(len(counts)), counts.values()):
        plt.text(x, y, y, ha='center', va='bottom') #出現回数
        plt.text(x, y/2, "{0}%".format(round((y/total*100),3)),ha='center',va='bottom')  #パーセンテージ
    plt.tick_params(width=2, length=10) #ラベル大きさ 
    plt.tight_layout()  #整える
    plt.show()
```

`matplotlib`のplotを用いて、最頻出単語をグラフで表示させてます。
plotは全く触ってなかったのでggっていいとこ取りしてるだけなのでカバカバなコードになってますがご容赦ください。

よく使う箇所としてはこれ

```lambda.py
sorted(countedwords.items(), key=lambda x: x[1], reverse=True)
```
lambda式を用いてdictを降順にしています。

## カウントした単語の検索

```search.py
def Search(self, countedwords,search):
    results = {}
    total = {"単語の種類数":sum(countedwords.values()),"単語の総数":len(countedwords)}
    for k, v in countedwords.items():
        if search == k:
            results.update({str(k): int(v)})

    return results , total
```
グラフで表示させるのとは別に、検索もできます。

# 実際にカウントしてみる
[安倍総理が今年一番多く発言した単語を調べてみた](https://qiita.com/Sak1361/items/350cf0cb9d925e3c0924)
[Wikipediaの記事の単語をカウントしてみた](https://qiita.com/Sak1361/items/bad78b5e26ce0f82de46)
[安倍総理の発言を各年毎に集計してグラフにしてみた](https://qiita.com/Sak1361/items/282af50e3c387bf0be3d)