このドキュメントは、ぴょこりんクラスタ Advent Calendar 2015のために書いたものです。
皆さん師走はどうお過ごしでしょうか。なかなか忙しく、ツイッターに張り付いていられない日々を送っているのではないかと思います。自分が見ていないうちに興味のある内容のツイートが流れてしまった・・・なんてこともしばしば。実際のところ僕もすべてのサナララツイートを追うことが出来ず、日々心を痛めています。そこで、多くのサナララツイートを自動的に収集、僕に見やすいように可視化する枠組みを構築することを決意、今回はツイート収集に関する基礎的な検討を行いました。
とりあえずサクッと叩いてみます。以下のを参考に。
from requests_oauthlib import OAuth1Session
CKey,CSecret, AToken, ASecret=open('./TWOauth','rb').read().strip().split('\n')
knatsuno=OAuth1Session(CKey,CSecret, AToken, ASecret)
query=u'"サナララ"'
uri = 'https://api.twitter.com/1.1/search/tweets.json?q=%s&count=100'%query
req = knatsuno.get(uri)
print uri
https://api.twitter.com/1.1/search/tweets.json?q="サナララ"&count=100
諸々のキーでOAuth認証キメて、上記URLをgetすると結果が返ってくる感じ。ちなみに認証しないと
{"errors":[{"code":215,"message":"Bad Authentication data."}]}
って感じで返ってくる。 少しだけ注釈。
中身はjson形式で格納されていて、以下のような感じで色々見れる。100個並べるとドキュメントが長くなりすぎてつらいので3個だけ。
for i in req.json()[u'statuses'][:3]:
print i[u'created_at']
print i['user'][u'name'] + '(@' + i['user'][u'screen_name'] + u')さんは' + u'言いました。'
print u'『'+i['text']+u' 』と。\n'
Mon Dec 07 09:01:45 +0000 2015 美少女ゲーム関連速報(@GameTwN)さんは言いました。 『【サナララ】 それは…誰にでも起こるかもしれない、少し不思議な物語… 一生に一度だけ全ての人に訪れる「チャンス」 それを告げに来たの...https://t.co/nPJZU8cKL8 https://t.co/6cZGfRXv4l 』と。 Mon Dec 07 08:30:03 +0000 2015 ひだまりスケッチ コピペbot(@hds_cpp_bot)さんは言いました。 『宮子:この前、「ここがサナララの世界か…」って首からトイカメラをぶら下げたお兄さんがいたから、多分違うよ~?っていったら「そうか、だいたいわかった」って言ってバイクでどっか行っちゃった 』と。 Sun Dec 06 21:01:57 +0000 2015 ゆみづか(@yumiduka2015)さんは言いました。 『そう考えるとやっぱサナララは神なんだよなぁ〜〜〜〜〜 』と。
と、こんな感じ。さて、100個取ったはずなので本当に100個あるのか確認してみましょう。
len(req.json()[u'statuses'])
78
なるほど78個、なるほど。
先ほどリンクを張った公式ドキュメントを再確認。クエリのパラメータとしてqやcountの他にツイートの時期を指定するuntilなんてのがあるのですが、そこの説明にこんな注意書きがありました。
Keep in mind that the search index has a 7-day limit. In other words, no tweets will be found for a date older than one week.
過去一週間分しか検索出来ない仕様だと。使えねぇな。検索結果のうしろの方見てみましょう。
for i in req.json()[u'statuses'][:-4:-1]:
print i[u'created_at']
print i['user'][u'name'] + '(@' + i['user'][u'screen_name'] + u')さんは' + u'言いました。'
print u'『'+i['text']+u' 』と。\n'
Sat Nov 28 17:24:13 +0000 2015 キウイ風呂入りたい(@punch_subaru)さんは言いました。 『RT @yumiduka2015: 補正込込込込込でサナララがナンバーワン 』と。 Sat Nov 28 20:57:36 +0000 2015 ひだまりスケッチ コピペbot(@hds_cpp_bot)さんは言いました。 『宮子:この前、「ここがサナララの世界か…」って首からトイカメラをぶら下げたお兄さんがいたから、多分違うよ~?っていったら「そうか、だいたいわかった」って言ってバイクでどっか行っちゃった 』と。 Sat Nov 28 23:24:06 +0000 2015 そうだ エロゲー、やろう!(@pcgame_anime)さんは言いました。 『【美少女ゲーム】サナララR【視聴は→https://t.co/ihbqw3UAK2】エロゲー #エロゲー https://t.co/UJo0fJCHTP 』と。
確かに一週間前っぽい。そうか、だいたいわかった。
神(twitter)によれば一週間しかダメと言うことでした。どこかで聞いたことがある話です。でも一般的にこういうのには抜け道があるはずです。例えばほら、ここで検索するともっと昔のツイートが見つけられる。こんなの納得いかないよ。『観測できれば干渉できる』って胡散臭い小動物的な何かも言ってました。僕は観測できれば何でも干渉できるとは思わないのですが、今回のケースはきっと何とかなります。
サクッと落としてサクッとスクレイピングしてみましょう。これだけなら簡単。
req2 = knatsuno.get(u'https://twitter.com/search?f=tweets&vertical=default&q=%s&src=typd'%query)
from bs4 import BeautifulSoup as bs
import bs4
def strongstrip(s):
if type(s)!=bs4.element.NavigableString and type(s)!=str:
if len(s.contents)==0: s=''
else:
s= s.contents[0]
s=strongstrip(s)
return s
t=bs(req2.content, "lxml").findAll("div", { "class" : "content" })
for text in t[1:4]:
print text.findAll('a')[0]['href'].strip('/')
print text.findAll('strong')[0].contents[0]
print text.findAll('a')[1]['title']
print ''.join([strongstrip(i) for i in text.findAll('p')[0]])
print ''
GameTwN 美少女ゲーム関連速報 1:01 - 2015年12月7日 【サナララ】 それは…誰にでも起こるかもしれない、少し不思議な物語… 一生に一度だけ全ての人に訪れる「チャンス」 それを告げに来たの... pic.twitter.com/6cZGfRXv4l hds_cpp_bot ひだまりスケッチ コピペbot 0:30 - 2015年12月7日 宮子:この前、「ここがサナララの世界か…」って首からトイカメラをぶら下げたお兄さんがいたから、多分違うよ~?っていったら「そうか、だいたいわかった」って言ってバイクでどっか行っちゃった yumiduka2015 ゆみづか 13:01 - 2015年12月6日 そう考えるとやっぱサナララは神なんだよなぁ〜〜〜〜〜
先ほどと同じように上位3ツイートを拾ってみる。スクリーンネーム、大体似たものが取れてる。微妙に時刻が違うんだけどさっきのはGMTで今回表示されてるのはGMT-8だからちょっと注意。ちなみに取れたツイートの個数は、
len(t)-1
20
20個、さっきより得られた結果が減ってる。Web上で表示されるときは最初はちょこっと出てて、スクロールしていくと検索結果が増えてくからまぁこれくらいかもしれない。さすが神のシステム、隙が無いぜ。ただ、仕様上はこっちなら1週間縛りは無いはずなので、何とかここで出た検索結果を出さずに、古い結果だけを出すような検索クエリが投げられれば、古いツイートも手に入るはず。
多分上位20件が表示されるという話だから、それより古いツイートを検索するって機能があれば話は簡単。で、さっきの1週間限定のヤツにはそれがある。
max_id(Optional): Returns results with an ID less than (that is, older than) or equal to the specified ID.
これって実はブラウザ上での検索でも使えるんじゃないの?という話。高度な検索なんかを見てもそんな機能は見当たらないんだけど、もしもこれが使えたら、検索結果のツイートよりさらに古いツイートを検索するのを結果が出なくなるまで続ければかなり古くまでツイートを漁るコトが出きるはず。
results=[]
t=bs(req2.content, "lxml").findAll("div", { "class" : "content" })
for i in range(10):
for text in t[1:]:
results.append([text.findAll('a')[0]['href'].strip('/'),text.findAll('strong')[0].contents[0], \
text.findAll('a')[1]['title'], ''.join([strongstrip(i) for i in text.findAll('p')[0]]), \
text.findAll('a')[1]['href'].split('/')[-1]])
req3= knatsuno.get(u'https://twitter.com/search?f=tweets&q=%s max_id%%3A%s&src=typd'%(query,str(int(results[-1][-1])-1)))
t=bs(req3.content, "lxml").findAll("div", { "class" : "content" })
if len(t)==1:
break
for i in results[-1][:len(results[-1])]:
print i
hds_cpp_bot ひだまりスケッチ コピペbot 12:57 - 2015年11月28日 宮子:この前、「ここがサナララの世界か…」って首からトイカメラをぶら下げたお兄さんがいたから、多分違うよ~?っていったら「そうか、だいたいわかった」って言ってバイクでどっか行っちゃった 670708141898645504
表示してるのは得られたツイートの中で一番古いツイート。max_idを使ったクエリが投げれた・・・のはいいんだけど何で一番古いツイートが一週間以内なの・・・。個数も、
len(results)
66
たった66個、ブラウザで検索してるときのネットワークのやりとり見てるとなんとなくhttps://twitter.com/i/toast_poll?oldest_unread_id=0 にもgetしてるっぽくて怪しいんだけど今日は眠いからここまで。悔しい。