Twitterのツイートをwordcloudで可視化したい【後編】

前回はTwitterのタイムラインを取得するところで終了していましたが、実際に使用してみるうちに改良などをしていきました。

【前回のエントリ】 uepon.hatenadiary.com

修正のポイントを記載してから、wordcloudの処理に進めていこうと思います。(ソース内の*Twitterアプリのキーのため伏せています。)

Twitterのタイムラインを取得に使ったソース完成版】

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import json
from requests_oauthlib import OAuth1Session
from pytz import timezone
from dateutil import parser
import time
import re
import codecs
import emoji

CONSUMER_KEY = '************************'
CONSUMER_SECRET = '************************'
ACCESS_TOKEN = '************************'
ACCESS_TOKEN_SECRET = '************************'
sinceid = -1
total_count = 0
hash_pattern = r'[##]([\w一-龠ぁ-んァ-ヴーa-z]+)'

def removeEmoji(src):
    return ''.join(c for c in src if c not in emoji.UNICODE_EMOJI)

twitter = OAuth1Session(CONSUMER_KEY,
                        CONSUMER_SECRET,
                        ACCESS_TOKEN,
                        ACCESS_TOKEN_SECRET)

url = "https://api.twitter.com/1.1/search/tweets.json"
# query = '#nhk exclude:retweets'
query = '#紅白歌合戦 exclude:retweets'
params = {
    'q': query,
    'count': 200,
    'tweet_mode': 'extended',
    'since_id': sinceid
}

print('----------------------------------------------------')

while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
        search_timeline = json.loads(req.text)
        metadata = search_timeline['search_metadata']
        metasid = metadata['since_id']
        metamid = metadata['max_id']
        total_count += len(search_timeline['statuses'])
        limit = req.headers.get('x-rate-limit-remaining', 0)
        for tweet in search_timeline['statuses']:
            text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", tweet['full_text'])
            text = re.sub('\r', '', text)
            text = re.sub('\n', '', text)
            for hashtag in ((re.findall(hash_pattern, text))):
                r = r'[##]%s' % hashtag
                text = re.sub(r, '', text)
            jst_time = parser.parse(tweet['created_at']).astimezone(timezone('Asia/Tokyo'))

            logData += '"' + str(jst_time)+ '","' + str(tweet['id']) + '","' + \
                      tweet['user']['name'] + '","@' + tweet['user']['screen_name']  + '","' + \
                      text + '"\n'
            # ----------------------------------------------------
    else:
        print("ERROR: %d" % req.status_code)
    params['since_id'] = metamid + 1
    print('******')
    print('total_count::' + str(total_count))
    print('APIlimit::' + str(limit))
    # ----------------------------------------------------
    with codecs.open('./log/log.csv', mode='a', encoding='utf-8') as f:
        print(removeEmoji(logData), file=f, end='')
    # ----------------------------------------------------
    time.sleep(5)

Twitterでタイムラインを収集した際のポイント

先程の完成版で修正などを行ったところをメモしておきます。

Tweetの時刻がUTCとして記録されているので、JSTに変更したい

ツイートに含まれる生成時の時刻create_atUTCで表現されています。そのままだとわかりにくいのでJSTに変更するようにしました。 Pythonタイムゾーンや時刻を扱うにはpytzタイムゾーン処理)、python-dateutil(時刻処理)を使うことが多いようでしたので、 今回はそれを使用しています。

【参考】 qiita.com

【パッケージのインストール】

$ pip install pytz
$ pip install python-dateutil

UTCのDate文字列をJSTのDate文字列に変換する処理

【サンプル】

from pytz import timezone
from dateutil import parser

utc_string = "Sat Mar 19 06:17:57 +0000 2016"
jst_time = parser.parse(utc_string).astimezone(timezone('Asia/Tokyo'))
print(jst_time)
# 出力:2016-03-19 15:17:57+09:00

この処理を組み入れてツイート取得時に含まれる生成時刻create_atを以下のように記述して変換しています。

【プログラム抜粋】

while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
        search_timeline = json.loads(req.text)
【中略】
        for tweet in search_timeline['statuses']:
【中略】
            jst_time = parser.parse(tweet['created_at']).astimezone(timezone('Asia/Tokyo'))
【中略】

140文字以上のツイートを取得する

以前、twitterの書き込みが140文字以上になったというリリースがありましたが、現在はこのような長いツイートがあると全文が表示されず、途中省略されて全文へのリンクが表示されるようになっています。公式ドキュメントを確認すると以下のようになっています。

Tweet updates — Twitter Developers

