前回は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_at
はUTCで表現されています。そのままだとわかりにくいので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文字以上のツイートも取得できます。
WindowsでUTF-8文字列をファイルに書き込もうとするとコードエラーが発生する
取得したデータをcsvファイルに格納しようとするとエンコードエラーが発生してしまいました。 Python3であればUTF-8などの文字コードに関してはそれほど気にしなくてもいいのかなと思っていたのでちょっと驚きました。 更に開発に使用している画面表示に関してはPycharm側のコンソールを使っているのでUTF-8の表示も問題ありませんので、更に気がつくまでに時間がかかりました。
今回の問題はWindows側だけに発生しているようです。以下が参考になりました。
簡単に言うと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のサイトは以下になります。
テストされている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
特に大きなエラーも発生せず、問題なく生成されています。
では、このまま日本語のテキストもやってみようと思います。 以下のようなテキストを準備します。単語と単語の間にスペースを開ける必要があるようなのでこのようにしてみました。
【Jsamplet.txt】
今日 明日 明後日 晴れ 雨 しかし
以下のように実行してみると…
$ wordcloud_cli --text Jsample.txt --width 640 --height 480 --imagefile wordcloud.png
おや?文字が出ません。
ググってみると日本語ではフォントファイルの設定をしないと表示できないということがかかれていたので
--fontfile
オプションでフォントファイルを設定して以下のように実行してみました。
$ wordcloud_cli --text Jsample.txt --fontfile C:\Windows\Fonts\meiryo.ttc --width 640 --height 480 --imagefile wordcloud.png
これでうまく日本語が表示されました。
これからは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")
実行するとこんな感じに生成されます。今回は背景色を白に指定しています。
あとは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
で使用したいのは今回使用するのは表層系
と品詞
ぐらいでしょうか。
助詞や接続詞などが多く出てきても面白くないですしね。(ツイッターでは比較的短文が多いので、それらの品詞あまり数はないと思いますが)
では次は取得したデータを使って分かち書きの出力を行おうと思います。
ツイートデータから分かち書きファイルを生成する
取得したツイートのデータから形態素解析を行って分かち書きを行ってみます。
取得したツイートデータの形式は
となっているので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】
あとは出力されたファイルを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")
箱根駅伝直後の数分のデータで生成すると以下のようになりました。
割といい感じになっているような気がします。
ようやくお楽しみの時間へ
いくつかの番組をwordcloud
の処理をかけてみました。
【NHK紅白歌合戦 12/31 19時頃放送】
NGワード調整前
いろいろ抽象的なものが多かったので、除外ワード追加調整後
【ぐるナイ おもしろ荘 12/31 24:30頃放送】
【フットンダ 12/31 26:00頃放送】
【第95回箱根駅伝 往路 1/2 8:00頃放送】
【第95回箱根駅伝 復路 1/3 8:00頃放送】
おわりに
割と面白い結果がでてきますね。 それにしてもデータの取得さえしておくといろいろ楽しめます。