<

TikTokスクレイプ基盤をGCP上で構築してハマったこと

TikTok へスクレイプするバッチを GCP 上で構築しました。 GCP 構築のシステム設計話と、その構築時に、ハマったことを共有します。

きっかけ

2020 年、最もダウンロードされたアプリが Facebook を抜いてTikTokが一位になったそうです。

https://gigazine.net/news/20210811-tiktok-overtakes-facebook/

私も TikTok を利用しています。

ネットサーフィンをしている時に、tiktok-scraperというライブラリをcloudflare のサイトで発見しました。これを使って、TikTok の情報収集できるんじゃないかなと思い始めましたのがきっかけです。

※ スクレイプは私的利用であることが前提です。また、TikTok へ負荷をかけないようスクレイプ間隔に配慮しましょう。

tiktok-scraper

https://www.npmjs.com/package/tiktok-scraper

Scrape and download useful information from TikTok. No login or password are required. This is not an official API support and etc. This is just a scraper that is using TikTok Web API to scrape media and related meta information.

上記とおり、TikTok の WebAPI を通してスクレイプします。 ライブラリでは、特定の TikTok 動画をダウンロードすることができますが、次の切り口で、TikTok 動画を一括ダウンロードすることもできます。

  • ユーザー
  • ハッシュタグ
  • トレンド
  • 音楽
動画をダウンロード
動画をダウンロード
様々な切り口で、動画をダウンロード
様々な切り口で、動画をダウンロード

加えて、メタ情報(フォロワー数やいいね数など)も手に入ります。

中には、ユーザー画像や動画カバー画像などの TikTok CDN へのリンクもあります。(https://p16-sign-va.tiktokcdn.com)

リンクには、有効期限を示す文字が含まれており、一定の時間が経過すると Access Denied となります。

様々な切り口で、メタ情報をダウンロード
様々な切り口で、メタ情報をダウンロード

手に入れられない情報は、ログインが必要なものです。 例えば、私がフォローしているユーザーとかです。 その情報が欲しかったので、どうにかして手に入れました。(詳細は省きます) そのユーザー情報を使って、先程のユーザーという切り口で TikTok の動画やメタ情報を収集するバッチを作ろうと考えました。

※ WebAPI を叩きすぎると、TikTok 側のブラックリストに追加され、アクセス拒否されます。

システム設計

バッチを動かす環境ですが、プライベートでよく使っている GCP 上で構築しようと思いました。 バッチで収集したデータを閲覧する Web アプリケーションも作ろうと考え、Netlify と React で動かすことにしました。

Webアプリケーション UI
Webアプリケーション UI

目的

私がフォローしているユーザーの TikTok 動画やメタ情報を集めること。

I/O

  • インプット
    • ユーザー情報
  • アウトプット
    • TikTok 動画
    • メタ情報

GCP リソース選定

  • TikTok 動画
    • Cloud Storage へ保存
  • メタ情報
    • Cloud SQL へ保存
  • コンピューティングリソース
    • Cloud Run

設計図

実際に構築した GCP のシステム設計図が、次の画像のとおりです。

tiktok scrape platform overviews
tiktok scrape platform overviews

GCP リソースの用途は、次のとおりです。

GCP リソース用途
Cloud Schedulerバッチ起動のスケジュールを管理
Cloud Worlflowsバッチのワークフローを制御
Cloud Run役割に応じて処理
PubSubCloud Run を繋げる
Cloud Storage動画を保存
AutoML Vision動画のカバー画像をラベル検出
Cloud SQL全てのメタ情報を管理

各 Clour Run の役割は、次のとおりです。

Cloud Run 名役割
Loaderユーザー情報を読み込む
Processor一連の処理を行む
ScraperTikTok へスクレイプする
Storer渡された情報を保存する
Uploader動画をダウンロードし、Storage へアップロードする
Visioner画像を(Vision API を通して)ラベル情報を抽出する
APICloud SQL とのインターフェース

ハマったこと

Cloud Workflows の制限が厳しい

当初、PubSub は使わずに、Cloud Run の連携は Cloud Workflows で行おうと考えていました。 PubSub でワークフローを制御するよりも、Cloud Workflows の yaml でワークフローを制御した方が分かりやすいと思ったからです。 具体的には、Cloud Run へ HTTP リクエストし、HTTP レスポンスに応じて、次の Cloud Run を呼び出そうと考えていました。

ただ、Cloud Workflows には、次のページに書いてあるとおり、いくつかの制限があります。

https://cloud.google.com/workflows/quotas?hl=ja

特に困ったのが、全ての変数のメモリ合計が、64kb だということです。 HTTP レスポンスの Body を変数保持する構成を取ると、そのサイズを考慮しなければいけません。 いくつかやり方を見直してみたのですが、思うような形に仕上げることができず、断念しました。 結果、PubSub を使って Cloud Run を連携することになりました。 Cloud Workflows は、バッチのキック、通知などをすることとなりました。

Firestore のページカーソルに ±2 ページ以降への移動が難しい

GCP でデータストレージで、無料枠がある Firestore を当初使っていました。 理由は、単純に GCP 無料枠として Firestore があったからです。

当初、Firestore を使って、バッチと Web アプリを書いていました。 Web アプリには、バッチで収集した TikTok の動画を一覧表示する View を用意しました。

閲覧する TikTok 動画が多くなると、ページネーションが欲しくなりました。 そこで、Firestore でページネーションの実現方法を調べてみると、次の資料を発見しました。

https://firebase.google.com/docs/firestore/query-data/query-cursors?hl=ja

これを見ると、ページネーションは、現在位置から ±1 ページの移動は簡単です。 資料にあるサンプルコードのように、startAfterを使えばよいだけです。

var first = db.collection("cities").orderBy("population").limit(25);

return first.get().then((documentSnapshots) => {
  // Get the last visible document
  var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1];
  console.log("last", lastVisible);

  // Construct a new query starting at this document,
  // get the next 25 cities.
  var next = db
    .collection("cities")
    .orderBy("population")
    .startAfter(lastVisible)
    .limit(25);
});