ドキュメントの表内のExtendedの部分がそれに当たります。 APIアクセス時のパラメータにtweet_mode=extendedを追加し、戻ってきたJSONデータに関しては、これまでのツイート本文であったtextの代わりにfull_textを使えば良いと記載があります。これに合わせてソースを変更します。

【プログラム抜粋】

【中略】
query = '#nhk exclude:retweets'
params = {
    'q': query,
    'count': 200,
    'tweet_mode': 'extended',
    'since_id': sinceid
}
【中略】
while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
【中略】
        for tweet in search_timeline['statuses']:
            text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", tweet['full_text'])

これで140文字以上のツイートも取得できます。

WindowsUTF-8文字列をファイルに書き込もうとするとコードエラーが発生する

取得したデータをcsvファイルに格納しようとするとエンコードエラーが発生してしまいました。 Python3であればUTF-8などの文字コードに関してはそれほど気にしなくてもいいのかなと思っていたのでちょっと驚きました。 更に開発に使用している画面表示に関してはPycharm側のコンソールを使っているのでUTF-8の表示も問題ありませんので、更に気がつくまでに時間がかかりました。

今回の問題はWindows側だけに発生しているようです。以下が参考になりました。

go-journey.club

簡単に言うとPython側で処理した文字列をファイル保存する際にうまく処理がおこわなれないのが原因のようです。 そこでcodecというモジュールを使用し、さらに書き込み時にはwrite()ではなくprint()を使用し、更に引数にfileを指定することで解決ができます。

【サンプル】

import codecs

# 最初に文字コードを指定して「追加」モードでログファイルを開く
f = codecs.open('/var/log/python/app.log', 'a', 'utf-8')
new_words = 'データ'
# print()で書き込みを行う。出力先の引数にfileを指定する
print(new_words, file=f)

これを使用しています。途中removeEmoji()とありますがユーザ定義の関数になりますので、単にここに文字列の変数を入れれば問題ありません。 これが入っている理由はemojiが入っているテキストでは最終目的であるwordcloudで処理ができないためです。

【プログラム抜粋】

【中略】
    with codecs.open('./log/log.csv', mode='a', encoding='utf-8') as f:
        print(removeEmoji(logData), file=f, end='')
【中略】

ちなみにemojiを扱うパッケージは、そのままemojiとなりますのでインストールもそのままでOKです。

【インストール】

$ pip install emoji

これを使用して以下のようにemoji削除の関数を作成しています。

def removeEmoji(src):
    return ''.join(c for c in src if c not in emoji.UNICODE_EMOJI)

長時間動作中にKeyErrorエラーが発生する

プログラムを長時間動作させていると極稀にKeyErrorが発生することがあります。 エラーに該当している部分、取得したJSONのHeaderの中にキーが含まれないことがあることに起因します。むしろそんなことがあるのか?と驚きましたがあるようです。

そこで取得する方法をget()を使うことでキーが存在しなければ、指定したデフォルト値を返すという方法に変更しました。

【変更前】

        limit = req.headers['x-rate-limit-remaining']

【変更後】

        limit = req.headers.get('x-rate-limit-remaining', 0)

これで長時間動作も可能になりました。(エラーは数時間やって一回程度のものです。)

そろそろWordCloudが使いたい…

ようやくデータが取得できたので目的であるWordCloudを使う準備をしてみたいと思います。

パッケージのGitHubのサイトは以下になります。

github.com

テストされているPythonのバージョンは、2.7、3.4、3.5、3.6、3.7のようです。

WordCloudのライブラリ

$ pip install wordcloud

依存するパッケージも同時にインストールされるので、素の状態であれば以下のパッケージがインストールされます。

  • pillow
  • numpy
  • wordcloud

このパッケージをインストールすると、CLI環境で使用できるwordcloud_cliというツールも同時にインストールされます。 こちらを使ってもテストができます。

【使用例】

$ wordcloud_cli --text mytext.txt --imagefile wordcloud.png

GitHubにあるサンプルを使用して、実験を行ってみます。

【alice.txt】 https://raw.githubusercontent.com/amueller/word_cloud/master/examples/alice.txt

【今回の実行例】

$ wordcloud_cli --text alice.txt --width 640 --height 480 --imagefile wordcloud.png

特に大きなエラーも発生せず、問題なく生成されています。

f:id:ueponx:20190101195212p:plain

では、このまま日本語のテキストもやってみようと思います。 以下のようなテキストを準備します。単語と単語の間にスペースを開ける必要があるようなのでこのようにしてみました。

【Jsamplet.txt】

今日 明日 明後日 晴れ 雨 しかし

以下のように実行してみると…

