@Shin_beginner

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

TwitterAPIを用いて、Pythonでデータ取得

解決したいこと

Python初心者です。
大学の研究で、Xを研究対象にしています。
APIを申請したので、ある投稿にされたリツイートの情報を取り、CSVファイルに書き込むプログラムをhttps://www.topgate.co.jp/blog/techblog/4647
を参考にして、実行しようとしたのですが、エラーが出て、調べても解決できなかったので質問しました。
今回のコードは4つのキーで認証していますが、bearerトークンも所持しています。
APIの使用は今回が初めてです。
分かる方がいればよろしくお願いします。

発生している問題・エラー

ファイル名(例:retweet_info.csv):a.csv
--------送信するリクエスト数合計:1-------------
--------1回目。今回送信するリクエスト数:4--------------------
0:リクエストしたtweetID:XXXXXXXXXXXXXXXXXXX---------
Error: 401
Error数:1

該当するソースコード

from requests_oauthlib import OAuth1Session
import json
import csv
import collections as cl
import datetime,sys,time

file = input('ファイル名(例:retweet_info.csv):')
'''
OAuth認証
'''
def request_authorization (**url_and_params):
    twitter = OAuth1Session(url_and_params['ck'], url_and_params['cs'], url_and_params['at'], url_and_params['ats'])
    if('params' in url_and_params):
        request_result = twitter.get(url_and_params['request_url'],params=url_and_params['params'])
    else:
        request_result = twitter.get(url_and_params['request_url'])
    return request_result
'''
API制限をチェックし、制限解除までスリープ
'''
def sleep_until_api_limit(api_response):
    sec = int(api_response.headers['X-Rate-Limit-Reset']) - time.mktime(datetime.datetime.now().timetuple()) #UTCを秒数に変換
    sec += 60.0#念のため1分追加
    print(str(sec)+"秒待機")
    time.sleep(sec)
#取得した4つのkeyをここに差し込みます。
CK = "XXXXXXXXXXXXX" #Consumer key
CS = "XXXXXXXXXXXXX" #Consumer secret
AT = "XXXXXXXXXXXXX" #Access token
AS = "XXXXXXXXXXXXX" #Access token secret
global twitter#メソッド内で再認証を行うため
twitter = OAuth1Session(CK, CS, AT, AS)#認証処理。数時間すると認証が切れる。
# ツイート情報取得用エンドポイントURL
request_status_url = "https://api.twitter.com/2/tweets/:id/retweeted_by"
'''
情報を取得したいTweetIDをリストに格納
'''
tweet_id_list = ["TweetID"] #リツイート情報を取得したい TweetID をここに差し込みます。
tweet_id_list.sort()
total_requests_number = len(tweet_id_list)
print("--------"+"送信するリクエスト数合計:" + str(total_requests_number)+"-------------")
'''
TwitterからUserID情報の取得開始
'''
sched_requests_number = 0 #リクエストしたtweet_idの数
should_get_requests_limit = True #API上限数を取得するためのスイッチ
i_plus = 1 #API上限数を取得するためのスイッチ
requests_count = 0 #「API上限までリクエストを送る」を実行するのは何回目かカウントする用
error_count = 0 #エラーを返された回数をカウントする用
output_csv_file_name = file #取得した値を格納ファイル名
'''
ループ開始前にカラムへ書き込み
'''
with open(output_csv_file_name,'a') as f:
    writer = csv.writer(f, lineterminator='\n')
    header = ['tweet_id', 'retweet_id', 'user_id', 'created_at']
    writer.writerow(header)
