さいとー・ま

さいとー・ま

さいとう・まの。おしごとは manoestasmanoあっとgmail.com (あっとを いれかえてください)まで。

テキストマイニングへの道02

前回のつづきです。
前回→
hunihunisaito.hatenablog.com

前回は、KHcoder(けいえいちこーだー)を使う前までの途中経過を書きました。
続いて、KHCoderの導入と、Python(ぱいそん)のプログラミングについて書きます。

KHCoder

KHCoder(けいえいちこーだー)はテキストマイニング、つまりは文章を計量的に分析するのに最も有名なフリーソフトウェアです。
khcoder.net
Windowsを使っている人は、ダウンロードをすればすぐに使える優れもの。ただし、Macを使っていると様々な難関をクリアする必要があるらしいです…。大変。
自分はWindowsを使っているので、Windowsの話をします。といっても、話は簡単で、https://khcoder.net/dl3.htmlからフリーの方をダウンロードするだけです。簡単。
KHCoderのチュートリアルhttps://khcoder.net/tutorial.html)も用意してくれているので、それをやってみると良いでしょう(自分はすぐに自分のデータを使ってしまいました)。
詳しくはチュートリアルを読んでほしいのですが、大まかな手順は次の通りです。
1. たくさんのテキストをまとめたデータをエクセルなどで用意する。この時、一番左の列に分析したい文章を、その右側にそれがどこに書かれていたかなどの情報を残しておきます。この情報も合わせて分析できます。しかし、自分はいまだこの情報をうまく使えていません。
2. kh.coderを立ち上げて、プロジェクト→新規→参照で、1のファイルを選択する。また、説明(メモ)に何のデータかを記入しておくと便利です。後々わからなくなります(二敗)。okを押すと、データを読み込んでくれます。
3. 次に、前処理→前処理の実行をします。結構時間がかかるときもあります。しかし、だいたいが10秒以内に終わるイメージです。
4. ツール→抽出語→抽出語リストを選択します。すると、どんな単語が多く使われているかを確認できます。これは、特にある単語で文章を集めている場合にはあまり効果は発揮しませんが、小説などを分析すると思わぬ結果が出て面白いと思います。論文などに使いたいデータについては、抽出語リストの右下のExcel出力をすることで、データになります。ここで品詞ごとの分析と頻出語150の選択があります。品詞ごとを見ていると思わぬ発見がある気がします。頻出語150は論文のデータとして掲載しているのをよく見る気がします(自信がない)。
5. ツール→抽出語→階層的クラスター分析の実行をします。すると、似た使われ方をしている単語を集めて、トーナメント形式みたいな図にしてくれます。色ごとにクラスター(グループみたいな意味です)を分けてくれます。次にクラスターの数を調整するために、左下の「併合水準」を選択します。棒線グラフができるので、落ちている角度が急であるところを探して、その下ったところの数のクラスターに分けるよう「調整」します。詳しくは、KH Coder講座⑥階層的クラスター分析 データをグループ分けして特徴を掴む! - 統計LIFE~ゼロから始める統計学~を確認してください。
6. ツール→抽出語→共起ネットワークを選択します。すると、単語同士のつながりが図になります。この図はわかりやすくていいらしいです。詳しくはKH Coder講座⑤共起ネットワークの作成方法 直感的分かりやすさNo.1! - 統計LIFE~ゼロから始める統計学~を確認してください。

とりあえず、これくらいが出来ると本当に最低限のことが出来るといえるでしょう。というか、自分はまだこの程度しかできません。

Python1

ツイートの収集に使うコードを色々と試してみました。
ailaby.com
の「大量にダウンロード」のコードを参考にしました。また、
fanblogs.jp
teratail.com
も参考にしました。

# -*- coding: utf-8 -*-
# グーグルドライブのファイルに書き込み

from requests_oauthlib import OAuth1Session
import json
import datetime, time, sys
from abc import ABCMeta, abstractmethod

CK = 'APIキー'
CS = 'APIキーシークレット'
AT = 'アクセストークン'
AS = 'アクセストークンシークレット'


class TweetsGetter(object):
__metaclass__ = ABCMeta

