Douyinのコメントを取得する:動画URLから返信構造を保ったJSONへ
douyin.commentsの現在のパラメータを確認するところから始め、最小限のリクエストでコメントを読み取り、動画の取得元、コメントID、返信関係を保持します。あわせて、上限、空の結果、タイムスタンプの解析、繰り返し観測への対処も扱います。
DouyinのコメントをJSONにエクスポートした後、見落とされがちな問題があります。ファイル内の1行は、独立したコメントである場合もあれば、別のコメントへの返信である場合もあります。すべてを独立したユーザーフィードバックとして扱うと文脈が失われ、いいね数、返信数、コメント総数を混同すると、コメント欄のエンゲージメント指標があいまいになります。
そのため、動画URLからコメントを取得する処理は、APIが配列を返した時点で終わりにはできません。完全な連携ワークフローでは、動画オブジェクトを確認し、現在のパラメータを読み取り、コメントの識別情報と返信関係を保持したうえで、今回の観測で何をカバーし、何をカバーしていないかを明示する必要があります。本記事ではEveryInfraのdouyin.commentsアクションを使って手順を順に説明します。コメントを調査用スプレッドシート、コンテンツ分析、手動レビューのワークフローに組み込もうとしている開発者に向けた内容です。
このセクションのパラメータとフィールドは、2026-09-04時点の公開カタログとソースコードで確認したものです。今回は、リクエストテンプレートから課金が発生する操作を実行していません。以下の合成JSONは正規化の例を示すためだけのものであり、実在する動画、ユーザー、コメントのサンプルではありません。
連携対象のインターフェース種別の確認
Douyinの認可ログイン、投稿などの公式オープン機能を開発する場合は、Douyin Open Platformのインターフェース一覧から該当するワークフローに進み、アプリケーションの種類と権限を確認してください。公式のアクセストークン、パラメータ、エラーコードは公式APIのものであり、本記事で扱うEveryInfraのデータリクエストにはそのまま適用できません。
本記事で扱うのは、対象の動画が確定した後にコメントを読み取る処理のみです。EveryInfraでは、動画の詳細、コメント、検索、音声文字起こしはそれぞれ別のアクションです。コメント欄で何が話題になっているかを分析したい場合、動画のタイトルや音声の文字起こしをコメント本文の代わりに使わないでください。非公開、削除済み、制限付きのコンテンツについては、動画URLを持っているからといって読み取り権限があると想定することはできません。
ステップ1:コメントの現行パラメータの確認
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=douyin' \
| jq -e '.capabilities[] | select(.action == "comments") | {
action, required_params, optional_params, param_meanings,
mode, available, returns_list,
default_limit, max_limit, response_fields
}'現在のカタログによると、douyin.commentsはurlが必須で、同期的にレスポンスを返し、リストを出力します。デフォルトの件数は20、limitの上限は100です。リスト型の機能では共通のlimitを使用できます。これらの値はリクエストの規模を制限するものであり、100件が返されることや、動画のコメントが最大100件であることを保証するものではありません。
このアクションの現在の定義では、呼び出し側が使用できるpage、cursor、has_moreパラメータは提供されておらず、ソートやフィルタのパラメータも公開されていません。最新の100件を取得するために、独自にsort: newestを追加しないでください。他のアクションでサポートされているパラメータが、commentsでもサポートされているとは限りません。必要なソートやページングをドキュメントで確認できない場合は、要件を満たさない任意のサンプル取得へプログラムを暗黙にフォールバックさせず、その要件をまだ満たせないことを明示してください。
カタログのresponse_fieldsの説明はコメントレコードに関するものであり、ページネーションの仕組みを定義するものではありません。将来、レコードにcursorという名前のフィールドが追加されたとしても、次のリクエストに進むために使用する前に、それがページネーションの仕組みの一部であることを確認する必要があります。フィールド名だけを根拠にループを書かないでください。
ステップ2:リクエスト前の動画の特定
入力には、この機能が受け付ける有効な動画リンクを使用してください。実際の業務では、運用担当者から共有文の一部や短縮リンク、クエリパラメータだけが異なる同じ動画の複数のURLが渡されることがあります。任意の文字列をそのままurlに渡すよりも、呼び出し前に対象を整理しておくほうがトラブルシューティングしやすくなります。
業務レイヤーでは、ユーザーが最初に送信した参照情報、確定したリクエストURL、確実に特定できた動画IDの3つを保存することをおすすめします。動画IDを確実に取得できない場合は、確認済みの参照元情報を一時的に使用し、管理された形で関連付けてください。桁数からIDを推測したり、短い共有コードを動画の識別子として扱ったりしないでください。
同じ動画が複数の調査リストに含まれる場合、権限の範囲内で取得した同じ観測結果を再利用できますが、その観測結果がどの調査タスクに属するかは保持しておく必要があります。逆に、タイトルや登場人物が同じでも、2つの動画を自動的に統合してはいけません。重複排除はコンテンツの同一性に基づいて行い、文言の類似性を基準にしてはいけません。
リンクの正規化では、無関係であると確認済みの表記上の違いだけを処理します。クエリパラメータを一律に削除しないでください。元の入力と実際に送信した値は別々に保持します。未確認のリダイレクト、アクセスできないコンテンツ、別のオブジェクトを指すリンクは、URLの形式を変えて再試行し続けるのではなく、手動確認に回してください。
ステップ3:1本の動画を使った最小限のリクエスト
サーバー上でEVERYINFRA_API_KEYを設定し、EVERYINFRA_DOUYIN_VIDEO_URLに使用が許可された動画URLを設定済みであるとします。以下のリクエストを1回だけ送信し、少数の結果でフィールドを確認してください。自動リトライは有効にせず、上限いっぱいの件数が返されるとは主張しないでください。
: "${EVERYINFRA_API_KEY:?set the API Key first}"
: "${EVERYINFRA_DOUYIN_VIDEO_URL:?set the authorized full video URL first}"
jq -cn --arg url "${EVERYINFRA_DOUYIN_VIDEO_URL}" '{
platform: "douyin",
action: "comments",
params: {url: $url, limit: 5}
}' | curl --silent --show-error --include --max-time 180 \
'https://api.everyinfra.com/api/v1/social' \
-H "Authorization: Bearer ${EVERYINFRA_API_KEY}" \
-H 'Content-Type: application/json' \
--data-binary @-正常な同期レスポンスでは、外側の階層がリクエストを表し、コメントの記録はresultsに入っています。id、count、billingなどの情報にも注意してください。外側のidはAPIリクエストIDとして記録し、results内のコメントのidを上書きしないようにします。180秒はこの例におけるクライアント側の待機時間の設定であり、APIの性能保証ではありません。タイムアウトした場合は、結果を不明として扱うしかありません。
初期連携では、急いでテキストだけを保存しないでください。データベースに接続する前に、コメントが想定した動画から取得されたものか、IDが安定しているか、返信フラグや関係フィールドが含まれているかを確認してください。レスポンスがJSONとして解析できない場合や、対象のオブジェクトが想定したリストでない場合は、空配列を返して成功したように見せかけるのではなく、失敗時の情報を保持し、そのレスポンスの解析を中止してください。
ステップ4:コメント内容、エンゲージメント、返信関係の区別
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/fields?platform=douyin&action=comments' \
| jq '{fields, undocumented}'現在宣言されているフィールドは、id、text、like_count、reply_count、author_name、author_id、ip_location、liked_by_author、is_reply、reply_to_id、posted_at、platformです。すべての列を無理に埋めるのではなく、用途に応じて次の4つのグループに分けられます。
- 識別と出所:id、platform、およびこのリクエストに基づいてアプリケーション側で関連付けた動画ソース。投稿者のニックネームはコメントの識別子として使用しません。
- 内容と時刻:textとposted_at。加えて、実際の観測時刻を表すために自分で記録するobserved_at。
- エンゲージメント記録:like_count、reply_count、liked_by_author。欠損値はゼロではありません。また、投稿者による「いいね」はコンバージョン効果や商業的価値を意味しません。
- 返信関係:is_replyとreply_to_id。欠けている関係は検証対象として残し、テキストの類似度や配列内の位置に基づいて親コメントを自動的に補完しないでください。
コードは取得したコメントと返信をリストにまとめ、根拠がある場合は関連IDを保持します。そのため、配列自体がフラットであっても、すべての行が意味的に同じ階層にあるとは限りません。読み込み時は、次の行が前の行への返信だと仮定するのではなく、まずコメントIDのインデックスを構築してから返信関係を処理してください。
reply_countは返信数に関する情報であり、返される返信配列の長さを保証するものではありません。is_replyがtrueなのにreply_to_idがない場合や、reply_to_idが現在の結果に存在しないコメントを指している場合は、保持すべき関係の欠落となります。架空の親コメントでツリーを補完しないでください。また、ツリーを描けるからといって、レポートでスレッドが完全であると主張すべきではありません。
ステップ5:識別情報を保持する業務用JSONの設計
動画ソースは、コメントのペイロードが動画URLのコピーを返すことに頼らず、確認済みのリクエストコンテキストからレコードに関連付けてください。データベースでは、プラットフォーム、動画の識別子、コメントIDの組み合わせをキーにできます。同じコメントが再び現れた場合は、新たな増分として重複カウントするのではなく、観測を追記するか、変化しうるフィールドを更新してください。
IDは元の文字列のまま保持するのが理想です。長い10進数のIDをいったんJavaScriptのNumberに変換してから文字列に戻すと、元の値を復元できなくなる場合があります。JSON仕様では、数値の相互運用性における精度の境界が説明されています。算術演算が不要な値は識別子として文字列のまま保持すれば、無意味な数値変換を避けられます。
以下のJavaScriptは、意図的に範囲を絞ったオフライン変換ツールです。文字列であることが確認されたコメントIDのみを受け付け、認識できない型はデータベースへの自動挿入から除外します。この記事で必要なフィールドと関係の状態のみを保持するもので、完全なSDKではなく、ネットワークリクエストも一切行いません。合成したテキストは合成例であることを明記しており、実際のユーザーフィードバックとして使用することはできません。
function mapComment(row, source) {
if (!row || typeof row.id !== "string" || !row.id.trim()) {
throw new Error("string comment id required");
}
if (row.reply_to_id != null && typeof row.reply_to_id !== "string") {
throw new Error("string parent id required");
}
const parentId = row.reply_to_id || null;
const isReply = typeof row.is_reply === "boolean" ? row.is_reply : null;
const relation = isReply === true
? parentId ? "reply_with_reference" : "reply_parent_unknown"
: isReply === false && !parentId ? "top_level"
: "needs_review";
return {
platform: "douyin",
video_ref: source.video_ref,
comment_id: row.id,
text: typeof row.text === "string" ? row.text : null,
posted_at_raw: row.posted_at ?? null,
observed_at: source.observed_at,
like_count: typeof row.like_count === "number"
&& Number.isFinite(row.like_count) && row.like_count >= 0
? row.like_count : null,
reply_to_id: parentId,
relation
};
}
console.log(mapComment(
{
id: "synthetic-reply",
text: "synthetic example, not a real review",
is_reply: true,
like_count: null
},
{
video_ref: "synthetic-video",
observed_at: "2026-09-04T00:00:00Z"
}
));この例では、タイムスタンプを無理に変換せず、posted_at_rawを保持しています。実際にデータベースへ挿入する際は、解析済みのタイムスタンプ、適用したタイムゾーン、解析ステータスを別々に設定できます。単位や形式を確認できない場合は、秒、ミリ秒、ローカル時刻のいずれかを自動的に推測しないでください。同様に、欠損したテキストはnullのままにし、モデルが元の文言を補完しないようにします。
reply_with_referenceは親への参照が存在することを示すだけです。親が実在するかどうかは、同じ動画の取得済みレコード内で確認する必要があり、見つからない場合は外部参照または未取得の参照として保持します。top_levelも現在のフィールドに基づく分類にすぎず、元のプラットフォームの議論構造が完全に復元されたことを証明するものではありません。
ソース時刻の単位と意味を確認したら、解析済みの時刻をRFC 3339に従って記録し、Zまたは明示的なUTCオフセットを保持できます。仕様が扱うのは表現形式であり、ソースの数字列が秒とミリ秒のどちらを表すかを判断したり、不明なソースのタイムゾーンを現在のマシンのタイムゾーンに置き換えたりするものではありません。
ステップ6:定期観測と全件ページングの区別
現在公開されているリクエストにはページングのパラメータがないため、まったく同じリクエストを繰り返しても、2ページ目や3ページ目ではなく複数回の観測結果が得られるだけです。いくつかの結果がたまたま異なっていても、コメント欄をすべて巡回したとは断定できません。
定期タスクを設定する場合は、隣接する観測結果をコメントの識別子でマージし、最初と最新の確認時刻を保持できます。遅れて投稿されたコメント、エンゲージメントの変化、返信関係の追加は更新できますが、今回確認されなかったことはコメントが削除されたことを意味しません。明確な削除シグナルや別途の検証根拠がない限り、今回は観測されなかったことだけを記録してください。
2つの期間を比較する必要がある場合は、まず対象の動画セット、呼び出しパラメータ、観測頻度、観測に成功した範囲を揃えてください。前週はすべての対象を確認できたのに、翌週は一部しか確認できなかった場合、コメント件数をそのままエンゲージメント低下の根拠にはできません。感情タグを1つ追加するよりも、観測範囲の違いをレポートに記載するほうが重要な場合が多くあります。
集計単位も明確にしてください。コメントの行数を数えるのか、議論スレッドを数えるのか、投稿者を重複排除して数えるのかを区別します。1つのスレッドで複数人が返信している場合と、1人が連続投稿している場合とでは、議論の構造が異なります。レポート上のユニークユーザー数を単なる行数で代用すべきではありません。信頼できる投稿者識別子がない場合は、個別の人物を数えたと主張すべきではありません。
ステップ7:エラー、空の結果、課金の個別検証
失敗を処理する際は、少なくとも不正な入力、認証または権限の失敗、クォータ不足、一時的なサービス障害、空のリストが返された場合、クライアントが完全なレスポンスを取得できなかった場合を区別してください。それぞれ取るべき対応が異なるため、一律に「コメントなし」と記録すると、ユーザーを誤解させ、監視結果も歪めます。
422の場合は、error.codeとメッセージに従ってリクエストを修正し、回数制限なしに再試行しないでください。一時的な問題の場合は、元のリクエストが実行された可能性があるかを確認してから再試行するかどうかを判断します。タイムアウトしたPOSTリクエストが送信されていないとは限りません。アクセスできないことが明確に確認されたコンテンツについては、自動試行を停止して対象範囲を再確認し、制限の回避を試みないでください。
課金情報も同じリクエストに関連付けてください。取得したコメント数から支払額を直接計算するのではなく、実際の課金情報とAPIリクエストIDを記録します。データの解析失敗はローカル処理の失敗であり、それだけを根拠に、サーバー側で処理または取り消しが完了したと推測することはできません。同様に、レスポンスに課金フィールドがない場合は、無料として記録せず、「検証待ち」としてマークしてください。
モデルにコメントを渡す前のコンテキスト保持と人によるレビューへの導線確保
コメントの分類には、スレッド単位または範囲が明確な断片を入力するのが最適です。返信が「そうでもない」だけの場合、その一言だけでは何に反対しているのかわかりません。親コメントが未取得の場合は、無理にポジティブやネガティブのラベルを付けず、コンテキスト不足の状態として保持してください。
トピック、感情、判定ルールのバージョン、モデルのバージョン、手動修正などの派生結果は別に保存してください。これらのフィールドを元コメントの事実データ列に書き戻したり、モデルが出力した信頼度をユーザーの実際の態度の測定値と解釈したりしないでください。レポートには、権限のある担当者がレビューできるよう、取得元との紐付けを十分に残し、公開資料には表示が許可された必要な内容のみを掲載します。
スケールアップ前のチェックリスト
- 対象:動画の識別情報がリクエストURLと一致し、範囲と用途が確認済みで、非公開コンテンツについても手順が省略されていないこと。
- 構造:resultsが想定どおりのリストであり、コメントIDの精度が保たれ、レコードを特定の動画にさかのぼって紐付けられること。
- 関係:返信フィールドが保持され、親コメントの欠落や不明な状態を確認でき、存在しないスレッドが作られていないこと。
- 観測:欠損値をゼロで埋めず、投稿時刻と観測時刻を分け、根拠なく全件を網羅したと主張しないこと。
- 運用:タイムアウト時に自動再送しないこと。エラー情報と課金情報にリクエスト識別子を保持し、繰り返し行った観測を新たな増分として数えないこと。
- 保存:必要なフィールドのみを保持し、生テキストと投稿者情報にアクセス制御、保持期間、削除スケジュールを設定すること。
整形後の業務用JSONのスキーマを書く際は、フィールドが記述されていること、フィールドが必須であること、値がnullを許容することはそれぞれ別の条件である点に注意してください。JSON Schemaのpropertiesは、デフォルトではフィールドを必須にしません。不明な値をすべて1つの空文字列で通すのではなく、requiredと型制約を使って要件を個別に表現してください。
JSONファイルにとどまらない説明可能なデータセットの納品
再利用可能なコメントデータセットは、引き継ぐ担当者が対象範囲、取得元、観測時刻、返信関係、欠損状態を理解できるものであるべきです。これが実現できれば、JSONを分析、レビュー、レポートのワークフローに確実に組み込めます。そうでなければ、大量の行をエクスポートしても、もともとの不明点を別のファイル形式で保存しているにすぎません。
許可を得た動画1本から始め、まず構造と関係を検証してから、対象を段階的に追加してください。現在の機能は以下のドキュメントと公開中のカタログで確認できます。統合時の検証を、古い記事のパラメータで代用しないでください。
参考資料 LF-03