Flaskの基本

1.Twitter APIからユーザーのタイムラインを取得するプログラムを作る

APIの仕様を確認してプログラムの仕様を決める

プログラムを作成する前にまずはTwitter APIの仕様を確認します。

公式は英語しかないようなので、必要に応じて日本語の解説サイト等も検索して探してみてください。プログラムを作る上で関係する主な仕様は以下のとおりです。

  • 認証あり(OAuth方式、OAuth1.0とOAuth2.0どちらもあるが今回はOAuth1.0)
  • 認証キーの取得のためTwitter Developerに登録が必要

また、今回の目的であるユーザーのタイムライン情報を取得するには

GET statuses/user_timeline

で取得でき、URLとしては

https://api.twitter.com/1.1/statuses/user_timeline.json

にアクセスしてパラメータを渡せば指定したユーザーのタイムラインが取得できることが分かります。

APIの仕様を確認しながらプログラムの仕様も決めることになります。今回は、スクリーンネーム(@からはじまる英数字のユーザーID)を入力すると、そのユーザーのツイートとそのお気に入り数・リツイート数・ツイート日時の4つの項目をCSVとして出力するプログラムを作成することを目指します。

なお、多くのAPIはRequestsライブラリだけで取得が可能ですが、OAuth形式の場合はまた別のライブラリを使用する必要があります。

今回は、Requests-OAuthlibというRequestsにOAuth認証の機能を付けたライブラリを利用します。Requests-OAuthlibは、OAuthの認証機能がRequestsライブラリに追加されただけで、Requestsライブラリとほとんど同様の使い方ができます。

外部ライブラリなので、

pip install requests-oauthlib

などで忘れずにインストールしておきましょう。

Twitter APIでは汎用的なRequests-OAuthlibを利用しますが、例えば、同じOAuth認証でも、GoogleのAPIでは専用のPythonライブラリも用意されています。

GoogleやMicrosoft Azureのような非常に大規模なAPIには、専用のライブラリが開発されていることもあるので、機械的に同じライブラリを使用し続けるのではなく、新しいAPIを利用するときはどのように開発したらよいか都度調べるのが良いでしょう。

認証情報を取得してAPIにアクセスする

まずは上のような記事を参考にして、Twitter APIに登録、ログイン後に「Consumer API key」、「Consumer API Secret key」、「Access token」、「Access token secret」の4つの認証情報を取得します。

from requests_oauthlib import OAuth1Session #pip install requests-oauthlib
import pandas

CK = '取得したキーを入れる' # Consumer API key
CKS = '取得したキーを入れる' # Consumer API Secret key
AT = '取得したキーを入れる' # Access token
ATS = '取得したキーを入れる' # Access token secret

def search_tweets(screen_name):
    twitter = OAuth1Session(CK, CKS, AT, ATS)

    screen_name = screen_name.replace(" ","") #screen nameに空白が含まれていると自分(開発者)のツイートが表示される
    url = "https://api.twitter.com/1.1/statuses/user_timeline.json"
    params = {'screen_name': screen_name, 'count': '200', 'include_rts': 'false'}

    req = twitter.get(url, params=params, timeout=5)

    if req.status_code != 200:
        return "エラーが発生しました。ステータスコード: {}".format(req.status_code)

1行目で先ほど解説したrequests_oauthlibをimportします。

2行目では取得したデータを保存し、csvに出力するためのライブラリであるPandasをimportします。

4~7行目では取得した4種類の認証キーをそれぞれ4つの変数に代入し、定数のようなかたちで設定します。今回の認証キーのように、プログラムにより値を操作することがなく複数の関数で使われる見込みがある定数は、関数の外側に出し大文字表記の変数に代入するのが通例です。

9行目からスクリーンネームを入力値とし、CSVを出力する自作の関数search_tweets()を定義します。※APIの基本の解説では関数を作りませんでしたが、今回は他のPythonプログラムから読み込むことを前提としているので、関数を作るのは必須です。

