シェルスクリプトマガジン

バーティカルバーの極意(Vol.67掲載)

著者:飯尾 淳

前回に引き続き、筆者らの研究グループが開発したTwitterのトレンド分析システムについて解説します。前回は、そもそもTwitterのトレンドとは何かから始まり、開発者登録をしてトレンドAPIを叩き、そのリストを収集する方法まで説明しました。今回は、トレンドをキーにしてツイートを取得する方法と、ツイート群を対象として共起ネットワークグラフを作る方法を紹介します。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図4 トレンドを収集して分析するプログラムのコード(その1)

#!/usr/bin/env python
###########################################################
# ヘルパーファンクション
###########################################################
import re
# 特殊文字をエスケープする
def escape(s, quoted=u'\'"\\', escape=u'\\'):
  return re.sub(u'[%s]' % re.escape(quoted),
                lambda mo: escape + mo.group(),
                s)
# ノードのプリティプリンタ
def pp_node(ary_of_nodes):
  for node in ary_of_nodes:
    keyword = escape(node[0])
    frequency = node[1]
    print("node_hash['{0}'] = 
      trend.nodes.create(word: '{1}', freq: {2:6.2f})"
        .format(keyword, keyword, frequency))
# リンクのプリティプリンタ
def pp_link(ary_of_links):
  for link in ary_of_links:
    (kw1,kw2) = link[0].split(',')
    print("link = Link.create(corr: {0:6.2f})".format(link[1]))
    print("node_hash['{0}'].links << link".format(escape(kw1)))
    print("node_hash['{0}'].links << link".format(escape(kw2)))
# 値を正規化する。p_fragは百分率とするときTrue
def normalize(hash_obj, p_flag):
  maxval = max(hash_obj, key=lambda x: x[1])[1]
  factor = 100 if p_flag else 1
  return [(x[0], x[1] / maxval * factor) for x in hash_obj]

図5 トレンドを収集して分析するプログラムのコード(その2)

###########################################################
# ワードグラフの作成
###########################################################
import MeCab
# 「PATH_TO_MECAB_NEOLOGD」の部分は環境に合わせて修正
m = MeCab.Tagger("-d PATH_TO_MECAB_NEOLOGD")
stopwords = {'*', 'HTTPS', 'HTTP', 'WWW', 'の', 'ん', 'ン', 'ω', '???'}
def create_graph(keyword, collected, tweets):
  ary_of_ary = []
  for tweet in tweets:
    ary = []
    lines = m.parse(tweet).split('\n')
    for line in lines:
      if line.count('\t') == 0: continue
      (kw, prop) = line.split('\t')
      props = prop.split(',')
      if len({props[-3]} & stopwords) > 0: continue
      if props[1] == '固有名詞': ary.append(props[-3])
    ary_of_ary.append(ary)
  KW_THRESHOLD = 20
  kw_dict = {}
  counter = 0
  for ary in ary_of_ary:
    for kw in ary:
      if kw in kw_dict: kw_dict[kw] = kw_dict[kw] + 1.0
      else: kw_dict[kw] = 1.0
      counter = counter + 1
  for kw,val in kw_dict.items(): kw_dict[kw] = val / counter * 100
  if len(kw_dict) > 0:
    kw_dict = sorted(kw_dict.items(), 
                     key = lambda x: x[1], 
                     reverse = True)[0:KW_THRESHOLD-1]
    kw_dict = normalize(kw_dict, True)
    print("node_hash = {}")
    print("trend = Trend.create(label: '{0}', collected:'{1}')"
            .format(escape(keyword), collected))
    pp_node(kw_dict)
  corr_dict = {}
  for src in kw_dict:
    for dst in kw_dict:
      if src == dst: continue
      src_w = src[0]
      dst_w = dst[0]
      sd_pair = src_w + "," + dst_w
      if sd_pair in corr_dict: continue
      prob = 0.0
      for ary in ary_of_ary:
        if ((src_w in ary) & (dst_w in ary)): prob = prob + 1.0
      prob = 100 * prob / len(ary_of_ary)
      if prob > 0: corr_dict[dst_w + "," + src_w] = prob
  if len(corr_dict) > 0:
    lk_dict = sorted(corr_dict.items(), 
                     key = lambda x: x[1], reverse = True)
    # 後半3/4をカットして短くする
    lk_dict = lk_dict[0:int(len(lk_dict)*1/4)]
    if len(lk_dict) > 0:
      lk_dict = normalize(lk_dict, True)
      pp_link(lk_dict)