$ wordcloud_cli --text Jsample.txt --width 640 --height 480 --imagefile wordcloud.png

おや?文字が出ません。

f:id:ueponx:20190101201049p:plain

ググってみると日本語ではフォントファイルの設定をしないと表示できないということがかかれていたので --fontfileオプションでフォントファイルを設定して以下のように実行してみました。

$ wordcloud_cli --text Jsample.txt --fontfile C:\Windows\Fonts\meiryo.ttc --width 640 --height 480 --imagefile wordcloud.png

これでうまく日本語が表示されました。

f:id:ueponx:20190101202337p:plain

これからはPythonのプログラムと連携させて生成するようにしていきます。

(注)やっていてわかったのですが、wordcloudは画像を生成する際に?内部でmatplotlibモジュールを使用しているようなので、 もしインストールを行っていない場合には事前にインストールを行う必要があるようです。ほんと?

matplotlibモジュールのインストール】

$ pip install matplotlib

【wordcloudの画像生成】

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from wordcloud import WordCloud

contents = open('Jsample.txt', encoding="utf-8").read()
# contents = '今日 明日 明後日 晴れ 雨 しかし'

fpath = 'C:\\Windows\\Fonts\\meiryo.ttc'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(contents)
wordcloud.to_file("./wordcloud_sample.png")

実行するとこんな感じに生成されます。今回は背景色を白に指定しています。

f:id:ueponx:20190101213721p:plain

あとはTwitterのタイムラインとこのプログラムを結合することになります。 ただ、wordcloudの入力に与えられるテキストデータは単語をスペースを開けた形にする必要があるのでデータの整形が必須となります。

入力データの整形を行う

今回収集しているデータはCSVなのでそのうちのツイートを取り出し、更にテキストの分かち書きを行うことになります。 分かち書きは簡単に言うと「文章を品詞分解を行い、単語ごとに取り出す」ことになります。この分かち書きには形態素解析エンジンを使用します。

以前のエントリではMecabという形態素解析エンジンを使用していましたが今回は違うアプローチをしたほうが面白いと思ったのでjanomeという 形態素解析エンジンを使用することにします。

Python形態素解析エンジンjanome

Janomeは、Mecabと比べると実行速度は劣りますが、Pythonのみで実装されていて辞書も内包されている点が特徴となります。 pipコマンドだけでインストールできる容易さも魅力です。詳細は以下をお読みください。

janomeドキュメント】

Welcome to janome's documentation! (Japanese) — Janome v0.3 documentation (ja)

インストールは以下でOKです。

$ pip install janome

まずは、形態素解析のサンプルを作ってみます。janome公式のドキュメントを見ながら作ってみると以下のようになります。

【サンプル】

from janome.tokenizer import Tokenizer

Sentence='ことしも熱い戦いをありがとう'

t = Tokenizer()
tokens = t.tokenize(Sentence)

for token in tokens:
    print("表層形:",token.surface,"\n"
          "品詞:",token.part_of_speech.split(',')[0],"\n"
          "品詞細分類1:",token.part_of_speech.split(',')[1],"\n"
          "品詞細分類2:",token.part_of_speech.split(',')[2],"\n"
          "品詞細分類3:",token.part_of_speech.split(',')[3],"\n"
          "活用型:",token.infl_type,"\n"
          "活用形:",token.infl_form,"\n"
          "原形:",token.base_form,"\n"
          "読み:",token.reading,"\n"
          "発音:",token.phonetic)
    print('-'*32)

【サンプルの実行結果】

$ python.exe janome_sample.py
表層形: ことし 
品詞: 名詞 
品詞細分類1: 副詞可能 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: ことし 
読み: コトシ 
発音: コトシ
--------------------------------
表層形: も 
品詞: 助詞 
品詞細分類1: 係助詞 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: も 
読み: モ 
発音: モ
--------------------------------
表層形: 熱い 
品詞: 形容詞 
品詞細分類1: 自立 
品詞細分類2: * 
品詞細分類3: * 
活用型: 形容詞・アウオ段 
活用形: 基本形 
原形: 熱い 
読み: アツイ 
発音: アツイ
--------------------------------
表層形: 戦い 
品詞: 名詞 
品詞細分類1: 一般 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: 戦い 
読み: タタカイ 
発音: タタカイ
--------------------------------
表層形: を 
品詞: 助詞 
品詞細分類1: 格助詞 
品詞細分類2: 一般 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: を 
読み: ヲ 
発音: ヲ
--------------------------------
表層形: ありがとう 
品詞: 感動詞 
品詞細分類1: * 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: ありがとう 
読み: アリガトウ 
発音: アリガトー
--------------------------------

