3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ツイート文中の地名の抽出とビジュアライゼーション

Last updated at Posted at 2021-04-24

概要

ツイートをもとに地図上にマーカーを表示する。

  1. 任意のキーワードでツイッター上を検索しツイートを取得する
  2. 取得したツイートから地名を抽出する(形態素解析)
  3. 地名から位置座標を取得する(ジオコーディング)
  4. 地図上に表示する

余談
ある夜、ぐっすり眠っていたところを緊急地震速報に叩き起こされ、眠れず手持ち無沙汰でTwitterを開いたとき「此方は停電があって照明が付きません」だのなんだの停電被害の書き込みがわらわらと出てくるのを見た。そこで「停電」を含む書き込みの地図上の分布を知りたくなった。
プロフィールやツイート本文から地名を抽出してジオコーディングすることにした。

目次

手順

環境

  • Windows 10 Home 64bit
  • python 3.8.8
  • Visual Studio Code 1.55.2

TwitterAPI各種キー・トークン取得

TwitterAPIって何?

  • Twitterにある情報をできるだけ広く共有するために
  • API **(アプリケーション・プログラミング・インタフェイス)**を通して
  • 企業、開発者、利用者に提供します。

TwitterのAPIについて

TwitterAPIでは以下のキーが必要である。Twitter開発者アカウントを申請し取得する。

  • Consumer key
  • Consumer secret
  • Access token
  • Access token secret

Use Cases, Tutorials, & Documentation | Twitter Developer

参考記事様

Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報 - Qiita

各種キーはconfig.pyとして保存しておく。

CONSUMER_KEY = "*****"
CONSUMER_SECRET = "*****"
ACCESS_TOKEN = "*****"
ACCESS_TOKEN_SECRET = "*****"

ツイートを取得してjsonを保存する

  • 「停電」で検索し、結果をjsonファイルとして保存する
  • 日付時刻を名前としてフォルダを作成し、そこに保存する
  • TwitterAPIは一度に100件しかくれない。100回繰り返して10000件もらうことにする
    • 取得するツイートが被っては困るので、ツイートidの最大値を指定して「それ以前」のツイートに限定する
import json
import datetime
import config # さっき保存したキー
import os
import time
from requests_oauthlib import OAuth1Session

# 各キーの読み込み
CK = config.CONSUMER_KEY
CS = config.CONSUMER_SECRET
AT = config.ACCESS_TOKEN
ATS = config.ACCESS_TOKEN_SECRET

# 認証情報
twitter = OAuth1Session(CK, CS, AT, ATS)

# 保存フォルダ作成
now = datetime.datetime.now()
strNow = now.strftime('%Y%m%d_%H%M%S')
saving_directory_path = f'tweets_{strNow}'
os.mkdir(saving_directory_path)

# クエリを作成
url = 'https://api.twitter.com/1.1/search/tweets.json'
keyword = ['停電']
params = {
    'q': keyword,
    'lang': 'ja',
    'locale': 'ja',
    'result_type': 'recent',
    'count': 100,
    'tweet_mode': 'extended',  # full text
    'exclude': 'retweets'
}

for i in range(100):
    # API制限回避のため
    time.sleep(5)

    # リクエストを行う
    req = twitter.get(url, params=params)
    
    # 応答が正常ならば
    if req.status_code == 200:
        print('fin write a response')
        
        # json
        res = json.loads(req.text)

        # idの最小値をクエリに追加
        idList = []
        for index, tweet in enumerate(res["statuses"]):
            idList.append(tweet[u'id'])
        params['max_id'] = min(idList) - 1

        # ファイル保存
        now = datetime.datetime.now()
        strNow = now.strftime('%Y%m%d_%H%M%S')
        with open(f'{saving_directory_path}\{strNow}.json', mode='wt', encoding='utf-8') as file:
            json.dump(res, file, ensure_ascii=False, indent=2)

        print(f'get : {i+1}')

    else:  # 応答が正常でなければ
        print('Failed your requests, status_code: {}'.format(req.status_code))
        break

jsonファイルを読み込む

  • 参照するディレクトリ(さっき保存したやつ)を選ぶ
import os

# 参照jsonの設定

# カレントディレクトリ下に存在するディレクトリのリストを作る
path = "./"
files = os.listdir(path)
files_dir = [f for f in files if os.path.isdir(os.path.join(path, f))]

# ディレクトリのリストを出力
print('switch directory')
for i,item in enumerate(files_dir):
    print(i,item)

# 入力
dirNum = input("item number?")
input_dir = f'./{files_dir[int(dirNum)]}'
fileList = os.listdir(input_dir)

print(f'\n{input_dir}\n------------ file list ------------')
for i,item in enumerate(fileList):
    print(f'{i} \t {item}')
  • 選んだディレクトリ下のjsonファイルを読んで、リストに格納する
  • とりあえず テキスト、日付時刻、所在地を拾うことにする