'''
ループ開始。retweet_idの書き込み。
'''
for i in range(0,total_requests_number,i_plus):
    if(should_get_requests_limit == True):
        api_response = request_authorization(request_url=request_status_url, ck=CK, cs=CS, at=AT, ats=AS)
        requests_limit = api_response.headers['X-Rate-Limit-Remaining']#リクエスト可能残数の取得
        requests_limit = int(requests_limit)#limitが文字列なのでキャスト
        requests_count += 1
        print("--------"+str(requests_count)+"回目。今回送信するリクエスト数:" + str(requests_limit)+"--------------------")
        sched_requests_number += requests_limit
        should_get_requests_limit = False
    if(requests_limit != 0):
        i_plus = 1
        print(str(i) + ":リクエストしたtweetID:" + str(tweet_id_list[i])+"---------")
        request_url = "https://api.twitter.com/2/tweets/"+str(tweet_id_list[i])+"?/retweeted_by/count=10&trim_user=false"
        params = {'tweet_id':tweet_id_list[i]}
        request_result = request_authorization(request_url=request_url, ck=CK, cs=CS, at=AT, ats=AS, params=params)
        if request_result.status_code == 200:
            tweetinfo = json.loads(request_result.text)#JSON形式で読み込む
            for j in range(0,len(tweetinfo)):
                    print(str(i)+":"+str(j)+":"+"リツイートID" + str(tweetinfo[j]['id']))
                    followerdata = {'tweet_id':tweet_id_list[i],
                                    'retweet_id':tweetinfo[j]['id'],
                                    'user_id':tweetinfo[j]['user']['id'],
                                    'created_at':tweetinfo[j]['created_at']}
                    with open(output_csv_file_name, 'a') as f:
                        writer = csv.writer(f, lineterminator='\n')
                        writer.writerow(followerdata.values())
        else:
            print ("Error: %d" % request_result.status_code)
            error_count += 1
            print("Error数:"+str(error_count))
    else:
        i_plus = 0 #limitが0のままiが1増えてしまうと実行されないTweeetIDが発生するので、0入れて増やさない。
    if(i == sched_requests_number):
        print("--------"+"残りリクエスト数" + str(total_requests_number-requests_limit) + "-------------")
        sleep_until_api_limit(api_response)
        should_get_requests_limit = True

自分で試したこと

調べながら、エンドポイントURLを変えたりしてみましたが、エラーが無くなりません。

0 likes

1Answer

401ということなので認証エラーだと思うのですが、アクセストークンなどの認証情報が間違っていたり有効期限が切れていたりしないでしょうか?

2Like