def __init__(self):
self.session = OAuth1Session(CK, CS, AT, AS)

@abstractmethod
def specifyUrlAndParams(self, keyword):
'''
呼出し先 URL、パラメータを返す
'''

@abstractmethod
def pickupTweet(self, res_text, includeRetweet):
'''
res_text からツイートを取り出し、配列にセットして返却
'''

@abstractmethod
def getLimitContext(self, res_text):
'''
回数制限の情報を取得 (起動時)
'''

def collect(self, total = -1, onlyText = False, includeRetweet = False):
'''
ツイート取得を開始する
'''

#----------------
# 回数制限を確認
#----------------
self.checkLimit()

#----------------
# URL、パラメータ
#----------------
url, params = self.specifyUrlAndParams()
params['include_rts'] = str(includeRetweet).lower()
# include_rts は statuses/user_timeline のパラメータ。search/tweets には無効

#----------------
# ツイート取得
#----------------
cnt = 0
unavailableCnt = 0
while True:
res = self.session.get(url, params = params)
if res.status_code == 503:
# 503 : Service Unavailable
if unavailableCnt > 10:
raise Exception('Twitter API error %d' % res.status_code)

unavailableCnt += 1
print ('Service Unavailable 503')
self.waitUntilReset(time.mktime(datetime.datetime.now().timetuple()) + 30)
continue

unavailableCnt = 0

if res.status_code != 200:
raise Exception('Twitter API error %d' % res.status_code)

tweets = self.pickupTweet(json.loads(res.text))
if len(tweets) == 0:
# len(tweets) != params['count'] としたいが
# count は最大値らしいので判定に使えない。
# ⇒ "== 0" にする
# https://dev.twitter.com/discussions/7513
break

for tweet in tweets:
if *1:
pass
else:
if onlyText is True:
yield tweet['text']
else:
yield tweet

cnt += 1
if cnt % 100 == 0:
print ('%d件 ' % cnt)

if total > 0 and cnt >= total:
return

params['max_id'] = tweet['id'] - 1

# ヘッダ確認 (回数制限)
# X-Rate-Limit-Remaining が入ってないことが稀にあるのでチェック
if ('X-Rate-Limit-Remaining' in res.headers and 'X-Rate-Limit-Reset' in res.headers):
if (int(res.headers['X-Rate-Limit-Remaining']) == 0):
self.waitUntilReset(int(res.headers['X-Rate-Limit-Reset']))
self.checkLimit()
else:
print ('not found - X-Rate-Limit-Remaining or X-Rate-Limit-Reset')
self.checkLimit()

def checkLimit(self):
'''
回数制限を問合せ、アクセス可能になるまで wait する
'''
unavailableCnt = 0
while True:
url = "https://api.twitter.com/1.1/application/rate_limit_status.json"
res = self.session.get(url)

if res.status_code == 503:
# 503 : Service Unavailable
if unavailableCnt > 10:
raise Exception('Twitter API error %d' % res.status_code)

unavailableCnt += 1
print ('Service Unavailable 503')
self.waitUntilReset(time.mktime(datetime.datetime.now().timetuple()) + 30)
continue

unavailableCnt = 0

if res.status_code != 200:
raise Exception('Twitter API error %d' % res.status_code)

remaining, reset = self.getLimitContext(json.loads(res.text))
if (remaining == 0):
self.waitUntilReset(reset)
else:
break

def waitUntilReset(self, reset):
'''
reset 時刻まで sleep
'''
seconds = reset - time.mktime(datetime.datetime.now().timetuple())
seconds = max(seconds, 0)
print ('\n =====================')
print (' == waiting %d sec ==' % seconds)
print (' =====================')
sys.stdout.flush()
time.sleep(seconds + 10) # 念のため + 10 秒

@staticmethod
def bySearch(keyword):
return TweetsGetterBySearch(keyword)

@staticmethod
def byUser(screen_name):
return TweetsGetterByUser(screen_name)


class TweetsGetterBySearch(TweetsGetter):
'''
キーワードでツイートを検索
'''
def __init__(self, keyword):
super(TweetsGetterBySearch, self).__init__()
self.keyword = keyword