import json

textList = []
createdAtList = []
locationList = []
idList = []

# jsonファイルを検討する
for json_file in fileList:
    json_path = f'{input_dir}/{json_file}'
    json_open = open(json_path, 'r',encoding="utf-8") # jsonを読み込む
    res = json.load(json_open)

    # jsonを読んでいく
    for tweet in  res["statuses"]:
        textList.append(tweet[u'full_text'])
        createdAtList.append(tweet[u'created_at'])
        locationList.append(tweet[u'user'][u'location'])

print(min(createdAtList),' -> ',max(createdAtList))
print('Done.')

形態素解析して地名を抽出する

MeCabをインストールする。

MeCabは 京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクト を通じて開発されたオープンソース 形態素解析エンジンです。
MeCab: Yet Another Part-of-Speech and Morphological Analyzer (taku910.github.io)

インストールできたらパスを通しておく。
Path C:\Program Files (x86)\MeCab\bin
MECABRC C:\Program Files (x86)\MeCab\etc\mecabrc

python側のパッケージはmecab-pythonを使用する。

  • 地名を抽出し辞書配列にキーとして格納していく
    • カタカナや広すぎる単語は除外する
    • 複数同じものが存在するときは個数を、キーcountに格納する
    • 都道府県であるか、キーprefecturesに真偽を格納する
import MeCab, unidic_lite, re

# 除外ワードの設定
def WordVerification(text):
    
    # 正規表現でカタカナを除外する
    regex = u'^[\u30a1-\u30fa\u30fc]+$'
    match = re.match(regex, text)
    if match:
        return False
    
    # リストに一致するワードを除外する
    elif text in [u'新開発', u'日本',u'関東',u'関西',u'東北',u'',u'西',u'',u'']:
        return False

    else:
        return True
    

# 地名だけ抽出する
def PlaceExtracting(text):
    # 形態素解析
    mecab = MeCab.Tagger()
    node = mecab.parseToNode(text)
    locList = []
    while node:
        pos = node.feature.split(",")
        
        # 除外ワードに引っかからなくてかつ地名であるとき
        if WordVerification(node.surface) and pos[2] == "地名":
            locList.append(node.surface)
        node = node.next
    
    return locList

# 都道府県リスト
prefecturesList = [u'愛知',u'青森',u'秋田',u'秋田',u'石川',u'茨城',u'岩手',u'愛媛',u'大分',u'大阪',u'岡山',u'沖縄',u'香川',u'鹿児島',u'神奈川',u'岐阜',u'京都',u'熊本',u'群馬',u'高知',u'埼玉',u'佐賀',u'滋賀',u'静岡',u'島根',u'千葉',u'東京',u'徳島',u'栃木',u'鳥取',u'富山',u'長崎',u'長野',u'奈良',u'新潟',u'兵庫',u'広島',u'福井',u'福岡',u'福島',u'北海道',u'三重',u'宮城',u'宮崎',u'山形',u'山口',u'山梨',u'和歌山']

# ツイート本文と所在地を連結して扱う
combiList = textList + locationList

# 辞書配列を作成
locations_found_dic = {}

for i, loc in enumerate(combiList):
    
    # 地名をリストに抽出する
    location = PlaceExtracting(loc)
    
    # 地名について検討する
    for loc in location:
        # 地名の登場回数を計測
        try:
            locations_found_dic[loc]['count'] += 1
            print(f'{loc}\tcount:{locations_found_dic[loc]["count"]}')
        
        # 既存の辞書配列キーに無いとき、新しくキーを追加する
        except KeyError:    
            locations_found_dic[loc] = {'count':1}
            if loc in prefecturesList:
                locations_found_dic[loc]['prefectures'] = True
            else:
                locations_found_dic[loc]['prefectures'] = False
            print(f'{loc}\tnew!')

print('\nDone.')

Geocoder/OSMでジオコーディングする

  • Geocorderライブラリを通じてOSMさんに問い合わせ、緯度と経度を取得する
  • 取得した内容を辞書配列のcoordキーに格納しておく
  • 内容が無いようであれば、地名キーごと削除する
  • 辞書配列をjsonファイルに保存する
import geocoder

keys = []

print(len(locations_found_dic))

# 辞書配列キーのリストを作成する
for loc in locations_found_dic.keys():
    keys.append(loc)

# キーを検討していく
for i,place in enumerate(keys):
    
    # geocoderでOSMにジオコーディングしてもらう
    ret = geocoder.osm(place,timeout=5.0)
    coords = ret.latlng
    
    # 座標はあったか
    if not coords is None:
        # 辞書に座標を格納しておく
        locations_found_dic[place]['coord'] = coords
        print(i, place, coords)
    
    # なかったらキーごと消す
    else:
        locations_found_dic.pop(place)
        continue