図6 トレンドを収集して分析するプログラムのコード(その3)

###########################################################
# すでに登録済みか否かのチェック
###########################################################
import sqlite3
from contextlib import closing
def has_been_registered(keyword, collected):
  # 「PATH_TO_THE_DATABASE_FILE」の部分は環境に合わせて修正
  dbfile = "PATH_TO_THE_DATABASE_FILE(development.sqlite3)"
  with closing(sqlite3.connect(dbfile)) as conn:
    c = conn.cursor()
    sql = 'select label, collected from trends \
           where label=? and collected=?'
    res = (keyword, collected)
    c.execute(sql, res)
    return (len(c.fetchall()) > 0)
###########################################################
# メイン関数
###########################################################
from twitter import *
from datetime import date
def main():
  # WOEID を希望の場所に合わせて指定。「23424856」は日本
  woeid = 23424856
  # アプリ登録時に取得した情報を下記に設定
  CK = 'ADD_HERE_YOUR_CONSUMER_KEY'
  CS = 'ADD_HERE_YOUR_CONSUMER_SECRET'
  AT = 'ADD_HERE_YOUR_ACCESS_TOKEN'
  AS = 'ADD_HERE_YOUR_ACCESS_TOKEN_SECRET'
  twitter = Twitter(auth = OAuth(AT,AS,CK,CS))
  results = twitter.trends.place(_id = woeid, exclude="hashtags")
  d = date.today()
  collected = d.strftime('%Y-%m-%d')
  for location in results:
    for trend in location["trends"]:
      keyword = trend["name"]
      if has_been_registered(keyword, collected): continue
      query_kw = keyword + " exclude:retweets exclude:nativeretweets"
      tw_rslts = twitter.search.tweets(q=query_kw, 
                                       lang='ja', locale='ja', count=100)
      tw_ary = []
      for tweet in tw_rslts["statuses"]: tw_ary.append(tweet["text"])
      create_graph(keyword, collected, tw_ary)
if __name__ == "__main__": main()

図7 ワードグラフ作成部で出力されるRubyスクリプトの例

node_hash = {}
trend = Trend.create(label: 'ホシガリス', collected:'2020-06-07')
node_hash['アニポケ'] = trend.nodes.create(word: 'アニポケ', freq: 100.00)
node_hash['ポケモン'] = trend.nodes.create(word: 'ポケモン', freq:  56.76)
node_hash['ゴルーグ'] = trend.nodes.create(word: 'ゴルーグ', freq:  29.73)
node_hash['思'] = trend.nodes.create(word: '思', freq:  18.92)
(略)
node_hash['ピカチュウ'] = trend.nodes.create(word: 'ピカチュウ', freq:  5.41)
node_hash['パチリス'] = trend.nodes.create(word: 'パチリス', freq:  5.41)
node_hash['スピアー'] = trend.nodes.create(word: 'スピアー', freq:  5.41)
link = Link.create(corr: 100.00)
node_hash['ポケモン'].links << link
node_hash['アニポケ'].links << link
link = Link.create(corr:  75.00)
node_hash['ゴルーグ'].links << link
node_hash['アニポケ'].links << link
(略)
link = Link.create(corr:  25.00)
node_hash['マユルド'].links << link
node_hash['ドクケイル'].links << link
node_hash = {}
trend = Trend.create(label: '孤独のグルメ', collected:'2020-06-07')
(略)