しかし、現在位置から ±2 ページ目以降への遷移がしたい場合は、どうすれば良いでしょうか。 上記のサンプルコードで言えば、firstをコピペしてsecond変数を生成するのでしょうか。 それよりも、offsetメソッドがほしいところです。 しかし、次の資料を発見し、諦めることになります。

https://firebase.google.com/docs/firestore/best-practices?hl=ja

オフセットは使用しないでください。その代わりにカーソルを使用します。オフセットを使用すると、スキップされたドキュメントがアプリケーションに返されなくなりますが、内部ではスキップされたドキュメントも引き続き取得されています。スキップされたドキュメントはクエリのレイテンシに影響し、このようなドキュメントの取得に必要な読み取りオペレーションは課金対象になります。

という訳で、クエリカーソルを推奨されています。

解決策としては、順序を示すフィールドがあれば、解決するかもしれません。 例えば、orderというフィールドを用意し、1,2,3 とインクリメントしたデータがあれば、クリアできるかもしれません。 startAfterの引数は document オブジェクトだけではなく、orderBy 句で指定したフィールドの変数を含めることができます。

var next = db.collection("cities").orderBy("order").startAfter(50).limit(25);

これだと、1 ページ 25 個のデータを表示するならば、3 ページ目(51~75)を取得できます。(startAfterは開始点を含めません)

https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/query

そもそも、ドキュメントベースの設計よりも、RDB の設計に慣れていた私は、 Firestore よりも、Cloud SQL の方が扱いやすいと思いました。 そこで、データストレージを Firestore から Cloud SQL へ切り替えることとしました。 改修自体、Cloud Run の役割が明確に分離されていたので、一部の処理を書き換えるだけで、簡単にできました。

Eventac のリソース選択が物足りない

Cloud Run と PubSub の連携には、Eventac を使用します。

https://cloud.google.com/blog/ja/products/serverless/eventarc-unified-eventing-experience-google-cloud

昨年 10 月、60 を超える Google Cloud ソースから Cloud Run にイベントを送信できる新しいイベント機能、Eventarc を発表いたしました。Eventarc は、さまざまなソースから監査ログを読み取り、それらを CloudEvents 形式のイベントとして Cloud Run サービスに送信します。また、カスタム アプリケーションの Pub/Sub トピックからイベントを読み取ることもできます。

この Eventarc のソースとして、Cloud Storage の Object.create をトリガーとして設計を考えていました。 しかし、そのイベントをフィルタリングする選択肢は、2 つしかありません。

https://cloud.google.com/blog/ja/products/serverless/demystifying-event-filters-eventarc

できるのは、執筆時点(2021 年 8 月)で、次の 2 つです。

  • All resource
  • Specific resource

All resource は、Cloud Storage の全てのバケットにおける Object.create イベントがトリガーとなります。 Specific resource は、特定の Obeject 名が Object.create された場合のみ、トリガーとなります。 欲しいなと思ったのは、Specific resouce の正規表現によるフィルタリング、任意のバケットやフォルダの配下で限定など のフィルタリングです。例えば、gs://bucket/folder/*.json のような形式です。現状は、gs://bucket/folder/A.jsonとするしかありません。

今回は、PubSub のイベントのみでトリガーするようにしました。

PubSub をトリガーとする CloudRun で HTTP レスポンス 500 を返却すると、PubSub が再試行される

Cloud Run で、5XX 系のエラーとなった場合、PubSub の再試行されます。

https://cloud.google.com/pubsub/docs/admin?hl=ja#using_retry_policies

何度も PubSub が実行されると、Cloud Run のコンピューティングリソースが消費され続けます。 そうすると、課金が発生するので、対策が必要です。

Cloud Workflows の処理は、あまりカスタマイズできない

Cloud Workflows は、あくまでワークフローの管理です。 変数処理などは、基本的に使わず、ワークフローのタスクを連結するだけにした方が良いです。 次の資料には、Cloud Workflows で使える標準機能です。

https://cloud.google.com/workflows/docs/reference/stdlib/overview

ワークフローのタスクを並列処理する機能は、まだ実験段階なので、本番環境は使えないようです。

https://cloud.google.com/workflows/docs/reference/stdlib/experimental.executions/map

終わりに

システム設計変更が度々変更がありつつも、目的とする TikTok 動画やメタ情報を収集することは達成できました。 変更があったとしても、役割をできる限り小さく保つことで、変更に柔軟に対応することができます。 また、実際に動かすことで、気付けるポイントもあるので、フィードバックサイクルを短くすることも大切です。

まだまだ改善する余地はあります。ユーザー情報という切り口で情報収集していましたが、トレンドやハッシュタグなどからも 取得できるようにしたいです。また、ユーザーの RSS を作ることで、金銭的な節約もしてみたいと思っています。

役立ったら、☕でサポートしてね!

シェアしよう

関連するタグ