Python未経験者がPythonでTwitterAPIを叩いた話

この記事は [アイマストドン内非公式「ジョンベベベント・カレンダー」 Advent Calendar 2017](https://adventar.org/calendars/2762)の12日目の記事です by [ke_odakyu9000](https://imastodon.net/@ke_odakyu9000)
今日で折り返し！半分！！！アイマス成分入れたかったけど消えちゃった！！！ごめん！！！

前日の方：[Nep](https://imastodon.net/@Nep)さん:[デレステ創作譜面について](http://nepluminous.hatenablog.com/entry/2017/12/11/000000)
翌日の方：[WakuwakuP](https://imastodon.net/@WakuwakuP)さん:[jsはいいぞ(P名刺について)](https://www.wakuwakup.net/blog/2017/12/13/producer-business-card/#blogs) 

---
#まえおき
当方、**プログラミングはC++を１年間ぐらいしか触ったことない**
かつ
**Pythonに関してはこれがはじめて**
という人なのでぐっちゃぐっちゃで見にくいかと思いますがご承知おきください・・・
（ここへの投稿も初めてだったり）

備忘録も兼ねているのでかなり長いです。飽きてしまったらぶっ飛ばして適当に読んでください
それと間違いがあればどんどん編集リクエストをください。お願いします。

#動機
C++でつくってるのはいいけどGUIって難しいなぁ
→気分転換になにか別の言語を学びたい！→最近有名な言語ってなんだろう
→**Python**これだ！！！→なにができるの？→なんでもできるっぽい
→じゃぁやりたかったけどやれてなかったTwitterAPIで遊んでみよう！

ということで何にも考えずにPythonに触れました
ちなみにですが僕はもうTwitterもMastodonもStreamじゃないと落ち着かなくなってるのでとりあえずStream取得したい思いが最初から強かった気がします


#作ったもの
- TwitterにPOSTする(普通のツイート)
- TwitterのUserStreamを取得する(勝手に流れるTLのアレ）
- 任意のキーワードに一致するツイートをリアルタイム検索する

#事前準備
###1.Pythonのプログラミング環境を構築する
詳しい方法は公式が[日本語のドキュメント](http://www.python.jp/install/windows/index.html)を出しているので省略
これがものすごいわかりやすいのでさほど迷わずに構築できました

###2.ライブラリのインストール
今回は[**Requests-OAuthlib**](https://github.com/requests/requests-oauthlib)をメインに扱っていきます
同GitHubに記載してるとおり
`pip install requests requests_oauthlib`
をやればインストールは終わり
このライブラリは初心者にはさっぱりなOAuthの基本部分を全部カバーしてくれるのです
便利ですねー

###3.Twitterの各種APIキーを取得
取得はざっくり[このへん](http://website-planner.com/twitter%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88consumer-key%E3%80%81consumer-secret%E3%80%81access-token%E3%80%81access-token-secret/)を見てほしいなーと思います

私が気がつかない間に携帯電話の番号の登録が必須になっているっぽいので一時的に登録して取得するのもありかなと思います(Twitterさんや、Developerにも面倒にさせないでくれ・・・)

取得したAPIは一応権限をすべてにしておくことをお勧めしておきます

今回はそこで取得したAPIキーのうち
・ Consumer Key
・ Consumer Secret
・ Access Token
・ Access Token Secret
この４つ全てを使います

----
#書いてみる
##1.POST(投稿)を書く

これが自分で作った初めてのpythonプログラムです

```python:API.py
CK = '取得したConsumer Key'
CS = '取得したConsumer Secret'
AT = '取得したAccess Token'
AS = '取得したAccess Token Secret'
```
```py:POST.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#初期化
from requests_oauthlib import OAuth1Session #一番初めのやつ
import random  #ランダムな数字を生成するのに使うmodule
import API     #ひとつ上のやつ

#ランダム数字を生成
x = random.randint(1,100)
print (x)

#Twitterへの投稿
url = "https://api.twitter.com/1.1/statuses/update.json" #APIの投稿専用URL

params = {"status":"test"+str(x)}  #投稿内容(test"ランダムな数字")

twitter = OAuth1Session(API.CK, API.CS, API.AT, API.AS) #OAuthへの接続
req = twitter.post(url, params = params) #送る情報

if req.status_code == 200: #もしresponseが200(成功)なら
    print ("OK")
else: #違ったら
	print ("Error: %d" % req.status_code) #エラーコード
```
###解説(自分が詰まったところとか大事な処理だけ）

```py

#初期化
from requests_oauthlib import OAuth1Session #一番初めのやつ
import random  #ランダムな数字を生成するのに使うmodule
import API     #ひとつ上のやつ
```
各種ライブラリ、もしくは参照ファイルをここで定義します

この場合だと上２つがライブラリ、最後がさっきつくったKEYのまとめたファイルです

基本的にこれらを参照するときは
`ライブラリの名前.使うもの`
(Ex:`API.CS`/ `random.randint(1,100)`)
みたいに使います
特に自分で生成したファイルの読み取り方法を見つけるのにちょっと苦労したりしなかったり（基本）

```py

#ランダム数字を生成
x = random.randint(1,100)
print (x)
```
コメントの通りランダムな数字を生成できます

全てのツイートでtestとすると同一ツイートではじかれないようにするために使ってます、運が悪いと同じのが生成されて失敗しますｗ

他にも`randint()`の部分を変更すればいろいろできます
詳しく知りたい方は[ここ](http://www.python-izm.com/contents/application/random.shtml)が参考になるかとおもいます

最後に何の数字を生成したのか結果をだすように`print(object)`でやっています

```py

#Twitterへの投稿
url = "https://api.twitter.com/1.1/statuses/update.json" #APIの投稿専用URL

params = {"status":"test"+str(x)}  #投稿内容(test"ランダムな数字")

twitter = OAuth1Session(API.CK, API.CS, API.AT, API.AS) #OAuthへの接続
req = twitter.post(url, params = params) #送る情報

if req.status_code == 200: #もしresponseが200(成功)なら
    print ("OK")
else: #違ったら
	print ("Error: %d" % req.status_code) #エラーコード
```
ここが肝心要のtwitterへの投稿部

urlは自分のやりたいAPIを[ここ](https://developer.twitter.com/en/docs/api-reference-index)から探し当ててプログラムも変えればやりたいことは基本なんでもできます
あとはREST APIをどうにかこうにかして何か情報を送信するなら`post`, 受信処理なら`get`を使いこなしてがんばる感じです

paramには送りたい情報の中身が入ります
POSTを使うときはparamに何を入れるのかで苦戦したりしなかったりします

`"status":"test"+str(x)`をエラーの出ないように作るのに苦労しました。textに代入するオブジェクトの型はすべてにおいて一致ないといけないと理解するのにすごく時間がかかりました。（今でも結構頭を使うとところだったりします）

---
##2.UserStreamをGETする
###そもそも：UserStreamとはなんだという人向け
Mastodonユーザーならおわかりいただけるとは思いますがmastodonでいう左から２番目の"ホーム"にあたる部分で、**フォローしてるユーザーの投稿が自動で流れてくる**ものです
<details><summary>Twitter公式の説明</summary>
The Streaming APIs give developers low latency access to Twitter’s global stream of Tweet data. A proper implementation of a streaming client will be pushed messages indicating Tweets and other events have occurred, without any of the overhead associated with polling a REST endpoint.

<details><summary>日本語訳</summary>ストリーミングAPIを使うことで、開発者はTwitterにおけるツイートデータのグローバルストリームに低レイテンシでアクセスすることができます。 適切に実装すれば、RESTエンドポイントへポーリングする時のような負荷をかけずに、ツイートやその他イベントが発生したことを示すメッセージをクライアントへ通知できます。 cf:[Twitter 開発者 ドキュメント日本語訳](http://westplain.sakuraweb.com/translate/twitter/Documentation/Streaming-APIs/Overview.cgi)

ということで今回は普通の非公式クライアントのような毎度更新する必要のないTLをみれるようにしてみます


##UserStram(GET)を書く
`API.py`は上述と同じなので省略

```py:STREAM.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#Initialize
import sys
reload(sys)
sys.setdefaultencoding('utf-8') # デフォルトの文字コードを変更する．
import requests
from requests_oauthlib import OAuth1Session
import datetime, time
import json
import API

#UserStream
twitter = OAuth1Session(API.CK, API.CS, API.AT, API.AS)
url = "https://userstream.twitter.com/1.1/user.json"

params = {}
RLT =  180 #Rate-Limit time (min)

while(True):
	try:
		req  = twitter.post(url, stream=True, params = params)

		#Return
		print(req.status_code)

		if req.status_code == 200:
			if req.encoding is None:
				req.encoding = "utf-8"
		
		
			for js in req.iter_lines(chunk_size=1,decode_unicode=True):
				try:
					if js :
				
						tweet = json.loads(js)
				
						if tweet.has_key("text"):
							#jsonのkeyをそれぞれ変数化
							Name = (tweet["user"]["name"])
							SC_N = (tweet["user"]["screen_name"])
							Text = (tweet["text"])
		
							#print
							print ('----\n'+Name+"(@"+SC_N+"):"+'\n'+Text)
							continue
				
						else:
							continue
					
				except UnicodeEncodeError:
					pass	
					
		elif req.status_code == 420:
				print('Rate Limit: Reload after', RLT, 'Sec.')
				time.sleep(RLT)
			
		else:
			print("HTTP ERRORE: %d" % req.status_code)
			break

	except KeyboardInterrupt:
		print("End")
		break
		
	except:
		print("except Error:", sys.exc_info())
		pass
```

###解説
後半`while(True):`以降の流れをざっくりフローにしてみました
推測も含んでいるので正確とはいえませんが雰囲気をわかってくれればうれしいです
![チャート.png](https://qiita-image-store.s3.amazonaws.com/0/220278/3cb04b1b-71b1-d409-3431-1cdcc5777440.png)
200を受け取った時点で完全に右側のループを永遠に(Ctrl+Cするまで)処理し続けます

この処理の一番大事な部分は
**requests.iter_lines()**
です。本当は自分で作るべきなのですがrequestsライブラリーでは自動で１行ずつ処理してくれるのでありがたく使わさせて貰います
た　だ　し
・ 受信するJSONはUnicodeでなければならない
・ データの処理漏れが発生する
という問題つきの処理方法ですがざっくりUserStreamが見れれば満足なのでこのまま[公式リファレンス](http://docs.python-requests.org/en/master/user/advanced/#streaming-requests)を参考に実装しました(本当はもっとちゃんと処理できるようにしたい）


####例外処理とRateLimitについて

```python:
except UnicodeEncodeError:
```
```python:
except KeyboardInterrupt:
```
```python:
except:
print("except Error:", sys.exc_info())
```
でUTF-8の処理エラー、もしくは処理中止を例外処理というよろしくない方法を取っていて改善したいところではあります

Ratelimitの処理

```py
RLT =  180 #Rate-Limit time (min)
```

```py
elif req.status_code == 420:
print('Rate Limit: Reload after', RLT, 'Sec.')
time.sleep(RLT)
```

Twitter-StreamAPIにおいてRateLimitの時間がどこにもなく何分待てばいいのかわからないので適当に設定できるようにしてもし420が返されたら指定時間待つ処理をここでしています。

```py
 print ('----\n'+Name+"(@"+SC_N+"):"+'\n'+Text)
 continue
```
最後に結果を出力するのですが、

```
----
ユーザー名(@Twitter_ID):
TEXT
```
こんな感じのをループするようにしました
で、これの後にcontinueしないと１行目だけ吐き出して終わってしまうことに気がつくのにものすごく時間をとられました。


あとはさまざまな方のソースコードを参考にJSONのkeyの処理の仕方だったり、Python特有のインデントエラーと戦いながらなんとか１週間以内に形をなすことができました

---
##3.ツイートのリアルタイム検索を作る
基本的な部分は先ほど書いた`STREAM.py`となんら変わらないので変えた部分だけ紹介します

- 初期化に追加

```py
import codecs
sys.stdout = codecs.getwriter("shift_jis")(sys.stdout) # 出力
sys.stdin = codecs.getreader("shift_jis")(sys.stdin) # 入力
```
後で任意のキーワードを入力できるようにする際にこれがないとエラーの嵐で死にます

- 任意の文字列を入力できるようにする

```py
#検索ワード登録
print unicode("検索したいワードを入力してください")
print unicode("２つ以上入れる場合は 「,」 で区切って入力してください")

input_word = raw_input(">>> ")
```
見た目はシンプルなのですがとても曲者なのであとでもう少しだけ詳しく触れます

- URL, 送る情報の変更

```py
url = "https://stream.twitter.com/1.1/statuses/filter.json"

params = {"track": input_word}
```
URLは検索用のURL、paramsには前で入力した検索ワードを入れてこのワードで検索をかけます

以上を変更してできたコードが以下のとおりです

```py:SEARCH-STREAM.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#Initialize
import sys
reload(sys)
sys.setdefaultencoding('utf-8') # デフォルトの文字コードを変更する．
import codecs
sys.stdout = codecs.getwriter("shift_jis")(sys.stdout) # 出力
sys.stdin = codecs.getreader("shift_jis")(sys.stdin) # 入力
import requests
from requests_oauthlib import OAuth1Session
import datetime, time
import json
import API

#検索ワード登録
print unicode("検索したいワードを入力してください")
print unicode("２つ以上入れる場合は 「,」 で区切って入力してください")

input_word = raw_input(">>> ")

#UserStream
twitter = OAuth1Session(API.CK, API.CS, API.AT, API.AS)
url = "https://stream.twitter.com/1.1/statuses/filter.json"

params = {"track": input_word}
RLT =  180 #Rate-Limit time (min)

while(True):
	try:
		req  = twitter.get(url, stream=True, params = params)

		#Return
		print(req.status_code)

		if req.status_code == 200:
			if req.encoding is None:
				req.encoding = "utf-8"
		
		
			for js in req.iter_lines(chunk_size=1,decode_unicode=True):
				try:
					if js :
				
						tweet = json.loads(js)
				
						if tweet.has_key("text"):
							#jsonのkeyをそれぞれ変数化
							Name = (tweet["user"]["name"])
							SC_N = (tweet["user"]["screen_name"])
							Text = (tweet["text"])
		
							#print
							print ('----\n'+Name+"(@"+SC_N+"):"+'\n'+Text)
							continue
				
						else:
							continue
					
				except UnicodeEncodeError:
					pass	
					
		elif req.status_code == 420:
				print('Rate Limit: Reload after', RLT, 'Sec.')
				time.sleep(RLT)
			
		else:
			print("HTTP ERRORE: %d" % req.status_code)
			break	
		
	except KeyboardInterrupt:
		print("End")
		break
		
	except:
		print("except Error:", sys.exc_info())
		pass
```

###解説というか**"文字コード"**について
ここまで３つ(実際にはこれ以外にもいくつか)書いてきましたがどれでも頭を悩ませるのが"文字コード"です
はっきりいってこれの処理をどうするのかに一番時間を使ってました
私自身よくわかってないので一番詳しい[こちら](http://lab.hde.co.jp/2008/08/pythonunicodeencodeerror.html)のサイトを見て貰うほうがよっぽどためになりますし私自身参考にさせていただきました。
特にこれ

```py
print unicode("検索したいワードを入力してください")
print unicode("２つ以上入れる場合は 「,」 で区切って入力してください")

input_word = raw_input(">>> ")
```
Windows環境のコマンドプロントではどうもデフォルトではShift_jisで文字を扱っているらしく文字コードを変換してやらないと日本語のすべてが文字化けします。(自分で環境を変えている人を除き)
でも出力は明記するだけでどうにかなるでいいんです。
問題は入力。普通にUnicodeで指定すると一文字ずつUnicodeのencodeでエラーだらけになるんです。
そこで回避する方法が

```py
import codecs
sys.stdout = codecs.getwriter("shift_jis")(sys.stdout) # 出力
sys.stdin = codecs.getreader("shift_jis")(sys.stdin) # 入力
```
これです。荒業ですがコマンドラインで入力に対して処理が必要ならこれで処理しないとエラーの嵐に襲われます(マジでエラーログの嵐です、ループ処理している故）
これについては[こちら](http://d.hatena.ne.jp/artgear/20120529/1338303523)のサイトにあったものを使わさせていただきました。ありがとうございます・・・

たぶんこれからもutf-8には頭を悩ませることになりそうです・・・

###正直なところ
なんか動作が不安定というかほしい動作をしてくれなくていて
おそらくの原因がstreamAPIの仕様のものじゃないかなぁと思ってたり
キーワードで検索した際にハッシュタグはきちんと拾うのですが文章中のワードは拾ってくれないっぽいんです・・・
これはまた考えてどうにかしたいかなぁと思います（いっそのこと諦めて普通のsreachにしてしまうか)

---

#初めてpythonに触ってみた感想とかまとめとか
**Pythonってものすごく手軽**
っていうのが一番思ったことです
型がなんだとか文字コードがなんだとかいろいろありましたがすごい楽しかったです
今回はrequestsしか使いませんでしたがさまざまなライブラリがあり、機械学習、サーバープログラミングなどなどなんでも簡単にできるんんだなぁと思いました。先人方に感謝ですね
また、pythonさんものすごく処理が早いことも強みだなと思いました
思った以上にPythonいろんなことができて面白いので今後も何か作っていきたいなぁとか思ってたりします
ここまで長々としてしまいましたがご覧いただきありがとうございました

---

#参考文献
- [PythonでTwitter Streaming APIを叩いた話](https://qiita.com/nasa9084/items/40f223b5b44f13ef2925) - nasa9084
- [Twitter Streaming APIで取得した内容をJSON形式で書き出す](https://qiita.com/snofra/items/3189bb0a9605ea8e4ce6) - snofra
- [Python で Twitter API にアクセス](https://qiita.com/yubais/items/dd143fe608ccad8e9f85) - yubais

---
---
<font size=3>[アイマストドン内非公式「ジョンベベベント・カレンダー」 Advent Calendar 2017](https://adventar.org/calendars/2762)：</font>
<font size=3>明日は[WakuwakuP](https://imastodon.net/@WakuwakuP)さん:[jsはいいぞ(P名刺について)](https://www.wakuwakup.net/blog/2017/12/13/producer-business-card/#blogs) </font>

---
<font size=5>以下おまけ、さほど面白くもないので見たい人だけどうぞ</font>

<font size=6>ベベントカレンダー　前日の[Nep](https://imastodon.net/@Nep)さん:[デレステ創作譜面について](http://nepluminous.hatenablog.com/entry/2017/12/11/000000)をみて:</font>
<font size=7>音ゲーは好きですが自分で譜面作るってやったことないなぁと、これ製作者側の思いとかも感じられたりするのでしょうかね・・・楽しそうではあります</font>

<font size=6>今後やりたいこと:
<font size=7>・PythonでGUIをつくりたい（これもライブラリがあるらしい)
・上に書いたStream検索をどうにか有用性のあるものにしたい
・Twitterの膨大なデータで機械学習やってみたい</font>

<font size=6>最後のStream検索使えないとか言いながらなんで書いたの:
<font size=7>えっとですね、元はアイマス関連のアドベントカレンダーなのでアイマス関連ワードを自由に拾って例えばライブ後の感想をあつめてみたりしたかったんですけど時間と技術的に無理でした</font>

2018/3/3追記
なんかTwitterさんUserStream廃止するとか言ってますね
ここに書いたの意味ないじゃないですか！！！！！（憤怒）