twitterに動画付きで予約投稿か自動投稿を行うツールを探していたんですが、要望に合う物が見つからなかったので投稿数無制限・無料・動画付きツイート可でtweetを繰り返すbotを自作しました。
自動でツイートしたいデータをすでにエクセルやCSVで管理している人もいるかと思ったので、今回はgoogleスプレッドシートをベースにGASスクリプトで作ってみました。記事の下の方でコピペで動くコードを公開していますんで、良かったら使ってみて下さい。
仕上がりはこんな感じ
上記のサンプル動画はFANZA動画のデータを元にtweetしたのであだるてぃーな内容になっていますが、googleスプレッドシートに準備する内容を変えればどんなジャンル・動画でも自動でツイートできます。
自作に必要な物
- Twitter API Key
- Twitter API Key Secret
- googleアカウント
まずは一番重要なTwitterAPIを使うための申請を行う必要があります。TwitterAPIの申請方法は丁寧な解説記事がたくさんネット上にあるので「twitter api 申請」とかでググって調べてみて下さい。
googleの無料アカウントの取得方法はこちらで解説しています。
twitterで自動化OK・NGな操作を調べた
twitter操作の自動化に関して調べると垢バンされるという記述もあったので、とりあえず「検索のルールと留意点について」「Twitterの自動化開発ルール」辺りの公式ページでルールを確認してみます。まとめると大体こんな感じ。
【基本ルール】
禁止事項 |
内容が同じまたは似ているリンクやツイートを繰り返し投稿する。 |
特定のトレンドトピックやハッシュタグ(#記号の付いたキーワード)を乱用する。 |
ツイートや返信の送信を自動化する。 |
ボットやアプリケーションを使って特定のキーワードで構成された似たような文章を投稿する。 |
複数のアカウントで同じような文章を投稿する。 |
フォローとフォロー解除を過剰に行う。 |
【自動化ルール】
自動化操作 | 自動化 | 補足 |
ツイート | 原則OK | 同一内容のツイートやスパムなど禁止されているコンテンツはNG |
@ツイート | 原則NG | 不特定多数の利用者に一方的にメッセージを送る、キーワード検索にヒットしただけのツイートに対して相手方に許可を得ず自動的に返信を送る事はNG |
返信 | 原則NG | 不特定多数の利用者に一方的にメッセージを送る、キーワード検索にヒットしただけのツイートに対して相手方に許可を得ず自動的に返信を送る事はNG |
ダイレクトメッセージ | 原則NG | 相手方が許可していない一方的なダイレクトメッセージを一括送信または自動送信することはNG |
いいね | NG | 詳細の記述が無いので完全にNGっぽい |
リツイート | 原則OK | 一括投稿や過剰投稿、スパム的なリツイートNG |
フォローとフォロー解除 | 原則OK | 大量または無差別にフォローしたり、フォロー解除はNG |
リストまたはコレクションへの追加 | 原則OK | 大量または無差別にリストに追加したり、ツイートを大量または無差別にコレクションに追加するのはNG |
ポイントさえ注意すれば意外と自動化は許容されている印象でした。大量・無差別・同じ文章・乱用辺りを気を付ければ垢バンは回避できそうな感じですね。上記は僕なりの理解なので参考程度にして下さい。
スクリプトの仕様と自動処理の流れ
ツイートの自動化について気を付けるポイントは大体分かったので、制約と要件を満たす仕組みを考えてみます。
投稿可能な動画の制限
twitterの仕様として投稿できる動画は140秒以内(2分20秒)かつ512MB以内に収める必要があります。この制限を超えるとエラーが返されて投稿できなくなるので注意です。
tweet元のデータはgoogleスプレッドシートで用意
tweet元のスプレッドシートは定型の雛形でデータを用意する必要があります。テンプレートは下記のリンクからダウンロードできますんで必要な方はご利用下さい。
今回のtweetbot用のデータはFANZA動画から用意しており、同様のデータを準備したい方は「【維持費0円】FANZA動画データ無限に収集し続けるGASスクリプトの作り方を公開」を実施する事で同じデータを用意する事が可能です。
投稿対象のデータを自動で選択
収集したFANZA動画のうち下記の要件を満たすレコードだけをツイートの対象とします。
- サンプル動画が用意されている
- 未ツイートの動画
これらをフィルタリングして不要な動画を除外します。FANZAの人気ジャンルであれば数万件は動画データを利用できるので、フィルタリングで多少利用できるレコード件数が減ったとしても、ツイートのネタ切れは2-3年は心配しなくても大丈夫だと思います。
ツイートする文章を100文字以内に整形
今回はFANZA動画の説明文章を先頭から100文字以内に区切って、ツイートのコンテンツとして利用したいと思います。140文字じゃないのはアフィリエイトリンクとか固定で挿入したい文章の分余裕を持たせるためです。

FANZAの動画説明文章はAPIでは取得できない値なので、FANZA動画の商品ページをスクレイピングして取得する必要があります。ですが、FANZAの商品ページは普通にGETでリクエストしても年齢認証ページに飛ばされてしまう面倒な仕様になっているので、少し工夫する必要があるんですが具体的な方法は「【維持費0円】FANZA動画データ無限に収集し続けるGASスクリプトの作り方を公開」の記事で解説していますので、必要な方は参考にしてみて下さい。
OAuth1ライブラリでtwitterAPIの認証
OAuth1はGASスクリプトで利用できる外部ライブラリで、ツールをtwitterAPIに認証させるURLを簡単に発行してくれます。認証情報は引き継がれるので作業は1回だけですが、自作しようとすると意外と面倒なので、ありがたく使わせて頂きましょう。
twitterAPI経由で動画をアップロード
普通のtweetbotを作るだけであれば不要な工程なんですが、今回は動画付きツイートなのでtwitterAPIからツイートする前に動画ファイルをアップロードする必要があります。
動画ファイルのアップロードはINIT(セッション開始)・APPEND(アップロード)・FINALIZE(終了処理)・STATUS(ポーリング)の4つの工程で処理されツイートに付与できるようになります。
細かいルールや仕様などいくつもあるので、詳しくは公式ドキュメントを読んでみて下さい。

アップロードが完了しツイートに添付して投稿した動画ファイルは「Media Studio」から確認する事ができます。
動画付きでツイート
動画のアップロードさえできればAPI経由でツイートする事自体は簡単で、数行のスクリプトで実現可能です。詳しくは次の項でサンプルコード掲載しています。
ちなみに動画ファイルをアップするエンドポイントとツイートするエンドポイントはそれぞれ異なるので混同しないように注意です。
動画アップロード~動画付きツイートのサンプル
下記のコードはtwitterAPIを経由した動画付きツイートの1例です、このままコピペしても動きませんしTwitterBotができるわけじゃないので、自作用の参考程度にご利用下され。
function myFunction() {
const twitterService = getService();
const sample_movie_url = 'https://cc3001.dmm.co.jp/litevideo/freepv/4/422/422base00035/422base00035_sm_s.mp4';
const content = 'これはテスト投稿です';
if (twitterService.hasAccess()) {
const endpoint_status = 'https://api.twitter.com/1.1/statuses/update.json';
const endpoint_media = 'https://upload.twitter.com/1.1/media/upload.json';
//動画の取得
const movie_blob = UrlFetchApp.fetch(sample_movie_url).getBlob();
const movie_file_size = movie_blob.getBytes().length;
console.log(movie_file_size);
const movie_64 = Utilities.base64Encode(movie_blob.getBytes());
const movie_64_file_size = movie_64.length;
console.log(movie_64_file_size);
//INIT
const movie_init_option = {
'method' : "POST",
'payload': {
'command':'INIT',
'media_type':'video/mp4',
'media_category':"tweet_video", //30秒超え対策
'total_bytes':movie_file_size
}
};
const movie_init = JSON.parse(twitterService.fetch(endpoint_media, movie_init_option));
console.log(movie_init);
//APPEND
const segment_index = 0;
const bytes_sent = 0;
const chunk_size = 1000000;
const chunk_num = Math.ceil(movie_64_file_size / chunk_size);
for (let index = 0; index < chunk_num; index++) {
const chunk = movie_64.slice(chunk_size * index, chunk_size * (index + 1));
console.log(chunk.length);
const movie_append_option = {
'method' : "POST",
"muteHttpExceptions" : true,
'payload': {
'command':'APPEND',
'media_data':chunk,
'media_id':movie_init['media_id_string'],
'segment_index':index
}
};
twitterService.fetch(endpoint_media, movie_append_option);
}
//FINALIZE
const movie_finalize_option = {
'method' : "POST",
"muteHttpExceptions" : true,
'payload': {
'command':'FINALIZE',
'media_id':movie_init['media_id_string']
}
};
const movie_finalize = JSON.parse(twitterService.fetch(endpoint_media, movie_finalize_option));
console.log(movie_finalize);
// STATUS
while (true) {
var movie_status_option = { 'method':"GET" };
var movie_status = JSON.parse(twitterService.fetch(endpoint_media+"?command=STATUS&media_id="+movie_init['media_id_string'], movie_status_option));
console.log(movie_status);
if (movie_status["processing_info"]["state"] == "succeeded") {
break;
} else if (movie_status["processing_info"]["state"] == "failed") {
sheet.getRange(i, 14).setValue(movie_status["processing_info"]["error"]["message"]);
throw new Error(movie_status["processing_info"]["error"]["message"]);
} else {
Utilities.sleep(movie_status["processing_info"]["check_after_secs"] + 1);
}
};
// tweet
const movie_tweet_option = {
'method' : "POST",
"muteHttpExceptions" : true,
'payload': {
'status':content,
'media_ids':movie_init['media_id_string']
}
};
const tweet = JSON.parse(twitterService.fetch(endpoint_status, movie_tweet_option));
console.log(tweet);
} else {
Logger.log(service.getLastError());
}
}
// サービス取得
function getService() {
return OAuth1.createService('サービス名')
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
.setConsumerKey(PropertiesService.getScriptProperties().getProperty("CONSUMER_API_KEY"))
.setConsumerSecret(PropertiesService.getScriptProperties().getProperty("CONSUMER_API_SECRET"))
.setAccessToken("1323717410482909184-r7CiM0WpDzMaT9m9rQrba1AswRcSXw", "L54jND63Y2e5qAjMh8IcDUrhPBNhQrDJ2U7VaXoM3gyeh")
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties());
}
自動ツイートの頻度を計算
GASはトリガーという機能で簡単に定期実行を設定する事が可能で、今回作成した動画付きツイートを行うスクリプトを登録すると指定した時間毎に実行されます。

