Skip to main content

ニコニコ動画の再生数の推移を見られるWebアプリを作った

概要

ニコニコ動画の動画について、再生数をはじめ、マイリスト数、コメント数、いいね数の過去の日次の推移を表示するWebアプリを作りました。ニコニコ推移(仮)

こんな感じで、動画のIDを入力すると過去の値を表示します。対象は再生数が3000以上の動画(2023/9/19時点で410万件程度)です。2023/9/1のあたりで線が上に飛び出ているのは、毎年8/31の初音ミク生誕祭のために8/31の再生数が大きかったためですね。ちなみに、後で述べますが途中で線が途切れている部分はデータ取得に失敗していたところです。

技術構成

ニコニコ動画が公開している、当日の断面における個々の動画の再生数などのメタデータを返すスナップショット検索API v2というAPIを毎日叩いてデータを蓄積しています。

このAPIは当日分のデータしか返りません。面白そうなデータなので今後何かに使うときのために毎日貯めておこうと思ったのですが、このような過去の再生数を返すWebサイトはほとんどない1こともあり、バックエンドの勉強も兼ねてバックエンド基盤とインタフェースのWebアプリを作ってみた次第です。

構成図はこちらです。バックエンドとフロントエンドでGitのリポジトリを2個作っています。

バックエンド: VPS (GitHub Actions), GCP (Terraform)

データ取得部分はVPS、データ基盤部分はGCPです。

VPS

RでスナップショットAPIを叩きCSVファイルで出力し、PythonでCloud Storageにアップロードするコードをcronで1日1回実行しています。データ取得の終了時にSlackに通知します。言語が分かれているのは、既存のRコードを使い回したためです。

データ取得部分をGCPではなくVPSで作っているのは、APIからのデータ取得の際にアウトバウンドの通信が発生することから通信料が定額のVPSを選んだためです。

VPSへのコードデプロイは、GitHub上のmainブランチにコミットがpushされたらVPSにssh接続してgit pullするGitHub Actionsを用いています。

ちなみにVPSの監視としてはてな製のサーバ監視サービスであるMackerelを入れており、ディスク容量がいっぱいになったときなどにSlackに通知されます。ホスト数5台まで、メトリクスのグラフ表示期間1日でよければ無料で使えてUIも見やすく、インストールも楽な便利なサービスです。個人開発で使うVPSの管理には十分ではないでしょうか。

上の画像で2023/3のグラフが途切れているところですが、新しく引っ越したVPSにMackerelを入れるのを忘れており、ディスク容量がいっぱいになってしまっておりしばらくデータ取得ができないことに気づいていませんでした。サーバ監視エージェントは入れるべきですね。

GCP

CSVファイルがCloud Storageにアップロードされたことをトリガーとして、Cloud FunctionsでCSVファイルの中身をBigQueryにwrite_appendします。また、動画IDをクエリストリングとして与えると、BigQueryからその動画IDのレコードを取得して返す認証付きのCloud Functionsも用意しています。

APIはクエリ部分の1本のみですので、API Gatewayを使わずHTTPトリガーでCloud Funtionsをコールする作りにしています。SQLインジェクション対策のためにフロントとバックエンドで両方正規表現で入力値のバリデーションをしています。

GCPのリソースはTerraformで定義し、ローカルからterraform applyすることでデプロイしています。リソース名の先頭に開発環境(dev)と本番環境(prod)を付与することで環境を分けられるようにしています。最初は全てGCPコンソールから作っていましたが、Terraformだとリソースを全部コードで定義できるので管理しやすいですね。Terraformは一度文法を覚えれば他のパブリッククラウドでも使えるのもナイスです。

Cloud Functionsなどのサーバレス関数では厳しいような重いスクレイピング・クローリングはVPS、DBはGCP(AWSでもAzureでもいいですが)というのは個人的に気に入っている構成でよく採用しています。

フロントエンド: Streamlit (Python) + Heroku

テキストボックスに動画IDを入力すると、Cloud FunctionsのAPIエンドポイントを叩いて過去の再生数などをBigQueryから取得してグラフ描画するStreamlitのWebアプリを作成しました。あわせて最新の再生数をgetthumbinfo(ニコニコ動画の非公式API)から取得して上に表示します。

HerokuのEco Dynoを利用しており、フロントエンドのリポジトリのmainブランチにコミットをpushするとデプロイされるように設定しています。

Streamlitはフロントエンドの言語の知識がなくても簡単にアプリを作れていいですね。一方、細かいところは制御しづらいかゆさもあります。実際、外部公開するWebサイトのフロントというよりは、データ分析担当や機械学習エンジニアがモックを作るときに使われるようなフレームワークですね。

技術的なTips

BigQueryのテーブル設計: パーティショニング

BigQueryのテーブルを一部抜粋します。カラムは順に日時、動画ID、再生数を表します。