Comments

  1. @Shin_beginner

    Questioner

    回答ありがとうございます。
    確認しましたが、アクセストークンは合っていました。
    APIはBasicプランで11/30に利用可能になった為、期限はまだ大丈夫だと思います。

  2. 下記1度目の呼び出し部分は200で返ってきている状態ですかね。
    そうであれば認証情報は問題なさそうですね。

            api_response = request_authorization(request_url=request_status_url, ck=CK, cs=CS, at=AT, ats=AS)
    

    あと思いつく点としてはこちらの URL 構築部分でしょうか。

            request_url = "https://api.twitter.com/2/tweets/"+str(tweet_id_list[i])+"?/retweeted_by/count=10&trim_user=false"
    

    URLを変えたりしてみたということですが、この通りの場合であれば https://~/tweets/xxxxxx?/retweeted_by のように ID の後ろに余分な "?" が付与されるかと思います。
    また、クエリパラメータは "?" の後ろに記述するので retweeted_by/count~ ではなく retweeted_by?count~ になると思います。

            # こうではないでしょうか?
            request_url = "https://api.twitter.com/2/tweets/"+str(tweet_id_list[i])+"/retweeted_by?count=10&trim_user=false"
            # 文字列の前に f を書いて変数名を { } で囲めば + で繋ぐ必要もなくなります
            request_url = f"https://api.twitter.com/2/tweets/{tweet_id_list[i]}/retweeted_by?count=10&trim_user=false"
    

    あとこちらのAPIドキュメントを確認したところクエリパラメータに counttrim_user も見当たらなかったのですがそちらについては大丈夫でしょうか?

  3. @Shin_beginner

    Questioner

    回答ありがとうございます。
    ご指摘してもらった通り、エンドポイントURLを

    request_url = f"https://api.twitter.com/2/tweets/{tweet_id_list[i]}/retweeted_by?"
    

    にして実行したところ、

    ファイル名(例:retweet_info.csv):a.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:4--------------------
    0:リクエストしたtweetID:641830981045911552---------
    Error: 400
    Error数:1
    

    というエラーになりました。

  4. 400エラーに変わったということで、こちらのページに記載のいずれかの問題だと思います。
    https://developer.twitter.com/en/docs/twitter-ads-api/response-codes
    ↑は Twitter API ではなく Twitter Ads API のリンクでした。
    Twitter API のエラーコードについてのページは↓でした。
    https://developer.twitter.com/en/support/x-api/error-troubleshooting

    原因としてはパラメータ指定を間違えていることなどが考えられますが、それ以上はちょっと分からないですね......。
    ドキュメントを見た感じレスポンスボディを見ればエラーの原因は書いてありそうなので、レスポンスボディの確認をお勧めいたします。

  5. @Shin_beginner

    Questioner

    回答ありがとうございます。
    自分でも調べてみて、OAuth認証のバージョンや、やり方が違うのかなと思って、その部分の修正を試みているのですが、OAuth2.0の認証を行う際のコードの書き方が調べてもよく分からず苦戦しています。

  6. @Shin_beginner

    Questioner

    もしくは、Basicプランだと1週間前までの制限があるから、古いポストのリクエストが通らないようになっているとかですかね?

  7. もしくは、Basicプランだと1週間前までの制限があるから、古いポストのリクエストが通らないようになっているとかですかね?

    その可能性は考えられなくもないですが、まずは API からどのようなレスポンスが返ってきているのかを確認しないことには何とも言えませんね。

    エンドポイントを下記に変更したということであれば、末尾に不要な ? が付いている事が悪さをしているのかもしれません。

    request_url = f"https://api.twitter.com/2/tweets/{tweet_id_list[i]}/retweeted_by?"
    

    末尾の ? を削除(request_url = f"https://api.twitter.com/2/tweets/{tweet_id_list[i]}/retweeted_by")して送信するとどうなりますか?
    そのレスポンスボディはどのような内容になっていますか?
    レスポンス内容自体は request_result.text で取得できるかと思いますので、適当に print(request_result.text) などと追記してあげればどのようなレスポンスが返ってきているのかは確認できると思います。

  8. @Shin_beginner

    Questioner

    実行したところ、

    ファイル名(例:retweet_info.csv):a.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:2--------------------
    0:リクエストしたtweetID:1735126698814042465---------
    Error: 400
    Error数:1
    {"errors":[{"parameters":{"tweet_id":["1735126698814042465"]},"message":"The query parameter [tweet_id] is not one of [id,max_results,pagination_token,expansions,tweet.fields,user.fields]"}],"title":"Invalid Request","detail":"One or more parameters to your request was invalid.","type":"https://api.twitter.com/2/problems/invalid-request"}
    

    と表示されました。

  9. レスポンスの message に

    The query parameter [tweet_id] is not one of [id,max_results,pagination_token,expansions,tweet.fields,user.fields]

    とありますね。
    google翻訳すると

    クエリ パラメータ [tweet_id] は [id,max_results,pagination_token,expansions,tweet.fields,user.fields] のいずれでもありません

    となります。
    質問者様のコードでは下記部分でリクエストパラメータとして tweet_id を指定しています。

    params = {'tweet_id':tweet_id_list[i]}
    request_result = request_authorization(request_url=request_url, ck=CK, cs=CS, at=AT, ats=AS, params=params)
    

    しかし、ドキュメントにクエリパラメータとして tweet_id の記述は無いですし、エラーメッセージからもクエリパラメータとして不正であると考えられます。
    クエリパラメータを指定せずに (params=params 部分を削除した状態で) 実行すれば解決するか、少なくともそのエラーは解消されるかと思います。

  10. @Shin_beginner

    Questioner

    実行したところ

    ファイル名(例:retweet_info.csv):a.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:0--------------------
    --------残りリクエスト数1-------------
    572.0秒待機
    

    と出て待機中ですが、この挙動で正しいですか?

  11. def sleep_until_api_limit(api_response):
        sec = int(api_response.headers['X-Rate-Limit-Reset']) - time.mktime(datetime.datetime.now().timetuple()) #UTCを秒数に変換
        sec += 60.0#念のため1分追加
        print(str(sec)+"秒待機")
        time.sleep(sec)
    

    待機するようにコードを記述されているので、572 秒 (約 9 分半) 待機するのなら (何をもって正しいとするかはともかく) 記述した通りに動いているかと思います。

  12. @Shin_beginner

    Questioner

    待機が終わって実行されたら、

    ファイル名(例:retweet_info.csv):a.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:4--------------------
    0:リクエストしたtweetID:641830981045911552---------
    Traceback (most recent call last):
      File "C:\python\get_retweet.py", line 79, in <module>
        print(str(i)+":"+str(j)+":"+"リツイートID" + str(tweetinfo[j]['id']))
    KeyError: 0
    

    今度はこのエラーが表示されましたが、どう間違っているのかがよくわかりません。

  13. KeyErrorと書いているので tweetinfo[j]['id'] 部分で key の指定方法を間違えているのだと思います。

    ドキュメントに書いてあるレスポンス例を確認したり、いったん下記のように for 文の中身をコメントアウトして tweetinfo がどのような内容になっているのかを print するなどしてどのようなデータになっているのかを確認すれば良いかと思います。

                print(tweetinfo)
                '''
                for j in range(0,len(tweetinfo)):
                        print(str(i)+":"+str(j)+":"+"リツイートID" + str(tweetinfo[j]['id']))
                        followerdata = {'tweet_id':tweet_id_list[i],
                                        'retweet_id':tweetinfo[j]['id'],
                                        'user_id':tweetinfo[j]['user']['id'],
                                        'created_at':tweetinfo[j]['created_at']}
                        with open(output_csv_file_name, 'a') as f:
                            writer = csv.writer(f, lineterminator='\n')
                            writer.writerow(followerdata.values())
                '''
    

    tweetinfo = json.loads(request_result.text)#JSON形式で読み込む という部分で辞書型として読み込まれると思いますので、上記のように print すれば {'data': [{'id': '1111',... のような出力がされるのではないでしょうか?
    あとは辞書型から任意のデータを取得するにはどのようにすれば良いかを調べれば取得したいデータが取れると思います。
    例として tweetinfo の中身が上記のような状態であれば、id の 1111 を取得するには tweetinfo['data'][0]['id'] となります。
    これについては Python の配列と辞書型の基礎的な知識になりますので検索するなりして学習し、ご自身で対応できるようになることを推奨します。

  14. @Shin_beginner

    Questioner

    リクエスト結果をテキスト化してみたところ

    ファイル名(例:retweet_info.csv):a.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:4--------------------
    0:リクエストしたtweetID:1734415313108967869---------
    {"data":[{"id":"1362390204250681344","name":"二代目野良猫\uD83D\uDC79","username":"oniwaka_aikoku2"},{"id":"1947854082","name":"ドリンク","username":"jackunjackun"},{"id":"1719515411769913344","name":"ぱちんこ業界の地元密着営業を応援✨『宣伝隊長』","username":"QpEdovuREn1499"},{"id":"215274457","name":"伯爵令嬢ツイート出来るようになったーーー\uD83E\uDD79","username":"hakusyakureijou"},{"id":"145882204","name":"ちゃり","username":"amb98"}],"meta":{"result_count":5,"next_token":"7140dibdnow9c7btw483gv1lvrdbmxo2hbi74c9eratkk"}}
    Traceback (most recent call last):
      File "C:\python\get_retweet.py", line 80, in <module>
        print(str(i)+":"+str(j)+":"+"リツイートID" + str(tweetinfo[j]['id']))
    KeyError: 0
    

    このように返ってきてることが分かりました。
    今回のエラーは、返ってきたデータとCSVに入力する際の形が違うということですか?

  15. まずそもそも CSV に入力するところまで行っておりません。
    エラーの原因としては tweetinfo の内容を取得する際に tweetinfo[j] の部分で対応する key が無いためです。
    具体的にはこの時点では j は 0 のはずですが、tweetinfo[0] に対応するデータが無いためエラーになっています。

    これについては非常に初歩的な内容であり、python の辞書型、配列について検索すればいくらでも情報は出てくるので後はご自身で学習すれば対応可能な内容だと思います。
    厳しいことを言われているように思うかも知れませんが、tweetinfo の内容が分かっている状態で「なぜtweetinfo[0] がエラーになるか?」が分からなければこの先続けることは困難かと思います。

    「python 入門」で検索すれば最初にヒットするこちらのサイトなどで python の初歩的な内容から学習するべきかと思います。
    非常に簡単な内容ですがこのサイトの内容を一通りやるだけでもなぜエラーになるのかは理解できるようになるかと思います。
    応援しています。

  16. @Shin_beginner

    Questioner

    回答ありがとうございます。
    ご指摘のおかげで無事に動かすことが出来ました。

    コマンドプロンプト上 IDは念の為隠します。

    ファイル名(例:retweet_info.csv):b.csv
    --------送信するリクエスト数合計:1-------------
    --------1回目。今回送信するリクエスト数:1--------------------
    0:リクエストしたtweetID:XXXXXXXXXX---------
    {"data":[{"id":"XXXXXXXXXX","created_at":"2015-09-10T04:30:35.000Z","text":"RT @nekoyatsuki: 常総市三坂付近で鬼 怒川決壊。東側の方は西側へ避難を!っとのことですが、水海道有料道路は西側が冠水で通行止めです。気をつけてください","author_id":"XXXXXXXXXX","edit_history_tweet_ids":["XXXXXXXXXX"]}],"meta":{"result_count":1,"next_token":"XXXXXXXXXX"}}
    0:リツイートID:XXXXXXXXXX
    

    ファイル出力結果

    tweet_id,retweet_id,user_id,created_at
    XXXXXXXXXX,XXXXXXXXXX,XXXXXXXXXX,2015-09-10T04:30:35.000Z
    

    同じようなことをしたい人がいた時用

    from requests_oauthlib import OAuth1Session
    import json
    import csv
    import collections as cl
    import datetime,sys,time
    
    file = input('ファイル名(例:retweet_info.csv):')
    '''
    OAuth認証
    '''
    def request_authorization (**url_and_params):
        twitter = OAuth1Session(url_and_params['ck'], url_and_params['cs'], url_and_params['at'], url_and_params['ats'])
        if('params' in url_and_params):
            request_result = twitter.get(url_and_params['request_url'],params=url_and_params['params'])
        else:
            request_result = twitter.get(url_and_params['request_url'])
        return request_result
    '''
    API制限をチェックし、制限解除までスリープ
    '''
    def sleep_until_api_limit(api_response):
        sec = int(api_response.headers['X-Rate-Limit-Reset']) - time.mktime(datetime.datetime.now().timetuple()) #UTCを秒数に変換
        sec += 60.0#念のため1分追加
        print(str(sec)+"秒待機")
        time.sleep(sec)
    #取得した4つのkeyをここに差し込みます。
    CK = "XXXXXXXXXX" #Consumer key
    CS = "XXXXXXXXXX" #Consumer secret
    AT = "XXXXXXXXXX" #Access token
    AS = "XXXXXXXXXX" #Access token secret
    global twitter#メソッド内で再認証を行うため
    twitter = OAuth1Session(CK, CS, AT, AS)#認証処理。数時間すると認証が切れる。
    # ツイート情報取得用エンドポイントURL
    request_status_url = "https://api.twitter.com/2/tweets/:id/retweets"
    '''
    情報を取得したいTweetIDをリストに格納
    '''
    tweet_id_list = ["XXXXXXXXXXX"] #リツイート情報を取得したい TweetID をここに差し込みます。
    tweet_id_list.sort()
    total_requests_number = len(tweet_id_list)
    print("--------"+"送信するリクエスト数合計:" + str(total_requests_number)+"-------------")
    '''
    TwitterからUserID情報の取得開始
    '''
    sched_requests_number = 0 #リクエストしたtweet_idの数
    should_get_requests_limit = True #API上限数を取得するためのスイッチ
    i_plus = 1 #API上限数を取得するためのスイッチ
    requests_count = 0 #「API上限までリクエストを送る」を実行するのは何回目かカウントする用
    error_count = 0 #エラーを返された回数をカウントする用
    output_csv_file_name = file #取得した値を格納ファイル名
    '''
    ループ開始前にカラムへ書き込み
    '''
    with open(output_csv_file_name,'a') as f:
        writer = csv.writer(f, lineterminator='\n')
        header = ['tweet_id', 'retweet_id', 'user_id', 'created_at']
        writer.writerow(header)
    '''
    ループ開始。retweet_idの書き込み。
    '''
    for i in range(0,total_requests_number,i_plus):
        if(should_get_requests_limit == True):
            api_response = request_authorization(request_url=request_status_url, ck=CK, cs=CS, at=AT, ats=AS)
            requests_limit = api_response.headers['X-Rate-Limit-Remaining']#リクエスト可能残数の取得
            requests_limit = int(requests_limit)#limitが文字列なのでキャスト
            requests_count += 1
            print("--------"+str(requests_count)+"回目。今回送信するリクエスト数:" + str(requests_limit)+"--------------------")
            sched_requests_number += requests_limit
            should_get_requests_limit = False
        if(requests_limit != 0):
            i_plus = 1
            print(str(i) + ":リクエストしたtweetID:" + str(tweet_id_list[i])+"---------")
            request_url = f"https://api.twitter.com/2/tweets/{tweet_id_list[i]}/retweets?tweet.fields=id,created_at,author_id"
            params = {'tweet_id':tweet_id_list[i]}
            request_result = request_authorization(request_url=request_url, ck=CK, cs=CS, at=AT, ats=AS)
            print(request_result.text)
            if request_result.status_code == 200:
                tweetinfo = json.loads(request_result.text)#JSON形式で読み込む
                for info in tweetinfo['data']:
                        print(str(i)+":"+"リツイートID:" + str(info['id']))
                        followerdata = {'tweet_id':tweet_id_list[i],
                                        'retweet_id':info['id'],
                                        'user_id':info['author_id'],
                                        'created_at':info['created_at']}
                        with open(output_csv_file_name, 'a') as f:
                            writer = csv.writer(f, lineterminator='\n')
                            writer.writerow(followerdata.values())
            else:
                print ("Error: %d" % request_result.status_code)
                error_count += 1
                print("Error数:"+str(error_count))
        else:
            i_plus = 0 #limitが0のままiが1増えてしまうと実行されないTweeetIDが発生するので、0入れて増やさない。
        if(i == sched_requests_number):
            print("--------"+"残りリクエスト数" + str(total_requests_number-requests_limit) + "-------------")
            sleep_until_api_limit(api_response)
            should_get_requests_limit = True
    

    お忙しい中、質問に真摯に向き合ってくださり、とても感謝しています。

Your answer might help someone💌