🐦

Twitterのフォロワー数の推移を記録&取得するAPIをサーバーレスで作った

2022/02/22に公開約7,900字

作ったもの

Twitterのフォロワー数の遷移を記録、取得するAPIをサーバーレス構成で作ってみました🐤

以下のページでAPIを叩いて、chart.jsで表示してみています(表示の雑さはご愛嬌)

https://twitracker.shira79.dev/

要件は以下です^^

  • 対象のユーザーを登録
  • 登録したユーザのプロフィール情報を一定間隔で保存する
  • ユーザーごとのプロフィール情報を時系列順で取得する

ソースコードのリポジトリ(API部分のみ)

https://github.com/shira79/serverless-twitter

モチベーション

機能面

好きな芸能人アカウントのフォロワーの伸びを見届けたいというのが作りたいと思った理由です。要は推しを成長を見守りたいってことです^^。他人のアカウントのフォロワーの推移を確認する無料サービスは、ツイプロSOCIAL BLADEなどがあるようですが、データの保存期間に制限があったため自作に至りました。

一応サイトを公開してはいますが、自分以外が使う想定もしていないためデータ量が膨れ上がるという心配も特にしていないです。

技術面

個人的な趣味開発のため、なるべくお金は払いたくありません。したがってクラウドサービスの無料枠を組み合わせながら構築することになります。

https://www.publickey1.jp/blog/21/free_tier2021.html

上記の記事を参考にした上で、以下の点を考慮しました。

  • 自分しか使わないので、常時サーバーが立ち上がってるのはコストが高い
  • 定期実行処理があるためサーバーレスの実行環境で手軽に実装できそう
  • 使い慣れてるAWSであること

結果、Lambda + DynamoDB + API Gatewayを使うことに決めました。

さらに以下の理由からServerless Frameworkを使っています

  • サーバーレス環境を簡単に構築できる(特にLambdaのLayersとAPI Gatewayの設定を楽したい)
  • インフラ設定をコードに落としこむことで、作業間隔が空いた時にインフラの設定を把握し直す面倒さを解消したい

ちなみにサーバーレスアーキテクチャやServerless Frameworkの簡単な内容をこちらの書きましたので、読んでみてください👽

https://zenn.dev/shlia/articles/5bec35d77ef471

技術的なポイント

構成

DynamoDBにTwitterAPIから取得したデータを詰めてく形です。Lambdaにはフォロワー数などのデータの保存や、ユーザープロフィール更新といった定期実行の関数と、データを呼び出すAPI用の関数があります。

今回Serverless Frameworkで構成管理しているのはAPI部分のみです。フロント部分は別リポジトリに置いてあり、Netfilyにホスティングしてます(そっちのほうが自分は早くデプロイできるため)。

関数の定義

例として、フォロワー数などのデータを保存する関数をみていきます。functions/storeCounts.jshandleの中身を毎日実行する設定です。

serverless.yml
functions:
  storeCounts:
    handler: functions/storeCounts.handle
    timeout: 15
    events:
    - schedule: cron(0 15 * * ? *)

storeCounts.jsの中身では登録されているアカウント一覧を呼び出して、1件ずつデータを取得・保存します。

storeCounts.js
'use strict'

module.exports.handle = async (event) => {
  const utils = require('./modules/utils')
  const DbManager = require('./modules/dbManager')
  const DB = new DbManager()

  try {
    //ユーザーリストを取得
    let userList = await DB.getUserList()

    //並列で登録処理を実行する
    await Promise.all(userList.Items.map(async user => {
      return DB.storeCountData(user.id)
    }))

    return utils.getResponseData("done")

  }
  catch (e) {
    return utils.getResponseData({error:e})
  }

}

DynamoDBに登録されたアカウントを取得してます。typeというパーティションキーがuserのものを取得しています。

modules/dbManager.js
  /**
   * DynamoDBからuserのリストを取得する
   */
  getUserList(){
    let queryParams = {
      TableName: process.env['TABLE_NAME'],
      KeyConditionExpression: "#type = :type",
      ExpressionAttributeNames:{
        "#type": "type",
      },
      ExpressionAttributeValues: {
        ":type": 'user',
      }
    }

    return this.dynamoClient.query(queryParams).promise()
  }

上記の関数の中で呼ばれている、Twitter APIを叩いてDynamoDBへデータを保存する処理です。もちろん現在の時間もまとめてputします。

