ｳｯｶﾘツイッターツリー小説を書いてしまったので #TwitterAPI と #Python で解決する

## あっやばい。うっかりツイッターに小説書いちゃった…
2次創作のオタクをやっていると、うっかり妄想の興が乗ってツイッターツリー小説を書いてしまうことがあります。
「ないやろ」って思ってもあるんです。

（↓こんな感じ）
<img width="240" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/813aba2a-57b7-b8cd-19ae-f62716eca495.png">

折角なのでこれを修正して某創作作品向けSNSに載せたり同人誌に載せようとするのですが、このツリーの内容を全部コピペしようとすると**すごく大変です。**
興が載ってると400ツイートとかあったりして。しかもツイッターは仕様上ツリーのすべてを一度に表示できないからCtrl+Aとかもできなかったりして。

**TwitterAPIとPythonで自動で収集して救われたい！！！！**
救います。

## 環境
Pyhton v3.7.6
TwitterAPI v2（Essential）
--- 権限：Read and write
--- プロトコル：OAuth2.0

## ざっくり仕様
* ツイートの最後尾を起点に、リプライ先を遡っていってツイートツリーのテキストをすべて取得する。
    ※1 最初のツイートを起点にすると途中で別の人がリプライで送ってくれた感想等が混ざってしまうため。
    ※2 TwitterAPIで返ってくるConvercationIDも同理由で不可。
* 取得したツイートはすべてつなげて、テキストファイルとして出力する。

これだけ決めてPythonで実装していきます。

## 実装① ツイートのIDを基に、ツイートのテキストとリプライ先のIDを取得する
これを行うにはまずはTwitterAPIの登録と設定が必要なのですが、検索するとたくさん出てくるのでその説明は割愛します。

ツイートの取得はTweepyを使用します。
最初Tweepyの使用で躓いたので、そこについては下記の記事で軽く触れています。

https://qiita.com/mhwtss/items/12dadef1e4dc804bfedd

以下のコードで、とりあえず一連のツリーのテキストが、変数'fulltxt'に入力されます。
仕組みとしてはツリー末尾のツイートを起点に、そのテキストとリプライ先のIDを取得して、
最初のツイートまでひとつずつ遡ってテキストを取得する原始的な感じ。

また、TwitterAPIは15分間ごとにリクエストの上限が決まっているので、wait_on_rate_limitの設定をTrueにしておきます。

~~~Python:GETtweet
import tweepy

BEARER_API_TOKEN = '' # 作成したTwitterAPIのBearerTokenを記入
last_tweet_id = '' # ツイートのIDを記入
fulltxt = '' # ここにテキストをつなげていく

# tweepyでTwitterAPIとの接続設定
# return_typeがdefultだと分かりにくかったのでdict型で受取る
client = tweepy.Client(BEARER_API_TOKEN, return_type = dict ,wait_on_rate_limit = True)

tweet_id = last_tweet_id

while tweet_id != 'End':

    # ツイートIDを基にツイートの内容を取得。リプライ先のIDを取得するためexpansionsのオプションを設定。
    Res = client.get_tweet(tweet_id, expansions = ['referenced_tweets.id'])
    txt = Res['data']['text']
    fulltxt = txt +'\n'+ fulltxt

    if 'includes' in Res: # Responceに'includes'の項目の有無で一番最初のツイートかどうか判定
        # tweet_idにリプライ先のツイートIDを入力
        tweet_id = Res['includes']['tweets'][0]['id']
        continue
    else:
        # stop_idに最後のツイートIDを入力し、tweet_idは'End'にしてwhile文から抜ける。
        stop_id = tweet_id
        tweet_id = 'End'

Print(fulltxt)
~~~

## 実装② 得られたテキストを.txtで出力
ここはあまり説明が要らないかもしれません。
標準ライブラリのosで一応出力先ディレクトリの有無を確認してから、ファイルを出力しています。

ファイル名は TweetText_[先頭ツイートID]-[末尾ツイートID].txtになっています。
また、Twitterは結構絵文字とかが含まれていることが多いので文字のエラーを防ぐためにerrors='replace'にしました。

~~~Python:output_tweet_textfile
import os
stop_id = '' # 前述のコードで取得
last_tweet_id = '' # 前述のコードで取得
fulltxt = '' # 前述のコードで取得

output_dir = 'C:/Users/****/Documents/outputTweetText'

if os.path.isdir(output_dir) == False:
    os.mkdir(output_dir)

with open(output_dir + '/TweetText_' + str(stop_id) + '-'+str(last_tweet_id) + '.txt', 'w', encoding= 'utf-8', errors='replace') as f:
    f.write(fulltxt)
~~~

## 合体して完成！
上のコードを関数に仕立てて合体させたのが下記コードです。

~~~Python:
import tweepy
import os

BEARER_API_TOKEN = ''
last_tweet_id = ''
output_dir = 'C:/Users/****/Documents/outputTweetText'

def GETtweet(tweet_id):

    fulltxt = ''
    while tweet_id != 'End':

        Res = client.get_tweet(tweet_id, expansions = ['referenced_tweets.id'])
        txt = Res['data']['text']
        fulltxt = txt +'\n'+ fulltxt

        if 'includes' in Res:
            tweet_id = Res['includes']['tweets'][0]['id']
            continue
        else:
            stop_id = tweet_id
            tweet_id = 'End'

    return stop_id, fulltxt

def output_tweet_textfile(dir, data, id1, id2):
    if os.path.isdir(dir) == False:
        os.mkdir(dir)
    with open(dir + '/TweetText_' + str(id1) + '-'+str(id2) + '.txt', 'w', encoding= 'utf-8', errors='replace') as f:
        f.write(data)

if __name__ == "__main__":

    client = tweepy.Client(BEARER_API_TOKEN, return_type = dict ,wait_on_rate_limit = True)
    
    first_tweet_id, fulltext = GETtweet(last_tweet_id)
    output_tweet_textfile(output_dir, fulltext, first_tweet_id, last_tweet_id)
~~~

これを実行すれば、output_dirで指定したディレクトリにテキストファイルが出力されます。

## まとめ
あまりにニッチな要望過ぎて同じ悩みを抱えている人は少なそうですが、いざというときには役立つかも。
ツイッターにうっかり小説を書いてしまった人は、ぜひPythonとTwitterAPIを試してみてください。

ちなみに試しに実験台になってもらったフォロワーのツイートは、出力後に確認したところ305ツイート38453文字ありました。
う～ん、手作業で回収は無理だって。