10行目でRequests-OAuthlibのOAuth1Session()メソッドにより、4種の認証キーをセットしてAPIへの接続準備を行います。ここの内部的な動作の意味はOAuthの仕組みなどを理解していないと難しいので、OAuth1.0で認証を行うときの決まった書き方だと覚えるのが良いでしょう。

12行目では、引数として渡されるスクリーンネームについて、余分なスペースを削除する処理を行っています。これは、入力値に誤ってスペースが含まれている(例えばYamada Taro)と、Twitter Developerに登録した自分のアカウントのツイートが表示されるというTwitte APIの仕様への対策です。

今回は最初から書いてしまっていますが、このような仕様は公式ドキュメントに(おそらく)明記されておらず、実際の開発中のデバッグにより見つかったりするものなので、後から適宜追記していく必要があるものです。

13行目以降は、Requestsライブラリの基本的な使い方です。

13行目で、URLを変数に代入。

14行目で、URLに渡すパラメータを代入。Twitter APIの仕様を確認し、screen_nameに引数として渡されたスクリーンネーム、countにツイートの表示数200(最大値)、include_rtsをfalseとしてリツイートは非表示とします。

16行目で、requestsのget()メソッドによりTwitter APIとGET通信を行うコードを書きます。

18~19行目で、接続時にHTTPエラーが発生した場合=200以外のステータスコードが返ってきた場合のエラー処理を書いて、APIに接続するまでのコードはひとまず完成です。

Twitter APIでは、認証情報が違うなど正常に接続できなかった場合、200以外のエラーの種類に応じたステータスコードが返ってきます。

エラーが発生しているのにステータスコードが200を返してくるようなAPIの場合、または、JSON形式のエラー情報を解析・表示した場合は、JSONの中身に応じたエラー処理を書く必要があります。

Webアプリなどを公開する場合、流出すると問題のある認証キーを直接コードに書くことはセキュリティ上好ましくありません。コードにパスワードや認証キーを直接書くのは軽くローカル環境で試してみたいときだけにしましょう。

実際に運用するときは、認証キーをサーバーの環境変数として設定したり、別ファイルに認証情報を書き込んでそれを読み込むなど、ワンクッション入れるようにしましょう。

import os
CK = os.environ["TWITTER_CK"]
CKS = os.environ["TWITTER_CKS"]
AT = os.environ["TWITTER_AT"]
ATS = os.environ["TWITTER_ATS"]

Pythonの標準モジュールであるosのenvironにより環境変数を取得することができます。本番環境では、環境変数として認証キーを設定したうえで、 今回の認証キーの代入部分は上記のようなコードに差し替えることをおすすめします。

APIから返ってきたJSONを解析する

では、先ほど作成したfetch_tweets()にJSONを辞書形式に変換するコードを最後の行に追記して、APIから返ってきたデータを確認してみましょう。

def search_tweets(screen_name):
    twitter = OAuth1Session(CK, CKS, AT, ATS)

    screen_name = screen_name.replace(" ","") #screen nameに空白が含まれていると自分(開発者)のツイートが表示される
    url = "https://api.twitter.com/1.1/statuses/user_timeline.json"
    params = {'screen_name': screen_name, 'count': '200', 'include_rts': 'false'}

    req = twitter.get(url, params=params, timeout=5)

    if req.status_code != 200:
        return "エラーが発生しました。ステータスコード: {}".format(req.status_code)

    timeline = req.json()
    print(timeline)

if __name__ == '__main__':
    screen_name = "twitterapi"
    search_tweets(screen_name)