Process finished with exit code 0

形態素解析の結果でwordcloudで使用したいのは今回使用するのは表層系品詞ぐらいでしょうか。 助詞や接続詞などが多く出てきても面白くないですしね。(ツイッターでは比較的短文が多いので、それらの品詞あまり数はないと思いますが)

では次は取得したデータを使って分かち書きの出力を行おうと思います。

ツイートデータから分かち書きファイルを生成する

取得したツイートのデータから形態素解析を行って分かち書きを行ってみます。

取得したツイートデータの形式は

  • 書き込み時刻
  • TweetしたユーザID
  • Username(ユーザ名)
  • Screenname(@のついたユーザ名)
  • Tweetテキスト

となっているのでCSVの第4フィールドを取り出して形態素解析を行い、スペースで分かち書きした形式で出力を行います。

import csv
from janome.tokenizer import Tokenizer
import codecs

wakachi = ''

with codecs.open('./wakachi_log.txt', mode='w', encoding='utf-8') as fw:
    None

t = Tokenizer()
with open('log.csv', 'r', encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        tweet = row[4]

        word_list = []
        tokens = t.tokenize(tweet)
        for token in tokens:
            word = token.surface
            word_base = token.base_form
            partOfSpeech = token.part_of_speech.split(',')[0]
            if token.base_form in ["ある", "なる", "こと", "よう", "そう", "これ", "それ", "する", "いる", "いい"]:
                continue
            if partOfSpeech in ["形容詞", "動詞", "名詞", "代名詞", "副詞"]:
                if (partOfSpeech == "名詞"):
                    if (token.part_of_speech.split(',')[1] in ["数", "接尾", "助数詞", "非自立"]):
                        continue
                elif (partOfSpeech == "動詞"):
                    if (token.part_of_speech.split(',')[1] not in ["自立"]):
                        continue
                elif (partOfSpeech == "形容詞"):
                    if (token.part_of_speech.split(',')[1] not in ["自立"]):
                        continue
                elif (partOfSpeech == "副詞"):
                    if (token.part_of_speech.split(',')[1] in ["助詞類接続"]):
                        continue
                word_list.append(word_base)
                print("表層形:", token.surface, "\n"
                    "品詞:", token.part_of_speech.split(',')[0], "\n"
                    "品詞細分類1:", token.part_of_speech.split(',')[1], "\n"
                    "品詞細分類2:", token.part_of_speech.split(',')[2], "\n"
                    "品詞細分類3:", token.part_of_speech.split(',')[3], "\n"
                    "活用型:", token.infl_type, "\n"
                    "活用形:", token.infl_form, "\n"
                    "原形:", token.base_form, "\n"
                    "読み:", token.reading, "\n"
                    "発音:", token.phonetic)
            print('-' * 8)

        wakachi = " ".join(word_list)
        with codecs.open('./wakachi_log.txt', mode='a', encoding='utf-8') as fw:
            print(wakachi, file=fw)

いろいろノイズが多いので品詞で絞ったり、あってもよくわからないNGワードは抜いています。 出来上がったファイルは以下のようになりました。

【wakachi_log.txt】

f:id:ueponx:20190103163505p:plain

あとは出力されたファイルをwordcloudの処理にかければOKです。 前述のファイルを少し変えて以下のようにしました。

【wc.py】

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from wordcloud import WordCloud

contents = open('wakachi_log.txt', encoding="utf-8").read()

fpath = 'C:\\Windows\\Fonts\\meiryo.ttc'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(contents)
wordcloud.to_file("./wordcloud.png")

箱根駅伝直後の数分のデータで生成すると以下のようになりました。

f:id:ueponx:20190103163941p:plain

割といい感じになっているような気がします。

ようやくお楽しみの時間へ

いくつかの番組をwordcloudの処理をかけてみました。

NHK紅白歌合戦 12/31 19時頃放送】

NGワード調整前

f:id:ueponx:20190103180840p:plain

いろいろ抽象的なものが多かったので、除外ワード追加調整後

f:id:ueponx:20190107080322p:plain

ぐるナイ おもしろ荘 12/31 24:30頃放送】

f:id:ueponx:20190103193948p:plain

フットンダ 12/31 26:00頃放送】

f:id:ueponx:20190103170050p:plain

【第95回箱根駅伝 往路 1/2 8:00頃放送】

f:id:ueponx:20190103200926p:plain

【第95回箱根駅伝 復路 1/3 8:00頃放送】

f:id:ueponx:20190103203314p:plain

おわりに

割と面白い結果がでてきますね。 それにしてもデータの取得さえしておくといろいろ楽しめます。