# 日付を名前にして保存
now = datetime.datetime.now()
strNow = now.strftime('%Y%m%d_%H%M%S')
with open(f'{strNow}_dic.json', 'w') as f:
    json.dump(locations_found_dic, f, indent=2, ensure_ascii=False)
print(f'\nsaved as "{strNow}_dic.json"')
print('Done.')

ビジュアライゼーション

  • これは以前に保存した地名を読み込むときに使うプログラム
  • 必須ではない
import os
import glob
import json

# 参照jsonの設定

# jsonファイルのリストを作成する
filesList = glob.glob('./*.json')

print('switch directory')
for i,item in enumerate(filesList):
    print(i,item)

# 入力を促す
dirNum = input("item number?")
input_path = filesList[int(dirNum)]

print(f'input "{input_path}"')

# 辞書として読み込み
with open(input_path) as f:
    locations_found_dic = json.load(f)

#内容の確認
createdAtList = []
for loc in  locations_found_dic.keys():
    print(f'{loc}\tcount:{locations_found_dic[loc]["count"]}')

print(f'total:{len(locations_found_dic.keys())}\nDone.')

地図を描写するためにbasemapを導入する。
どうやらサポートが終了してしまったらしいので、windows用の非公式whlを利用する。
basemap‑1.2.2‑cp38‑cp38‑win_amd64.whl
注意書きがあるので読んでおく。

The files are unofficial (meaning: informal, unrecognized, personal, unsupported, no warranty, no liability, provided "as is") and made available for testing and evaluation purposes.

これらのファイルは非公式( ここでの意味 : 非公式であり認証はなく個人的で、いかなるサポートも保証も責任もなく、「あるがまま」提供される)であり、テストおよび評価の目的で利用可能になる。

Python Extension Packages for Windows - Christoph Gohlke (uci.edu)

県境を描写したいのでjapan_border2ライブラリを利用する。
こちらからコピーできる。

使い方

(略)Pythonパスの通ったフォルダに保存するか、地図のプログラムと同じフォルダに保存します。

Pythonで世界地図-12(日本地図に都府県境界線を追加するライブラリ-2) - Qiita

  • matplotlibとbasemapで地図をプロット
    • japan_border2で都道府県境を描写
  • 辞書配列を読んでリストを作成
  • リスト内容を散布図としてプロット
    • countはサイズで表現する
    • 都道府県は黄色、ほか市区町村などは赤で表示
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
import japan_border2 as jb2
import time
import datetime

# 表示する範囲を緯度経度で指定
top=50
bottom=25
right=151
left=125

# 表示するサイズ
fig=plt.figure(figsize=(10,10))

# 地図を描く
map = Basemap(projection='merc',llcrnrlat = bottom,urcrnrlat = top,llcrnrlon = left,urcrnrlon = right,resolution = "i")
map.drawcoastlines(linewidth=1,color='#6f6f6f')
map.fillcontinents(color = '#444444',lake_color='#353d7e')
map.drawmapboundary(fill_color='#1b1b1b')
jb2.prefectural_bound(map = map,linewidth=0.5,color='#6f6f6f',linestyle = '-')


# 辞書配列を読んでリストを作る
x = []
y = []
p_x=[]
p_y=[]
s=[]
p_s=[]

for place in locations_found_dic:
    dic = locations_found_dic[place]
    try:
        coords = dic['coord']

        if dic['prefectures']:
            p_x.append(coords[1])
            p_y.append(coords[0])
            p_s.append(dic['count'])
        else:
            x.append(coords[1])
            y.append(coords[0])
            s.append(dic['count'])
    except KeyError:
        print('error')
        continue

# 散布図の要領で座標の場所に丸を描く
map.scatter(p_x, p_y, alpha = 0.4, s = p_s, c='#f4cd27', edgecolor='y',latlon=True,zorder=3)
map.scatter(x,y,alpha=0.4, s = s, c='#cc1b23', edgecolor='r',latlon=True,zorder=3.1)

# 名前を日付時刻にしてJPG保存
now = datetime.datetime.now()
strNow = now.strftime('%Y%m%d_%H%M%S')
plt.savefig(f'IMG_{strNow}.jpg', dpi=600, bbox_inches='tight')

# 出力
plt.show()

結果

検索ワード : 停電

Sat Mar 20 09:27:43 2021 -> Sat Mar 20 09:33:47 2021 (地震直後)
取得数 : 100件

IMG_20210423_151641.jpg

丸が小さくてよくわからない。
countを*100してみる

IMG_20210423_152259.jpg

地震情報 2021年03月20日 18時09分頃発生 最大震度:5強 震源地:宮城県沖(牡鹿半島の北東20km付近) - 日本気象協会 tenki.jp
こちらの情報と見比べてみた。
やりたかったことは概ねできているように思える。

検索ワード : コンビニ

Sun Apr 18 01:21:13 2021 -> Sun Apr 18 07:57:43 2021
取得数 : 1000件
countを*4した

IMG_20210423_154622.jpg

課題

参考サイト

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?