<

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 を作ることで、金銭的な節約もしてみたいと思っています。

Tags

【大阪】GCPUG Kansai 〜 Cloud Next Extended ~ - 2019年5月14日 参加レポート

2019-05-22

こちらの参加しましたので、ご報告します。hashtagはこちらです。next19extended 目的 2019/04/09 ~ 04/11 にサンフランシスコで開催された Google Cloud Next '19 San Francisco で発表された Google Cloud の 新サービスに関する解説や振り返りの内容がメインのイベントとなります!...

Cloud Runをたった3ステップでデプロイしてみた (golang)

2019-04-11

Cloud Run とは? Cloud Run is a managed compute platform that enables you to run stateless containers that are invocable via HTTP requests. Cloud Run is serverless ※ https://cloud.google.com/run/ 詳しくは割愛するが、Cloud FunctionsやApp Engineと同じようなサーバーレスで動作するもの。コンテナをdeployするため、GKEから制御することもできる。...

ブラウザの仕組みを学ぶ

2021-05-24

Webフロントエンジニアたるもの、ブラウザの仕組みに興味を持つのは自然の摂理です。本記事では、私がブラウザの仕組みを学んでいく過程を備忘録として残します。...

リモートワークになってから『気軽にすぐ聞く』ことが難しくなった

2021-03-10

リモートワークが普及しつつある今、オンラインでの仕事に慣れているエンジニアも多いのではないでしょうか。私も、そのエンジニアの一人であり、約1年はリモートワークしています。そんな中、久々に会社へ出社すると、気軽に話しかける楽さ を実感しました。この体験について、深堀りしたいと思います。...

20代後半エンジニアである私がこれから学ぶべきこと

2020-10-29

私は、現在26歳のWebエンジニアです。これまでの技術に対する学び方と、これからの技術に対する学び方について、少し考えたいと思っています。...

Micro Frontends を調べたすべて

2020-10-07

Micro Frontendsに関わる記事を100件以上読みました(参考記事に記載しています)。そこから得たMicro Frontendsについてこの投稿に記録します。また、調査メモについて、次のリポジトリに残しています。...

ZoomのMeetingを自動生成するGASライブラリ zoom-meeting-creator を作った

2020-06-06

みなさん、Zoom使っていますか? ZoomのMeetingを自動生成するGASライブラリを公開しましたので、そのきっかけと使い方について紹介しようと思います。...

アカウント画像一括更新ツールを作ったので、紹介と学びについて

2020-06-04

GoogleやGithubなど、様々なサービスのプロフィール情報(画像, etc)を一括更新するツール、puppeteer-account-manager を開発しました。開発の目的や、開発から得た知見を紹介します。...

Micro Frontends を学んだすべて

2020-05-04

Micro FrontendsというWebフロントエンドアーキテクチャがあります。このアーキテクチャを知るために、書籍を読み、簡単なサンプルWebアプリを開発しました。そこから学んだことをすべて議事録として残したいと思います。...

TwitterにあるLinkを収集するツール Cotlin で、世界中のプレゼンテーション資料を知ろう

2020-03-15

Twitterに投稿されているLinkを収集するツール Cotlin を作りました。Collect links in tweet から、Cotlinという名前にしました。Androidのアレに似ています。...

Google Apps Script で FetchAllとRedirctURL の組み合わせは悪い

2020-02-24

Google Apps Script (以下、GAS)で、困ったことがあったので備忘録として残しておこうと思います。...

GMailをGCalendarに登録するサービス rMinc を作ってみた

2020-02-17

ターゲットユーザー * GMailとGCalendarを使っている人 メールを開くって面倒じゃないですか? 例えば、次のようなメールを受信していたとします。* アマゾンで商品を購入した際、お届け予定日が記載されたメール * 映画館(TOHOシネマ)でネット予約した際、上映日が記載されたメール * ホテルをネット予約した際、宿泊日が記載されたメール...

1コマ漫画検索サービスTiqav2 (Algolia + Cloudinary + Google Cloud Vision API) 作ってみた

2020-02-08

画像で会話って楽しい 皆さん、チャットツールでコミュニケーションするとき、絵文字や画像って使ってますか?僕はよく使ってます。人とコミュニケーションするのに、文字だけだと堅苦しいイメージですよね。例えば、『OKです、それで先に進めて下さい。』というフレーズだけだと、相手がどのような感情なのか読み取りにくいです。...

フィリピンに行ってきたら、日本は良いなって思うようになった

2019-10-27

2019年10月11日~2019年10月15日の5日間、フィリピンに行ってきました。日本人男性(前職の先輩:Kikuchi)とフィリピン人女性が結婚するため、その結婚式旅行に同伴させて頂きました。Kikuchiさんとは、私が新人の頃に大変お世話になった方なので、お祝いの気持ちが込み上げてきました。:)...

技術書典7で初執筆した経験をすべて公開

2019-09-06

技術書典7で初執筆しました。記事の目的 * 執筆でどういったことをしたのかの備忘録 * 執筆を考えている人の助けになりたい実際に販売する本は↓のものです。...