lastModifiedcontentIdviewCounter
2023-09-15T08:59:37+09:00sm109744516575782
2023-09-16T08:53:20+09:00sm109744516576909
2023-09-17T08:52:41+09:00sm109744516578086

フロント側でデータを取得する際、例えば取得したい動画IDをsm1097445とすると、以下のクエリを書くことになります。

SELECT lastModified, contentId, viewCounter FROM TABLE_NAME WHERE contentId = "sm1097445" ORDER BY lastModified;

クエリ量を削減するためにこのテーブルにパーティショニングを設定します。where句で絞るcontentIdでパーティショニングしたいところですが、パーティショニング可能なのは整数範囲、時間単位、取り込み時間のいずれかです。

contentIdはアルファベットの小文字2文字+数字1文字以上で表されることを利用し、contentIdの数字部分を4000で割った余りであるidModという列をテーブルにwrite_appendする際に付け加え、この列でパーティショニングすることにしました。

lastModifiedcontentIdviewCounteridMod
2023-09-15T08:59:37+09:00sm1097445165757821445
2023-09-16T08:53:20+09:00sm1097445165769091445
2023-09-17T08:52:41+09:00sm1097445165780861445

SQLでパーティショニングのidMod列をwhere句に含めることで、理想的にはクエリサイズが1/4000に抑えられます。

SELECT lastModified, contentId, viewCounter FROM TABLE_NAME WHERE contentId = "sm1097445" and idMod = 1445 ORDER BY lastModified;

今回はcontentIdでフィルタするクエリを書くためにこのようなパーティショニングを設定しましたが、例えば同一のlastModifiedにおけるレコードを全件取得するような使い方をするならlastModified列で時間単位パーティショニングすることになります。

Terraformの環境分け(本番環境と開発環境)

Terraformでリソースを記述しているGCP部分は、本番環境(prod)と開発環境(dev)を分けられるように定義しています(今のところ環境を分けるほどの機会がないためdevしか作っていないのですが)。

Terraformで異なる環境を作成する方法としては以下の三つがメジャーなところかと思いますが、三番目の方法を取っています。

  • Terraform Workspacesを使う
  • moduleを使う
  • .tfbackendファイルと.tfvarsファイルを用いて変数で環境を分ける

具体的にはこちらです。

  • リソース名の先頭に環境名を付ける(例: prod-hoge-bucket
  • 環境名をprod.tfvars, dev.tfvarsに記載する
  • Terraformのstatusを管理するCloud Storageの情報をprod.tfbackend, dev.tfbackendに記載する

小規模のバックエンド基盤では楽な方法ですね。

詳細には、こちらの記事(Terraformでmoduleを使わずに複数環境を構築する)が丁寧に解説されています。

もう少し説明

ディレクトリ構成はこのような感じです。

.
└── terraform
    ├── envs
    │   ├── dev
    │   │   ├── dev.tfbackend
    │   │   └── dev.tfvars
    │   └── prod
    │       ├── prod.tfbackend
    │       └── prod.tfvars
    ├── main.tf
    └── variables.tf

main.tfは以下の通り

provider "google" {
  project = var.project_id
  region  = var.project_region
}
terraform {
  # バージョンは任意
  required_version = "~> 1.5.5"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.80.0"
    }
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.4.0"
    }
  }
  backend "gcs" {
    # envs/(env_name)/(env_name).tfbackendに定義
  }
}

# 例えばCloud Storageのバケットを作成してみる
resource "google_storage_bucket" "tmp_bucket" {
  name          = "${var.env}-tmp-bucket"
  location      = var.project_region
  force_destroy = var.env == "prod" ? true : false
}

envs/dev/dev.tfvarsは以下の通り

env = "dev"
project_region = "<PROJECT_REGION>"
project_id = "<PROJECT_ID>"

envs/dev/dev.tfbackend(stateを置くバックエンドのCloud Storageの情報)は以下の通り

bucket = "<BUCKET_NAME>"
prefix = "<PREFIX>"

variables.tfは以下の通り

variable "env" {
  type        = string
  description = "environment name"
}

variable "project_region" {
  type        = string
  description = "Google Cloud Region"
}

variable "project_id" {
  type        = string
  description = "Project ID"
}

以上のように用意して、terraform initするときにtfbackendファイルを、terraform planterraform applyするときにtfvarsファイルをオプションで渡すことで、環境ごとに異なるバックエンドを参照して異なるリソースを作成することができます。

# dev環境にdeployする
$ cd terraform
$ terraform init -backend-config=envs/dev/dev.tfbackend
$ terraform plan -var-file=envs/dev/dev.tfvars
$ terraform apply -var-file=envs/dev/dev.tfvars

  1. 調べた範囲だとニコログがあります。こちらはランキングに載った動画や新着動画は収集されており、該当の動画だと1時間単位でデータがありますが、該当しないと収集されていないようです。 ↩︎