出力結果
[{'contributors': None,
  'coordinates': None,
  'created_at': 'Wed Jul 24 15:56:09 +0000 2019',
  'entities': {'hashtags': [],
               'symbols': [],
               'urls': [{'display_url': 'twitter.com/i/web/status/1…',
                         'expanded_url': 'https://twitter.com/i/web/status/1154057692723519494',
                         'indices': [117, 140],
                         'url': 'https://t.co/8YgCwYoE3q'}],
               'user_mentions': []},
  'favorite_count': 86,
  'favorited': False,
  'geo': None,
  'id': 1154057692723519494,
  'id_str': '1154057692723519494',
  'in_reply_to_screen_name': None,
  'in_reply_to_status_id': None,
  'in_reply_to_status_id_str': None,
  'in_reply_to_user_id': None,
  'in_reply_to_user_id_str': None,
  'is_quote_status': True,
  'lang': 'en',
  'place': None,
  'possibly_sensitive': False,
  'quoted_status': {'contributors': None,
                    'coordinates': None,
                    'created_at': 'Tue Jun 11 22:13:27 +0000 2019',
                    'entities': {'hashtags': [],
                                 'symbols': [],
                                 'urls': [{'display_url': 'twitter.com/i/web/status/1…',
                                           'expanded_url': 'https://twitter.com/i/web/status/1138569964032385025',
                                           'indices': [117, 140],
                                           'url': 'https://t.co/qMtoumuG1e'}],
                                 'user_mentions': []},
                    'favorite_count': 99,
                    'favorited': False,
                    'geo': None,
                    'id': 1138569964032385025,
                    'id_str': '1138569964032385025',
                    'in_reply_to_screen_name': None,
                    'in_reply_to_status_id': None,
                    'in_reply_to_status_id_str': None,
                    'in_reply_to_user_id': None,
                    'in_reply_to_user_id_str': None,
                    'is_quote_status': False,
                    'lang': 'en',
                    'place': None,
                    'possibly_sensitive': False,
                    'retweet_count': 77,
                    'retweeted': False,
                    'source': '<a href="http://twitter.com" '
                              'rel="nofollow">Twitter Web Client</a>',
                    'text': 'Starting July 15, 2019, all connections to the '
                            'Twitter API (and all other Twitter domains) will '
                            'require TLS 1.2. Re… https://t.co/qMtoumuG1e',
                    'truncated': True,
                    'user': {'can_media_tag': True,
                             'contributors_enabled': False,
                             'created_at': 'Wed May 23 06:01:13 +0000 2007',
                             'default_profile': False,
                             'default_profile_image': False,
                             'description': 'The Real Twitter API. Tweets '
                                            'about API changes, service issues '
                                            "and our Developer Platform. Don't "
                                            "get an answer? It's on my "
                                            'website.',
                             'entities': {'description': {'urls': []},
                                          'url': {'urls': [{'display_url': 'developer.twitter.com',
                                                            'expanded_url': 'https://developer.twitter.com',
                                                            'indices': [0, 23],
                                                            'url': 'https://t.co/8IkCzCDr19'}]}},
                             'favourites_count': 31,
                             'follow_request_sent': False,
                             'followed_by': False,
                             'followers_count': 6102235,
                             'following': False,
                             'friends_count': 12,
                             'geo_enabled': False,
                             'has_extended_profile': True,
                             'id': 6253282,
                             'id_str': '6253282',
                             'is_translation_enabled': False,
                             'is_translator': False,
                             'lang': None,
                             'listed_count': 12856,
                             'location': 'San Francisco, CA',
                             'name': 'Twitter API',
                             'notifications': False,
                             'profile_background_color': 'C0DEED',
                             'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png',
                             'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png',
                             'profile_background_tile': True,
                             'profile_banner_url': 'https://pbs.twimg.com/profile_banners/6253282/1497491515',
                             'profile_image_url': 'http://pbs.twimg.com/profile_images/942858479592554497/BbazLO9L_normal.jpg',
                             'profile_image_url_https': 'https://pbs.twimg.com/profile_images/942858479592554497/BbazLO9L_normal.jpg',
                             'profile_link_color': '0084B4',
                             'profile_sidebar_border_color': 'C0DEED',
                             'profile_sidebar_fill_color': 'DDEEF6',
                             'profile_text_color': '333333',
                             'profile_use_background_image': True,
                             'protected': False,
                             'screen_name': 'TwitterAPI',
                             'statuses_count': 3674,
                             'time_zone': None,
                             'translator_type': 'regular',
                             'url': 'https://t.co/8IkCzCDr19',
                             'utc_offset': None,
                             'verified': True}},
  'quoted_status_id': 1138569964032385025,
  'quoted_status_id_str': '1138569964032385025',
  'retweet_count': 83,
  'retweeted': False,
  'source': '<a href="https://mobile.twitter.com" rel="nofollow">Twitter Web '
            'App</a>',
  'text': 'TLS 1.2 reminder: this change will be enacted as of tomorrow, July '
          '25, 2019. Please reference our developer forum p… '
          'https://t.co/8YgCwYoE3q',
  'truncated': True,
  'user': {'can_media_tag': True,
           'contributors_enabled': False,
           'created_at': 'Wed May 23 06:01:13 +0000 2007',
           'default_profile': False,
           'default_profile_image': False,
           'description': 'The Real Twitter API. Tweets about API changes, '
                          "service issues and our Developer Platform. Don't "
                          "get an answer? It's on my website.",
           'entities': {'description': {'urls': []},
                        'url': {'urls': [{'display_url': 'developer.twitter.com',
                                          'expanded_url': 'https://developer.twitter.com',
                                          'indices': [0, 23],
                                          'url': 'https://t.co/8IkCzCDr19'}]}},
           'favourites_count': 31,
           'follow_request_sent': False,
           'followed_by': False,
           'followers_count': 6102235,
           'following': False,
           'friends_count': 12,
           'geo_enabled': False,
           'has_extended_profile': True,
           'id': 6253282,
           'id_str': '6253282',
           'is_translation_enabled': False,
           'is_translator': False,
           'lang': None,
           'listed_count': 12856,
           'location': 'San Francisco, CA',
           'name': 'Twitter API',
           'notifications': False,
           'profile_background_color': 'C0DEED',
           'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png',
           'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png',
           'profile_background_tile': True,
           'profile_banner_url': 'https://pbs.twimg.com/profile_banners/6253282/1497491515',
           'profile_image_url': 'http://pbs.twimg.com/profile_images/942858479592554497/BbazLO9L_normal.jpg',
           'profile_image_url_https': 'https://pbs.twimg.com/profile_images/942858479592554497/BbazLO9L_normal.jpg',
           'profile_link_color': '0084B4',
           'profile_sidebar_border_color': 'C0DEED',
           'profile_sidebar_fill_color': 'DDEEF6',
(中略)

Twitter APIのドキュメントや実際のツイートと照らし合わせながら返ってきたJSON(変換した辞書)の中身を確認すると、'text'がツイート内容、'favorite_count'がお気に入り数、'retweet_count'がリツイート数、'created_at'がツイート日時であることがわかります。

これらの情報をもとに、次はAPI返ってきたデータを抽出・加工します。

この項目で追記した

if __name__ == '__main__':
    screen_name = "twitterapi"
    search_tweets(screen_name)

のif name == 'main':は、この.pyファイルが他のPythonプログラム(Flask)から読み込まれることを前提として作られているので、必須です。

読み込まれることを前提としたファイル中のif name == 'main':文は、今回のようなデバッグを行うために書かれます。

データの加工

from requests_oauthlib import OAuth1Session #pip install requests-oauthlib
import pandas
from datetime import datetime, timezone, timedelta
    timeline = req.json()

    columns = ["text", "favorite", "retweet", "created_at"]
    df = pandas.DataFrame(columns=columns)

    for tweet in timeline:
        text = tweet['text']
        favorite = tweet['favorite_count']
        retweet = tweet['retweet_count']

        created_at = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
        created_at = created_at.astimezone(timezone(timedelta(hours=+9)))
        created_at = datetime.strftime(created_at, '%Y-%m-%d %H:%M:%S')
        se = pandas.Series([text, favorite, retweet, created_at], columns)
        df = df.append(se, ignore_index=True)

まず、ツイート日時のデータの処理を行うため、Pythonの標準ライブラリであるdatetimeからdatetime、timezone、timedeltaの3つのメソッドをimportします。今回は、

Wed Jul 24 15:56:09 +0000 2019

のような、日本人にとっては非常に分かりづらい日時表記になっている上に日本時間ではないため、日時の変換処理を行いますが、最初から分かりやすいかたちの日時データが返ってくるAPIではこのような処理は必ずしも必要ありません。

22行目でJSONを辞書に変換した後、データの加工を行っていきます。

24~25行目で取得データを保存するために、保存したい4つの項目を列名として定義し、Pandasの空のデータフレームを作成します。

27行目から辞書形式となっている取得データが格納されている変数timelineをfor文により展開、欲しいデータを抽出・加工していきます。

28行目でツイート内容を変数textに代入、

29行目でお気に入り数を変数favoriteに代入、

30行目でリツイート数を変数retweetに代入、

32行目でツイート日時の文字列をdatetimeライブラリのdatetimeオブジェクトに変換した上で、変数created_atに代入、

33行目でタイムゾーンを日本時間に変換、

34行目でdatetimeオブジェクトを再び文字列に変換して見やすい表記に変えています。

35行目で4つの項目をPandasのSeriesとして格納し、

36行目でデータフレームに追加しています。

日時の表記を変えたいときは、Pythonの標準ライブラリであるdatetimeを用いて、

created_at = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
created_at = datetime.strftime(created_at, '%Y-%m-%d %H:%M:%S')

と、

  1. 文字列をdatetimeオブジェクトに変換
  2. datetimeオブジェクトを文字列に変換

という流れで行うのが一般的です。

今回はさらにタイムゾーンの変換も行うため、日時の計算を行うtimedeltaメソッドとタイムゾーン情報をdatetimeオブジェクトに付与するastimezoneメソッドを組み合わせて、日本時間に変換しています。

CSVへの保存(コードのまとめ)

from requests_oauthlib import OAuth1Session #pip install requests-oauthlib
import pandas
from datetime import datetime, timezone, timedelta

CK = '取得したキーを入れる' # Consumer API key
CKS = '取得したキーを入れる' # Consumer API Secret key
AT = '取得したキーを入れる' # Access token
ATS = '取得したキーを入れる' # Access token secret

def search_tweets(screen_name):
    twitter = OAuth1Session(CK, CKS, AT, ATS)

    screen_name = screen_name.replace(" ","") #screen nameに空白が含まれていると自分(開発者)のツイートが表示される
    url = "https://api.twitter.com/1.1/statuses/user_timeline.json"
    params = {'screen_name': screen_name, 'count': '200', 'include_rts': 'false'}

    req = twitter.get(url, params=params, timeout=5)

    if req.status_code != 200:
        return "エラーが発生しました。ステータスコード: {}".format(req.status_code)

    timeline = req.json()

    columns = ["text", "favorite", "retweet", "created_at"]
    df = pandas.DataFrame(columns=columns)

    for tweet in timeline:
        text = tweet['text']
        favorite = tweet['favorite_count']
        retweet = tweet['retweet_count']

        created_at = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
        created_at = created_at.astimezone(timezone(timedelta(hours=+9)))
        created_at = datetime.strftime(created_at, '%Y-%m-%d %H:%M:%S')
        se = pandas.Series([text, favorite, retweet, created_at], columns)
        df = df.append(se, ignore_index=True)

    df.to_csv('保存するファイル名.csv', index=False)
    return

if __name__ == '__main__':
    screen_name = "twitterapi"
    search_tweets(screen_name)

データフレームのCSVへの出力は非常に簡単で、38行目にto_csvを追記するだけです。

試してみるとツイート情報がCSVとして生成されていることがわかります。

Flaskに組み込むときに多少のコードの変更は行いますが、これでツイート情報をCSVとして出力するプログラムが完成しました。このプログラムは「fetchtweets.py」という名前で保存し、最後の記事で改めて使うことになります。

タイトルとURLをコピーしました