ですが、googleの無料アカウントで実行するGASには利用可能なリソースに制限があり、その範囲内にFANZA動画の収集~動画付き自動ツイートまでを収める必要があります。今回の実装に影響のあるリソースはこんな感じ。
- スクリプト1回の実行時間制限:6分
- 1日のスクリプト総実行時間:90分
- 1日のURLFetchの呼び出し回数:20,000回
ポイントはスクリプトを動かしている間の時間にリミットが設定されているという事ですね。
今回自作したスクリプトでは動画付きツイートを1回行うと大体20-30秒消費します。遅いベースの30秒で計算すると、スクリプトは90分/日が天井なので90分=5,400秒/30秒=180回/日が動画付きツイートの理論上の最大値です。
ですが、僕のようにFANZA動画データの収集もGASスクリプトで自動化しているなど、動画付きツイート以外でもスクリプトを実行してリソースを消費している場合は、180回/日より実行可能回数が減るのでその辺も加味してツイートの実行頻度は計算する必要があります。
まとめ
長くなってしまったので、今回はTwitterのルール確認とシステムの仕様を決める所で区切ろうと思います。次回は具体的な作業手順をスクショで解説しながら、実際に動画付きTweetBotを作っていきます。
↓↓続きはnote.comの方で公開しています↓↓
個人開発プログラマーを応援するメンバーシップを始めました('ω')ノ
質問・要望・共同作業など、みんなのやりたい事をスマイルがお手伝いします。立ち上げたばかりでよく分かってないので、とりあえず何でもありやってみます。
コメント
こちらのbotを作成したいのですが、手順を教えていただくことはかのうでしょうか?
管理人のスマイルです(‘ω’)ノ
> こちらのbotを作成したいのですが、手順を教えていただくことはかのうでしょうか?
当記事のbotはtwitterAPI_v1.1が利用できるアカウントである事を前提として書いた物です。
当時noteで公開していたbot作成マニュアル記事は一応残してありますんで、有料で良ければお譲りできます。
必要な場合はお問い合わせからご連絡下さい。
よろしくお願いしますm(_ _)m
動画データ収集を購入しましたがこちらが販売終了になっており持て余しております。
再販の予定はありませんか?
また、こちらは自作ですか?(自作したことない、、、)
管理人のスマイルです(‘ω’)ノ
> 再販の予定はありませんか?
> また、こちらは自作ですか?(自作したことない、、、)
自作という定義が合っているか分かりませんが、googleスプレットシートにGASスクリプトをコピペしてシステムを構築する手順を公開していました。
当記事はすでにFANZA側から対策されてしまっており、データが収集できなくなったので記事の公開を止めた経緯があります。
それでも勉強のために売ってほしいという事であればログは残ってるので対応可能ですので、お問い合わせからご連絡下さい。
よろしくお願いしますm(_ _)m