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 で動かすことにしました。
目的
私がフォローしているユーザーの TikTok 動画やメタ情報を集めること。
I/O
- インプット
- ユーザー情報
- アウトプット
- TikTok 動画
- メタ情報
GCP リソース選定
- TikTok 動画
- Cloud Storage へ保存
- メタ情報
- Cloud SQL へ保存
- コンピューティングリソース
- Cloud Run
設計図
実際に構築した GCP のシステム設計図が、次の画像のとおりです。
GCP リソースの用途は、次のとおりです。
GCP リソース | 用途 |
---|---|
Cloud Scheduler | バッチ起動のスケジュールを管理 |
Cloud Worlflows | バッチのワークフローを制御 |
Cloud Run | 役割に応じて処理 |
PubSub | Cloud Run を繋げる |
Cloud Storage | 動画を保存 |
AutoML Vision | 動画のカバー画像をラベル検出 |
Cloud SQL | 全てのメタ情報を管理 |
各 Clour Run の役割は、次のとおりです。
Cloud Run 名 | 役割 |
---|---|
Loader | ユーザー情報を読み込む |
Processor | 一連の処理を行む |
Scraper | TikTok へスクレイプする |
Storer | 渡された情報を保存する |
Uploader | 動画をダウンロードし、Storage へアップロードする |
Visioner | 画像を(Vision API を通して)ラベル情報を抽出する |
API | Cloud 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 を使用します。
昨年 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 を作ることで、金銭的な節約もしてみたいと思っています。
シェアしよう
関連するタグ
- [覚書]Reactを業務で使い始めて知ったこと
- WebComponentsでoEmbedのコンポーネントを開発して、学んだこと
- Ruby on Railsを業務で使って思ったこと
- Reactを学ぶ前に歴史を知る
- オミクロン株に感染したので、分かったことを書く
- ブラウザの仕組みを学ぶ
- リモートワークになってから『気軽にすぐ聞く』ことが難しくなった
- 20代後半エンジニアである私がこれから学ぶべきこと
- Micro Frontends を調べたすべて
- アカウント画像一括更新ツールを作ったので、紹介と学びについて
- Micro Frontends を学んだすべて
- TwitterにあるLinkを収集するツール Cotlin で、世界中のプレゼンテーション資料を知ろう
- Google Apps Script で FetchAllとRedirctURL の組み合わせは悪い
- GMailをGCalendarに登録するサービス rMinc を作ってみた
- 1コマ漫画検索サービスTiqav2 (Algolia + Cloudinary + Google Cloud Vision API) 作ってみた
- 技術書典7で初執筆した経験をすべて公開
- 手軽に安い食材を見つけられるアプリ「ぼちぼち」を開発
- Qwikでブログページを刷新して学んだこと
- ライティングの効率化ツール:AI Ghostwriterの紹介
- Stable Diffusion API 開発
- Micro Frontendsで組成するフラグメントをWeb Componentsで定義してModule Federationで共有する
- OEmbedとOGPのWebComponentsを作ったので、自分のブログサイトに使う
- Chrome拡張機能(Manifest V3)の開発で知ったこと
- Markdownで執筆するなら、WebComponentsが使えるSSG、Rocketがオススメ!
- silverbirderのポートフォリオページ刷新(v2)
- Googleアカウント画像を返却するだけのAPIを作った
- クライアントサイド(ES Module)でMicro Frontends
- Zalando tailor で Micro Frontends with ( LitElement & etcetera)
- Ara-Framework で Micro Frontends with SSR
- ZoomのMeetingを自動生成するGASライブラリ zoom-meeting-creator を作った
- アカウント画像一括更新ツールを作ったので、紹介と学びについて
- TwitterにあるLinkを収集するツール Cotlin で、世界中のプレゼンテーション資料を知ろう
- GMailをGCalendarに登録するサービス rMinc を作ってみた
- 1コマ漫画検索サービスTiqav2 (Algolia + Cloudinary + Google Cloud Vision API) 作ってみた