ハンバーグのおいしい季節

いっつもうめえよ

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してみる。

f:id:shin_nandesu:20190109025709p:plainf:id:shin_nandesu:20190109025743p:plain
f:id:shin_nandesu:20190109025719p:plainf:id:shin_nandesu:20190109025755p:plain

全員に共通して言えるのが、各人の誕生日周辺(上から1月,7月,9月,8月)が突出してリプライが多くなっている。あとはオタクにリプ辺をしている日とか、単純にツイートの数が多い日とかにリプライが多くなっていた。

時間別のリプライ数
メンバーに対するリプライをする時間をplotしてみる。
各メンバーごとにplotしてみたがあまり差がなかったので、全員に対するリプライ数をみる。
f:id:shin_nandesu:20190109234558p:plain

リプライ時間≒オタクが起きている時間であり、0時を越えるとリプライ数が減少するのでオタクは日々正しい生活を送っていることがわかる。

メンバーに対するリプライの内容

最後に、リプライの内容から頻出単語をメンバーごとにplotしてみる。

f:id:shin_nandesu:20190109235038p:plainf:id:shin_nandesu:20190109235050p:plainf:id:shin_nandesu:20190109235103p:plainf:id:shin_nandesu:20190109235107p:plain


フィロソフィーのダンスのメンバー全員に共通したワードや、各メンバーのニックネームなどが上位を占めるが、日向ハル(@halu_philosophy)さん
には「ゴリゴリ*1」だったり、奥津マリリ(@philosophy092)さんには「ビール」、だったり、佐藤まりあ(@_satomaria)さんには「りん*2」だったり、
十束おとは(@ttk_philosophy)さんには「ゲーム」と、各メンバーによって特色があるリプライがされていることがわかった。
あと、知っていはいたけどオタクは推しに対してめちゃくちゃ可愛いとか好きってリプライするし、めちゃくちゃライブを楽しみにしているのだなと再認識した。

結論

2018年をリプライで振り返ってみた。この記事を書いているうちに2019年になってしまったけれど、2019年も何か面白い形で振り返りたい。

*1:日向ハルさんは自称アイドル界のゴリゴリのゴリ

*2:佐藤まりあさんが飼っている柴犬の名前

dvipdfmxが通らない時の対策をしよう

Texで編集する機会があったので、久しぶりに触ったらdvipdfmxのコンパイルが通らなかったのでメモ。前までは同環境で動いてたのに。

環境

macOS High Sierra

エラー

$ 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などの無料コンテンツは私的利用のためのダウンロードなら可能という認識ですが、間違っていたらご指摘ください。

Webページ更新を自動チェックして通知させよう

以前、フィロソフィーのダンスというアイドルグループのクラウドファンディングに応募する際にモタモタ迷っていたら希望のリターンが枯れたという悲劇がありました。
しかし、コンビニ支払いを指定した人が支払いをしないことでキャンセル分が出現すると予測し、自動チェックプログラムを書くことにしました。

おそらく、この手のサービスやアプリは山ほどありそうだけど、なんとなく自分で作ってみたかったのでそのメモ。

自分が狙っていたリターンは40人限定で数分で完売したものでした。そこから、キャンセルするのは多くて2,3人…と踏んだため、誰よりも早くキャンセル出現に気づく必要があります。
この「キャンセル分早い者勝ち王決定戦」は絶対に負けられない戦いです。名前がダサい。

簡単な流れ

1.ページのtextデータを保存
2.毎分更新して比較
3.内容に変更があったら通知

URLのtextデータを保存

まず、クラウドファンディングのページに行ってhtmlタグを確認します。

f:id:shin_nandesu:20180526185245p:plain

(なんとなくリターンの内容は隠した)

これを観てみるとわかる通り、「OUT OF STOCK」の赤文字の部分が売り切れを示しています。

ちなみに、在庫があるリターンの表示はこちらで、選択ボタンが追加されます。

f:id:shin_nandesu:20180526185527j:plain

つまり、「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に投稿する

完成

以上をもって、webページの指定部分が更新されたらSlackに通知されるプログラムができました。

f:id:shin_nandesu:20180526173808p:plain

こんな感じで通知が来るし、スマホにもプッシュ通知がきます。嬉しい。

使用感

わりと幅広くclassを指定してしまったせいか、頻繁に通知が来ました。結構うるさい。
と最初は思っていましたが、それも2、3日で慣れてそれよりかちゃんとシステムが動いていてwebページを監視しているのがなんか楽しかった。

結果

コンビニ支払い選択者の支払い期限が3/10 23:59:59だったので次の日あたりに通知が来るかな、と考えていたのですが見事3/11の13:50あたりに通知が来て2枠キャンセル分の余りが出ました。無事買えて、胸をなでおろす。

f:id:shin_nandesu:20180526221237j:plain

なんだろう、パトロンになりましたのパワーワード感。

終わりに

とりあえず要件を満たすものをエイヤと作ってみた結果、キャンセル分のリターンを獲得してパトロンになることができました。
今はこのプログラムを拡張してロシアW杯の準決勝のチケットをキャンセル待ちしています。同じことを考えている人が世界には山ほどいそうなので、毎秒スクリプト回すとかしてなんとかチケットを勝ち取りたいです。