Twitterのリプライで2018年を振り返ろう
2018年も終わるにあたってSlackで一年を振り返るという記事を読み、自分もTwitterとかを使ってなにか振り返ってみたいという衝動に駆られた。
よくやられているのは、ツイート履歴からよく使われる単語を抽出してその人がどんな1年だったか振り返るという手法。
なんか独自性のあるものがやりたかったので、他人から特定の人に対するリプライでその人にとってどんな一年だったのかを振り返ってみたいと考えた。他人からの意見(リプライ)がもしその人を写す鏡になっていたら面白いなと。
Twitterデータ収集
いつもの通り、改悪された公式のTwitterAPIを使用すると過去7日のツイートしか取得できない。これでは1年が振り返れないではないか。
そこで検索してみると、GetOldTweets-pythonというリポジトリがgithubにて公開されていた。
https://github.com/Jefferson-Henrique/GetOldTweets-python
こちらを使用することで過去7日以上のツイートが抽出できる。非公式のため使用は自己責任で。
$ git clone https://github.com/Jefferson-Henrique/GetOldTweets-python $ cd GetOldTweets-python $ pip install -r requirements.txt
以上セットアップ完了。
import sys import pandas as pd if sys.version_info[0] < 3: import got else: import got3 as got tweetCriteria = got.manager.TweetCriteria().setQuerySearch('hoge').setSince("2015-05-01").setUntil("2015-09-30").setMaxTweets(1) tweet = got.manager.TweetManager.getTweets(tweetCriteria)[0] print(tweet.text)
setQuerySearchの引数に検索したいワード、setSinceとsetUntilの引数に取得したい日付、setMaxTweetsに取得したツイート件数を入れればすぐ検索できる。
ちなみに、setMaxTweetsは2000とかたくさん件数を指定することも可能だが、処理が非常に重くなるため20件くらいでwhileループさせる方がいい。
検索日時を秒単位で指定したい場合は、"2018-01-01_00:00:00_JST"などと書くらしい。また、検索ワードにを"to:ariyoshihiroiki"とすると、@ariyoshihiroikiに対するリプライのみ抽出が可能である。初めて知ったしTwitterの公式検索でも役立ちそう。
since = "2018-01-01_00:00:00_JST" until = "2018-12-21_23:59:59_JST" df_tweet = pd.DataFrame(columns=['id', 'username','text','date']) while(until[0:4] == '2018'): tweetCriteria = got.manager.TweetCriteria().setQuerySearch('to:ariyoshihiroiki').setSince(since).setUntil(until).setMaxTweets(20) for i in range(20): tweet = got.manager.TweetManager.getTweets(tweetCriteria)[i] tmp_se = pd.Series( [ tweet.id, tweet.username, tweet.text,tweet.date], index=df_tweet.columns ) df_tweet = df_tweet.append( tmp_se, ignore_index=True ) k1,k2 = str(tweet.date).split() until = "{}_{}_JST".format(k1,k2) print(until) df_tweet.to_csv('tweet.csv')
こんな感じで一年分のツイートの取得が可能になった。
リプライ分析
今回は、自分へのリプライを見てもあまり面白くなさそうなので、自分の好きなアイドルグループである「フィロソフィーのダンス」のメンバーを対象に分析してみる。
アイドルに対するファン・オタクのリプライは数も多いことながらバラエティに富んでいる印象なので、抽出してみるとなにか面白い結果が出そう。
日向ハル(@halu_philosophy)さん
奥津マリリ(@philosophy092)さん
佐藤まりあ(@_satomaria)さん
十束おとは(@ttk_philosophy)さん
の4人に対する2018年の一年間のリプライを先ほどの手法で抽出した。(スクリプトずっと回していたら丸1日くらいかかった)
1日あたりのリプライ数
まずは、リプライ数を1日ごとに集計してplotしてみる。
全員に共通して言えるのが、各人の誕生日周辺(上から1月,7月,9月,8月)が突出してリプライが多くなっている。あとはオタクにリプ辺をしている日とか、単純にツイートの数が多い日とかにリプライが多くなっていた。
時間別のリプライ数
メンバーに対するリプライをする時間をplotしてみる。
各メンバーごとにplotしてみたがあまり差がなかったので、全員に対するリプライ数をみる。
リプライ時間≒オタクが起きている時間であり、0時を越えるとリプライ数が減少するのでオタクは日々正しい生活を送っていることがわかる。
メンバーに対するリプライの内容
最後に、リプライの内容から頻出単語をメンバーごとにplotしてみる。
フィロソフィーのダンスのメンバー全員に共通したワードや、各メンバーのニックネームなどが上位を占めるが、日向ハル(@halu_philosophy)さん
には「ゴリゴリ*1」だったり、奥津マリリ(@philosophy092)さんには「ビール」、だったり、佐藤まりあ(@_satomaria)さんには「りん*2」だったり、
十束おとは(@ttk_philosophy)さんには「ゲーム」と、各メンバーによって特色があるリプライがされていることがわかった。
あと、知っていはいたけどオタクは推しに対してめちゃくちゃ可愛いとか好きってリプライするし、めちゃくちゃライブを楽しみにしているのだなと再認識した。
結論
2018年をリプライで振り返ってみた。この記事を書いているうちに2019年になってしまったけれど、2019年も何か面白い形で振り返りたい。
dvipdfmxが通らない時の対策をしよう
Texで編集する機会があったので、久しぶりに触ったらdvipdfmxのコンパイルが通らなかったのでメモ。前までは同環境で動いてたのに。
エラー
$ dvipdfmx hoge.dvi
上記コマンドを実行しようとすると、下記で怒られる。
** WARNING ** Filtering file via -->rungs -q -dNOPAUSE -dBATCH -dEPSCrop -sPAPERSIZE=a0 ..... ** ERROR ** pdf_ref_obj(): passed invalid object.
対策1
command -->rungs...で怒られているので、/usr/local/texlive/2017/texmf-config/dvipdfmx/dvipdfmx.cfgを編集する必要があるらしい。
D "rungs -q -dNOPAUSE -dBATCH -dEPSCrop -sPAPERSIZE=a0 -sDEVICE=pdfwrite -dCompatibilityLevel=%v -dAutoFilterGrayImages=false -dGrayImageFilter=/FlateEncode -dAutoFilterColorImage s=false -dColorImageFilter=/FlateEncode -dAutoRotatePages=/None -sOutputFile='%o' '%i' -c quit" ↓ D "gs -q -dNOPAUSE -dBATCH -dEPSCrop -sPAPERSIZE=a0 -sDEVICE=pdfwrite -dCompatibilityLevel=%v -dAutoFilterGrayImages=false -dGrayImageFilter=/FlateEncode -dAutoFilterColorImage s=false -dColorImageFilter=/FlateEncode -dAutoRotatePages=/None -sOutputFile='%o' '%i' -c quit"
これで解決だ!と思い実行してみるが、コンパイル通らず。
対策2
ghostscriptをもう一度入れ直してみる。
$ brew install ghostscript
gsはシンボリックリンクなのでリンクを貼り直す。
$ rm /usr/local/bin/gs $ ln -s /usr/local/Cellar/ghostscript/9.26/bin/gs /usr/local/bin/gs
バージョンを確認してみる。
$ gs -v GPL Ghostscript 9.26 (2018-11-20) Copyright (C) 2018 Artifex Software, Inc. All rights reserved.
最新になってるので、実行してみる。
$ dvipdfmx hoge.dvi hoge.dvi -> hoge.pdf [1][2][3][4][5][6][7][8][9][10] 5443026 bytes written
できた。
YouTubeチャンネルの動画を自動で全保存してみよう
先日、僕が応援する某アイドルのシンガーソングライター名義でのYouTubeチャンネルが大人の事情で削除されるかもしれないという情報を聞きつけ、消される前に動画を全保存しなければ。という使命感に駆られました。
また最近では、そのアイドルグループのMVやライブ映像もメジャーデビューに先駆け(?)削除されたり、静岡朝日SunsetTVで配信されている僕が好きなコンテンツの「Aマッソのゲラニチョビ」がプロデューサーの逮捕により全削除されたりと(現在は一部復活)、YouTube上の動画もいつ見れなくなるかわからない状況です。
Clipboxとかの既存ダウンロードツールもありますが、一つ一つやるの億劫なので自動化しようと思い、そのメモです。
こちらを参考にさせていただきました。
PythonでYoutubeの動画をダウンロードするまで(2017/11/30現在)
私的利用のためのダウンロードであり、再配布やアップロードなどの法的悪用をするつもりはありませんのでご留意ください。
YouTubeチャンネルの動画リスト抽出
YouTube Data API (v3)のSearch APIを用いることでキーワード検索やチャンネル検索などいろいろできました。
公式ドキュメントにサンプルコードがあったので、そのまま流用。
https://developers.google.com/youtube/v3/code_samples/python#search_by_keyword
パラメータにChannelIDを指定するだけで、そのチャンネルから投稿された動画をlistで返してくれます。
ChannelIDは
https://www.youtube.com/channel/XXXXXXXXX
のXXX部分に該当します。
また、一回のリクエストで50件しか取得できない問題があるので、それ以上動画投稿してるチャンネルとかだと繰り返し処理する必要があります。
search APIを投げた際に指定した件数(maxResults)以上の件数動画が存在する場合はnextPageTokenというパラメータが帰って来るので、リクエストパラメータのpagetokenに指定して再帰的に処理することで全件取得しました。
この辺はこちらを参考にしました。500件以上だとできない問題あるらしいけど今回僕の対象はそこまで動画が多くないので今は無視。
YouTubeの特定のチャンネルに紐付く動画を取得する。(YouTube Data API (v3) Search)
from apiclient.discovery import build from oauth2client.tools import argparser DEVELOPER_KEY = "自分のキー" YOUTUBE_API_SERVICE_NAME = "youtube" YOUTUBE_API_VERSION = "v3" ChannelId = "UC7MyccaiE4MYa9ERzqZYY4Q" #チャンネルID videos = [] #videoURLを格納する配列 def youtube_search(pagetoken): youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=DEVELOPER_KEY) search_response = youtube.search().list( # q=options.q, part="snippet", channelId= ChannelId, maxResults=50, order="date", #日付順にソート pageToken=pagetoken #再帰的に指定 ).execute() for search_result in search_response.get("items", []): if search_result["id"]["kind"] == "youtube#video": videos.append(search_result["id"]["videoId"]) try: nextPagetoken = search_response["nextPageToken"] #nextPageTokenが返ってくる限り処理を繰り返す youtube_search(nextPagetoken) except: return
YouTube動画の保存
Pythonにはpytubeというモジュールがあり、これがURLを引数に指定するだけで保存してくれる超優れものだったので使用しました。
ネット環境にも依存しますが、5分の動画を30秒くらいで保存してくれるのでまじ速い。もっと早く使えばよかったと思いました。
from pytube import YouTube def save_video(search_list): for ID in search_list: query = 'https://www.youtube.com/watch?v=' + ID print(query+"を保存") yt = YouTube(query) yt.streams.filter(subtype='mp4').first().download("./videos")
これらを実行すると、
https://www.youtube.com/watch?v=PhYT-_l2lMkを保存 https://www.youtube.com/watch?v=Ty0J4YL9BwYを保存 https://www.youtube.com/watch?v=zBrA2nztpu8を保存 ・ ・ ・
みたいに自動でチャンネル内全ての動画がローカルに保存されます。やったね。
最後に
僕は在宅時代、このシンガーソングライターさんのYouTube動画を見て、ライブ反省会と題した動画の中で自らのことをいちいちブスだの可愛いだのと一喜一憂する姿がとても人間味があって面白い人だなと感じ、彼女が所属するアイドル現場に足を運ぶきっかけとなりました。
このご時世YouTubeなどによる影響力はものすごいし、無料コンテンツながらもバタフライエフェクト的に経済を回していると思います。ですので、素晴らしい動画コンテンツが大人の事情で削除されるのは非常に勿体無いなぁと思いつつ、今回自動ダウンロードを試みました。
法律に詳しくないのでYouTubeなどの無料コンテンツは私的利用のためのダウンロードなら可能という認識ですが、間違っていたらご指摘ください。
Twitterのツイート情報から生活リズムを把握しよう
僕が応援するフィロソフィーのダンスというアイドルグループの十束おとはさん(以下、おとはす)が以前、こんなツイートをしていました。
ツイッター徘徊しすぎてみんなの生活リズムが分かってきました(お昼にほっこりするツイート)
— 十束おとは🎮フィロソフィーのダンス (@ttk_philosophy) 2018年2月9日
このツイートを見てから、目には目を歯に歯を!的な精神でおとはすの生活リズムをツイート情報から定量的に把握できないかな、と思い生活リズムの把握を試みました。
※興味本位なものであり、決して悪用目的やプライバシーを侵害する事が目的ではありません。
ツイート投稿時間からの把握
まずはツイート投稿時間をプロットして、何時くらいのツイートが多いのか見てみようと思いました。
TwitterAPIでは一回のリクエストで200件のツイートが取得できるので、API制限上で取得可能な上限の3200件を取得するために16回同じ処理を回します。投稿時間はGETメソッドを用いてstatuses/user_timeline.jsonの"created_at"から取得可能です。
APIを使用するにあたりOAuth認証周りがめんどくさいのでRequests-OAuthlib というライブラリを使用しました。以下参考。
Python で Twitter API にアクセス - Qiita
from requests_oauthlib import OAuth1Session import json import config #対象者のTwitterID screen_name = "ttk_philosophy" def get_all_tweets(): #config.py に取得したキーなどを登録しておく CK = config.CONSUMER_KEY CS = config.CONSUMER_SECRET AT = config.ACCESS_TOKEN ATS = config.ACCESS_TOKEN_SECRET # OAuth認証 twitter = OAuth1Session(CK, CS, AT, ATS) #timelineのURL url = "https://api.twitter.com/1.1/statuses/user_timeline.json" max_id = None timeCounter = [0 for i in range(24)] for i in range(16): #パラメータ指定 params = {"count" : 200 ,"screen_name": screen_name,"max_id":max_id} req = twitter.get(url, params = params) # レスポンスを確認 if req.status_code == 200: tweets = json.loads(req.text) max_id = tweets[-1]["id"]-1 for tweet in tweets: # TimeZoneをAsia/Tokyoにする time = tweet["created_at"].split() hour = int(time[3][0:2])+9 hour = hour -24 if hour >= 24 else hour timeCounter[hour]+=1 else: print ("Error: %d" % req.status_code) return timeCounter def plot_bar_chart(x,y): plt.title(screen_name+type) #タイトル plt.bar(x,y,color="gold",alpha=0.7) #カラー指定してbar plot plt.xticks(x) #x軸目盛 plt.ylim([0,600]) #y軸目盛 plt.xlabel('Hour') plt.ylabel('Tweets') plt.savefig(screen_name+".png") #画像として保存 if __name__ == '__main__': x_axis = range(24) y_axis = get_all_tweet() plot_bar_chart(x_axis,y_axis)
max_idを指定することで直前の処理で取得した最後のツイートの一個前のツイートから取得することが可能です。
取得した3200件のツイートを1時間毎のツイート投稿時間を棒グラフにしてみます。可視化ライブラリはmatplotlibを使用しました。
予想
おとはすさんはアイドルの中でも割と早寝早起きタイプだと思われます。以前、他のメンバーから「おとちゃんはゲーマーなのに早寝早起きだからLINE送っても早朝に返信が来る」的なことを聞いたことがあったので、深夜のツイートは少ないと予測しました。
結果
な、なんと規則正しい生活...!
深夜3時以降のツイートはほとんどなく、8時台から朝のツイートをし始めている!
アイドルでゲーマーという、なんとも不規則そうな生活を送っているのに早寝早起きをしているおとはすはエライ!
ツイート内容からの把握
おとはすさんのツイート内容には、就寝時と起床時の一貫したルールが存在します。
・起床時
╭◜◝ ͡ ◜◝╮/
— 十束おとは🎮フィロソフィーのダンス (@ttk_philosophy) 2018年6月6日
( •ω• )もくもくもくようびおはす〜
╰◟◞ ͜ ◟◞╯\
・就寝時
みんなは明日お休みかな?おとはすは明日お休みなのでたくさんエゴサしてから寝るゾ〜〜わいわいლ(^ω^ლ) ლ(^ω^ლ) おやはすლ(^ω^ლ) わいわい
— 十束おとは🎮フィロソフィーのダンス (@ttk_philosophy) 2018年4月29日
このように、朝一番のツイートには「おはす〜」、寝る前のツイートには「おやはす〜」という文字列が混在していることがわかります。
これらは、朝の挨拶の「おはよう」及び就寝時の挨拶の「おやすみ」に愛称である「おとはす」の"はす"の部分を結合したものでありマス!説明不要だと思いますが!
そのため、先ほど取得した3200件のツイートから「おはす」「おやはす」という文字列が混在するツイートをフィルタリングして、起床時間と就寝時間を見てみたいと思います。
ちなみに、午前中にツイートをせず昼を迎えた場合には「昼はす〜」という固有の挨拶も存在しますが、割愛しました。
結果
起床時と就寝時のツイートをプロットし、平均時間を算出しました。
・起床時のツイート時間
#起床ツイートの平均時間 10:31:47
・就寝時のツイート時間
#就寝ツイートの平均時間 0:54:17
以上の結果から、おとはすさんは、平均として10時31分47秒くらいに「おはす〜」を投稿し、0時54分17秒あたりに「おやはす〜」を投稿することが分かりました。
素晴らしく規則正しい生活、見習いたいです。
まとめ
興味本位で生活リズムを把握を試みましたが、予想通りと言えば予想通りの結果が現れました。
今回分析の対象とさせてくれた十束おとはさんには大変感謝しています。
ちなみにおとはすさんははてなブロガーなので、こちらもぜひ。
otohatotsuka.hatenablog.com
Webページ更新を自動チェックして通知させよう
以前、フィロソフィーのダンスというアイドルグループのクラウドファンディングに応募する際にモタモタ迷っていたら希望のリターンが枯れたという悲劇がありました。
しかし、コンビニ支払いを指定した人が支払いをしないことでキャンセル分が出現すると予測し、自動チェックプログラムを書くことにしました。
おそらく、この手のサービスやアプリは山ほどありそうだけど、なんとなく自分で作ってみたかったのでそのメモ。
自分が狙っていたリターンは40人限定で数分で完売したものでした。そこから、キャンセルするのは多くて2,3人…と踏んだため、誰よりも早くキャンセル出現に気づく必要があります。
この「キャンセル分早い者勝ち王決定戦」は絶対に負けられない戦いです。名前がダサい。
簡単な流れ
1.ページのtextデータを保存
2.毎分更新して比較
3.内容に変更があったら通知
URLのtextデータを保存
まず、クラウドファンディングのページに行ってhtmlタグを確認します。
(なんとなくリターンの内容は隠した)
これを観てみるとわかる通り、「OUT OF STOCK」の赤文字の部分が売り切れを示しています。
ちなみに、在庫があるリターンの表示はこちらで、選択ボタンが追加されます。
つまり、「OUT OF STOCK」という文字列が「残りn人まで」に変更したら通知を促せばよいのです。
そのため、この文字列が該当する部分のclass属性を抽出して、txtファイルに保存しました。
プログラムはPythonにて実装し、使用モジュールは以下の通りです。
・ requests
・ BeautifulSoup
パースの部分はこちらを参考にしました。
https://qiita.com/itkr/items/513318a9b5b92bd56185
import requests import bs4 def get_website(): url = 'hoge' file = 'hoge.txt' res = requests.get(url) res.raise_for_status() soup = bs4.BeautifulSoup(res.text,'html.parser')# Parser elems = soup.select('.limited.rfloat') # class要素の取得 str_elems = str(elems) # stringに変換 try: f = open(file) old_elems = f.read() except: old_elems = ' ' if(str_elems == old_elems): return False else: f = open(file, 'w') # 上書きする f.writelines(str_elems) f.close() return True if __name__ == "__main__": if(get_website()): #slackに通知する
思ったよりめちゃくちゃ簡単に書けました。
txtファイルにいちいち書き出さないようにするとか、改善点はありますがとりあえず要件を満たしたものができたので満足。
2.毎分更新して比較
スクリプトを自前サーバに置いて、crontabにて毎分実行させました。
* * * * * python3 check.py
3.内容に変更があったら通知
保存したtxtファイルと、新しく取得したtxtファイルの内容が異なっていたらslack botにて通知を促すことにしました。
Slack通知は以下参考。
PythonでSlackに投稿する
使用感
わりと幅広くclassを指定してしまったせいか、頻繁に通知が来ました。結構うるさい。
と最初は思っていましたが、それも2、3日で慣れてそれよりかちゃんとシステムが動いていてwebページを監視しているのがなんか楽しかった。
結果
コンビニ支払い選択者の支払い期限が3/10 23:59:59だったので次の日あたりに通知が来るかな、と考えていたのですが見事3/11の13:50あたりに通知が来て2枠キャンセル分の余りが出ました。無事買えて、胸をなでおろす。