modules/dbManager.js
/**
   * twitterのカウントデータをDynamoDBに保存する
   *
   * @param string id
   */
  async storeCountData(id){
    let apiResponse = await this.getUserDataFromTwitter(id)
    let twitterUserData = apiResponse.data.data

    //put用のデータを作成する
    let dt = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
    let now = dt.toFormat("YYYY-MM-DD-HH24-MI-SS")

    let putItem = {
      type:'count',
      id_date: twitterUserData.id + '_' + now,
      id: twitterUserData.id,
      date: now,
      username:twitterUserData.username,
      name:twitterUserData.name,
      public_metrics:twitterUserData.public_metrics,
      //オリジナルサイズの画像パスに変換する
      profile_image_url:twitterUserData.profile_image_url.replace("_normal.", "."),
      entities:twitterUserData.entities,
      url:twitterUserData.url,
      description:twitterUserData.description,
      created_at:twitterUserData.created_at,
    }

    let putParams = {
      TableName: process.env['TABLE_NAME'],
      Item: putItem
    }

    return this.dynamoClient.put(putParams).promise()
  }

また、HTTP経由で呼び出す場合は、以下のように定義するとまとめてApi Gatewayの構築もやってくれます。

serverless.yml
getCountData:
    handler: functions/getCountData.handle
    events:
    - http:
        path: /count/{id}
        method: get
        request:
          parameters:
            paths:
              id: true
        cors: true

自分と同じようにNode.js + AWS Lambda + API Gateway + DynamoDBでAPIをつくる場合はこの記事がかなり参考になると思います。

https://www.serverless.com/blog/node-rest-api-with-serverless-lambda-and-dynamodb

DynamoDBの設計

RDBにおける正規化の思想とは違って、取得効率を高めるためにデータの構造が冗長的になっていても問題ないという考えです。このアプローチを理解するのにとても苦戦しました><

AWSのドキュメントに詳しく書いてあります。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-general-nosql-design.html
  • DynamoDB の場合は答えが必要な質問が分かるまで、スキーマの設計を開始すべきではありません。ビジネス上の問題とアプリケーションのユースケースを理解することが不可欠です。
  • DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。

今回においては、以下の2つの要件を満たす必要があります。

  • フォロワー数データをアカウントID別に取得し、日付順にソートする
  • 登録したアカウントを一覧で取得する

それをふまえて、以下のように設計しました

  1. パーティションキーはデータの種類を表すtype。アカウントに関する項目なのかフォロワー数に関する項目なのかを表す
  2. ソートキーは「アカウントID + _ + 日付」からなるid_date

1.により異なるコンテキストのデータを1つのテーブルにまとめています。また2.のように設計することで、「フォロワー数データをアカウントID別に取得し、日付順にソートする」という要件を効率的に満たします。id_dateを対象にbegins_with構文を用いて、指定したIDから始まるid_dateの項目を取得するというクエリ投げることをことで、絞り込みと並び替えを実現しています。

DynamooDBについてのserverless.ymlの記述は以下です。

serverless.yml
resources:
  Resources:
    UserDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${env:TABLE_NAME}
        # キーの型を指定
        AttributeDefinitions:
          -
            AttributeName: type
            AttributeType: S
          -
            AttributeName: id_date
            AttributeType: S
        # キーの種類を指定(ハッシュorレンジキー)
        KeySchema:
          -
            AttributeName: type # Partition key
            KeyType: HASH
          -
            AttributeName: id_date # Sort key
            KeyType: RANGE
        # プロビジョニングするキャパシティーユニットの設定
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

Layersのデプロイ

serverless-layersというプラグインを使っています。package.jsonの情報を元にして
、Layersのデプロイを簡単にやってくれます。

https://www.serverless.com/framework/docs/providers/aws/guide/layers

https://dev.classmethod.jp/articles/serverless-framework-node-modules-to-lambda-layers/

※Layersデプロイ用のS3のバケットを用意する必要があります

serverless.yml
custom:
  serverless-layers:
    layersDeploymentBucket: ${env:LAYER_BUCKET}

Twitter API

Twitterのユーザーアカウントを取得するにはUserオブジェクトを取得するAPI群を使用します。

https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/user

Userオブジェクトにはidが付いていてこれによって一位に識別しています。ちなみに@yousuck2020などの@から始まる文字列はusernameという名前がついています。

またAPIを叩くにはbearerTokenをheaderに持たせます。bearerTokenの取得のために申請が必要なのです。

https://developer.twitter.com/ja/docs/authentication/oauth-2-0/bearer-tokens

終わりに

Serverless Frameworkを触ってみましたが、確かにコンソールポチポチより楽ですし、構成がコードで管理されてるというのはいい体験だなと感じました。ただsls deployコマンドでデプロイするときに時間がかかってしまっていて、細かい修正を繰り返した際はコンソール上からいじりたいなと思うときもありました。

あとはDynamoDBの設計の概念の理解が大変かつ面白かったです。今回は要件がシンプルでしたが複雑な場合はもっと難しいんだろうな〜と思います。

インスタやTiktokでも似たようなことをやりたいのですが、APIが充実していなく残念です😅

運用して半年が立ちますが、今のところ無料できているので満足です^^

参考

記事のフォーマットは以下の記事を参考にさせていただいていますmm

Discussion

ログインするとコメントできます