def specifyUrlAndParams(self):
'''
呼出し先 URL、パラメータを返す
'''
url = 'https://api.twitter.com/1.1/search/tweets.json'
params = {'q':self.keyword, 'count':100}
return url, params

def pickupTweet(self, res_text):
'''
res_text からツイートを取り出し、配列にセットして返却
'''
results =
for tweet in res_text['statuses']:
results.append(tweet)

return results

def getLimitContext(self, res_text):
'''
回数制限の情報を取得 (起動時)
'''
remaining = res_text['resources']['search']['/search/tweets']['remaining']
reset = res_text['resources']['search']['/search/tweets']['reset']

return int(remaining), int(reset)


class TweetsGetterByUser(TweetsGetter):
'''
ユーザーを指定してツイートを取得
'''
def __init__(self, screen_name):
super(TweetsGetterByUser, self).__init__()
self.screen_name = screen_name

def specifyUrlAndParams(self):
'''
呼出し先 URL、パラメータを返す
'''
url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'
params = {'screen_name':self.screen_name, 'count':200}
return url, params

def pickupTweet(self, res_text):
'''
res_text からツイートを取り出し、配列にセットして返却
'''
results =

for tweet in res_text:
results.append(tweet)

return results

def getLimitContext(self, res_text):
'''
回数制限の情報を取得 (起動時)
'''
remaining = res_text['resources']['statuses']['/statuses/user_timeline']['remaining']
reset = res_text['resources']['statuses']['/statuses/user_timeline']['reset']

return int(remaining), int(reset)


if __name__ == '__main__':

# キーワードで取得
getter = TweetsGetter.bySearch(u'キーワード')

# ユーザーを指定して取得 (screen_name)
#getter = TweetsGetter.byUser('AbeShinzo')

cnt = 0
for tweet in getter.collect(total = 100000):
cnt += 1
pname = "ドライブのファイルのパス"
fname = pname + '/ファイルの名前.txt'
f = open (fname, "a", encoding='utf-8')
print (tweet['text'].replace('\r', '').replace('\n', ''), ",",tweet['id'], ",",tweet['created_at'] ,",", tweet['user']['screen_name'], file=f)
f.close

APIキー、APIキーシークレット、アクセストークン、アクセストークンシークレットを自分のものに入れ替えてください。キーワード、ドライブのファイルのパス、ファイルの名前を入れ替えてください。これでcsvファイルになるので、あとはこれをKHCoderに入れます。もし文字化けする場合は、CSVファイルが文字化けした場合の対処方法 – freee ヘルプセンターを参考にしてください。これで、ツイート本文、日時、名前が分かるデータができます。
といっても、このコードが必要なのは、たくさんのツイートを集める時です。
ちなみに、このコードの最後は自分が書いたので非効率的に違いありません。

Python2

通常の少ない量を分析する上では、次のコードをつかいます。

import tweepy
import csv
consumer_key = 'APIキー'
consumer_secret = 'APIキーシークレット'
access_token = 'アクセストークン'
access_token_secret = 'アクセストークンシークレット''
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)
pname = "ドライブのファイルのパス"
fname = pname + '/ファイルの名前.txt'
with open (fname, "x", encoding='utf-8') as f:
for status in api.search(q='キーワード exclude:retweets',tweet_mode='extended',result_type="mixed", count=500,):
print(status.full_text.replace('\r', '').replace('\n', ''), ",", status.created_at, ",", status.user.name, ",", status.user.screen_name, ",", "Twitter.com/{}/status/{}".format(status.user.screen_name,status.id), ",", status.retweet_count, ",", status.favorite_count, ",", status.user.description.replace('\r', '').replace('\n', ''), "," , file=f)

APIキー、APIキーシークレット、アクセストークン、アクセストークンシークレットを自分のものに入れ替えてください。キーワード、ドライブのファイルのパス、ファイルの名前を入れ替えてください。これでリツイート数といいねの数とURLも含まれます。

しかし、なんと驚きのことが分かりました。

ついすぽ

tilde.afonomics.com
ついすぽという拡張機能がありました。こっちのほうが断然早いです……敗北。

*1:'retweeted_status' in tweet) and (includeRetweet is False