【GAS】Google Apps Script 活用事例 Twitter API version 2.0で、スプレッドシートに情報を書き出してみよう!!

今年の6月に、Twitter APIを使って、特定のユーザーの情報をスプレッドシートに書き出してくれる収集Botを作りました。用途は、自社のエゴサーチ業務で使うためです。しかし、8月末に前兆なく、突然動かなくなってしまいました。.....わずか2ヶ月。短い人生でした......。

なぜ、動かなくなったのか?

OAuth 2.0? BEAER_TOKEN? よう分からず.....途方に暮れました。時が解決してくれる!!........と思っていましたが、403エラーを吐き続けて一向に直る気配がない。見かねたノンプロ研の心優しき方が上記のサイトを勧めてくださいました。

勘違いから始まった、Twitter API 2.0への対応

上記のエラーの原因が、Twitter API 2.0がリリースされ、エンドポイントが変更になり、既存のエンドポイントが廃止されたがために、エラーが出ているのかなと自分なりに考えました。

プロジェクトを作っていない事が原因でした。

APIの2.0リリースに伴いTwitter APIの申請方法が変わっていて、より簡単に、申請出来るようになっています。作成したappとprojectを結びつける作業をしないと、エラーになってしまいます。

この情報は、ノンプロ研に、APIに詳しい方がいらっしゃって、console.logで、getContentText()を出力すると、最近のAPIだと、エラー内容が、丁寧に返ってくるよ。とアドバイスを受けました。下記は、エラーが出ていた時に、出力されていた、エラーコードです。

実際のエラーコード

console.log(`HTTPデータ: ${responseApi.getContentText()}`);
//HTTPデータ: {"client_id":"*********","required_enrollment":"Standard Basic","detail":"When authenticating requests to the Twitter API v2 endpoints, you must use keys and tokens from a Twitter developer app that is attached to a Project. You can create a project via the developer portal.","registration_url":"https://developer.twitter.com/en/portal/opt-in","title":"Client Forbidden","reason":"client-not-enrolled","type":"https://api.twitter.com/2/problems/client-forbidden"}
When authenticating requests to the Twitter API v2 endpoints, you must use keys and tokens from a Twitter developer app that is attached to a Project. You can create a project via the developer portal.

Twitter API v2エンドポイントへのリクエストを認証するときは、プロジェクトに接続されているTwitter開発者アプリからのキーとトークンを使用する必要があります。開発者ポータル経由でプロジェクトを作成できます。

Twitter Developper Portal

上記のリンクから、Appとprojectを結びつける作業をします。

App作成時の英作文などは、過去記事を参考にしてください。

それぞれのversionの違い

version 1.1 とversion 2.0の違い
・1.1は、エンドポイントを細かく指定しなくても、全部(要らない情報を含め)情報が含まれていた。

・2.0からは、欲しい情報を、エンドポイントにきちんとリクエストして、 取得する必要がある。

Twitter API v1.1 でのリクエスト例

const apiUrl = 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=' + user + '&' + 'count=' + tweetCounter;

Twitter API v2 でのリクエスト例

ユーザー情報の取得(ユーザーID、アカウントの画像、フォロワーなどの情報、アカウントの説明、場所)

const apiUrl = 'https://api.twitter.com/2/users/by?usernames=' + users + '&user.fields=id,profile_image_url,public_metrics,description,location';

Twitterでの検索結果(最大で20件に指定、投稿者のID、投稿日、内容、ふぁぼ、リツイート、デバイス、ツイートID)

※無料枠だと、100件まで取得可能なようです。確か15分で、300リクエストって書いてあった気がします。

const apiUrl = 'https://api.twitter.com/2/tweets/search/recent?query=' + keyWord + '&max_results=20&tweet.fields=author_id,created_at,text,public_metrics,source,id';

特定ユーザーの最新投稿(投稿者のID、投稿日、内容、ふぁぼ、リツイート、デバイス、ツイートID)

const apiUrl = 'https://api.twitter.com/2/tweets/search/recent?query=' + 'from:' + userName + '&max_results=20&tweet.fields=author_id,created_at,text,public_metrics,source,id';
検索欄に「from:(ユーザーID)」とすると、指定したユーザー自身が発信したツイートのみを検索できます。

IDから、ユーザーネームを取得

const apiUserInfo  = 'https://api.twitter.com/2/users?ids=' + authorId + '&user.fields=profile_image_url';

tweet.fieldsのパラメーターには、usernameがありません。なので、author_idを別関数の引数として渡して、usernameと、プロフィールのアカウント画像を取得します。

リクエストのスクリプトは、postmanというサイトに記載されていますが、ちょっと、アレンジしたい場合などは、一からの試行錯誤が続き、時間が掛かってしまいました。

画像1

右上のボタンに、Run in Postmanというのがあり、テスト出来るようです。試行錯誤している時は、知らなかった.....。もっと早く知りたかった。

本職の人に教わって書きましたが、こちらのライブラリを使うと、もっと楽に出来るようです。

ちょっと検索する際は、こちらが便利でした。

TweetをIDから詳細を調べる

const apiUrl     = 'https://api.twitter.com/2/tweets/' + '1168983690451591168';

ツイートIDの調べ方

https://twitter.com/Avicii/status/1168983690451591168

statusの後の数字の羅列が、ツイートのIDとなっています。
後述しますが、https://twitter.com/ + username + /status/ + ************* の組み合わせでツイートのURLが構成されています。

{ data:
{ id: '1168983690451591168',
text: 'Tim Bergling Foundation @aviciicharity will advocate for the recognition of suicide as a global health emergency and promote removing the stigma attached to the discussion of mental health issues. https://t.co/Zb1eFFthhC' } }

固定ツイート(Pinned Tweet)の取得

画像2

const apiUrl     = 'https://api.twitter.com/2/users/by?usernames=' + users + '&expansions=pinned_tweet_id&tweet.fields=created_at,text,id,public_metrics';

下記から、Google Apps Scriptを使って、実際にユーザーの最新投稿を取得したり、検索キーワードに関連する呟きを収集するスクリプトを掲示しています。こちらは、今、実際に自分が使っているスクリプトそのものです。(もちろん、Twitter API version 2.0です。)

無料で、1.1を使ったソースコードを公開していますので、出来る事が同じなら、どっちでも良いやという方は、過去のエントリーを参考にしてください。上にも書いたように、1,1のエンドポイントは健在です。

フォロワー数などを取得するスクリプト

Twitter API version 2.0から、カンマ区切りのリストで、ユーザー情報などを取得出来るようになりました。1回に、100ユーザーまで。

ただし、GASの場合は、実行が5分までという制限があったと思うので、100人全員分のツイートをシートに書き出したりするのは難しいかもしれません。そういう場合は、Pythonとかになるのかなと思います。

画像3

GET /2/users/by (lookup by list of usernames)

APIリファレンス

/* 
* userListに存在するユーザー名のフォロワー数などを調べる
* @return {string} トークン
*
*/
function setUserInfo() {
 const sheetName   = 'userList';
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const values      = getUserInfo_(sheetName);
 
 //1列目にユーザー名が記載されているため、2列目から書き込みを開始する。
 sheet.getRange(2, 2, values.length, values[0].length).setValues(values);

}


/* 
* Twitterのアクセストークンを取得する
* @return {string} トークン
*
*/
function getToken_() {
 
 const API = {
   KEY: PropertiesService.getScriptProperties().getProperty('API_KEY'),
   SECRET_KEY: PropertiesService.getScriptProperties().getProperty('API_SECRET_KEY'),
   MILLISECOND: 100000 //tokenの有効期限(ミリ秒)
 };
 
 const tokenUrl        = 'https://api.twitter.com/oauth2/token';
 const tokenCredential = Utilities.base64EncodeWebSafe(API.KEY + ':' + API.SECRET_KEY);
 const tokenOptions    = {
   headers : {
     'authorization': 'Basic ' + tokenCredential,
     'Content-Type': 'Application/x-www-form-urlencoded;charset=UTF-8' 
   },
   method:  'post',
   payload: 'grant_type=client_credentials'
 };
 
 const responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
 const parsedToken   = JSON.parse(responseToken);
 const token         = parsedToken.access_token;
 
 //console.log(token);
 return token;
}



/* 
* シートに記載されているユーザー名を取得する
* 
* @param  {string} シート名
* @return {object} 1次元配列
*
*/

function getUserList_(sheetName) {
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const lastRow     = sheet.getLastRow();
 
 //A2から最終行まで
 const userArray   = sheet.getRange(2, 1, lastRow -1, 1).getValues().flat();
 const string      = userArray.join(',');//文字列化
 
 console.log(string);
 return string;
}//end



/* 
* シートに記載されているユーザーのフォロワー数などを取得する
* 参考URL : https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-by-username-username
* @param  {string} シート名
* @return {object} 2次元配列
*
*/

function getUserInfo_(sheetName){
 
 const users      = getUserList_(sheetName);//string
 const apiUrl     = 'https://api.twitter.com/2/users/by?usernames=' + users + '&user.fields=id,profile_image_url,public_metrics,description,location';
 const token      = getToken_();
 const apiOptions = {
   headers : {
     'Authorization': `Bearer ${token}`
   },
     method : 'get',
     muteHttpExceptions : true
 };

 const responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
 
 console.log(apiUrl);
 console.log(`HTTPデータ: ${responseApi.getContentText()}`);


 //HTTP リスポンスが200以外だったら、処理を終了
 if (responseApi.getResponseCode() !== 200){return};
 const objects = JSON.parse(responseApi.getContentText());
 console.log(objects);

 let values = [];
 
 //usersは1度、シートからユーザ名を取得する際に使用している
 const targetUsers = objects.data;
 //console.log(tweets);

 for(const user of targetUsers){
   const userName        = user.username;//ユーザーネーム
   const nameJp          = user.name;//アカウント名
   const userImage       = `=IMAGE("${user.profile_image_url}",1)`;//プロフィール画像
   const followers       = user.public_metrics.followers_count;//フォロワー数
   const profileUrl      = 'https://twitter.com/' + userName;//プロフィールページのURL
   const userDescription = user.description;//アカウントの説明
   const userLocation    = user.location;
 
   console.log(`${userName}, \n${nameJp}, \n${userImage}, \n${profileUrl}, \nフォロワー ${followers}`);
   values.push([nameJp, userImage, profileUrl, followers, userDescription, userLocation]);
 
 }//for
 return values
}//end

シートから、調べたいユーザーネームを取得します。getValues()で取得すると、2次元配列になっています。それを一次元配列→文字列化しています。

反対に、文字列を配列化する場合は、splitを使います。スクリプト例では、日付の処理で使用しています。

検索結果を取得するスクリプト

画像4

シートのA列から、検索キーワードを取得して、検索結果を取得しています。列を引数で指定する事で、読み込む列を変えられるようにしています。

画像5

GET /2/tweets/search/recent

APIリファレンス


/* 
* シートに記載されているキーワードを含むツイートを検索する
*
*/

function setSearchTweetsToSheet() {
 const sheetName   = 'search';
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const lastRow     = sheet.getLastRow() + 1;
 const results     = getRecentTweets_(1);
 
 sheet.getRange(lastRow, 1, results.length, results[0].length).setValues(results);
 
 //重複を削除
 removeDuplicates_(sheetName, 6);
 
 //水平・垂直
 setStyle_(sheetName);
 
 //日付の新しいツイートが上に
 const range = sheet.getRange('A2:J');
 range.sort([
   {column: 1, ascending: false}
 ]);
}//end



/* 
* シート上の重複を削除する
* 
* @param  {string} シート名
* @param  {number} 列
* @return 
*
*/

function removeDuplicates_(sheetName, column) {
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const range       = sheet.getRange('A2:Z').activate();
 //E列、つまり5列目
 range.removeDuplicates([column]).activate();
}



/* 
* シートからキーワードのリストを取得する
* 
* @param  {number} 列
* @return 
*
*/

function getKeyWordsList_(column) {
 const spreadsheet    = SpreadsheetApp.getActiveSpreadsheet();
 const sheet          = spreadsheet.getSheetByName('keyWords');
 
 //シートの列をアルファベットで取得する。
 //(例) B列の最終行を取得
 const columnAlphabet = sheet.getRange(1, column).getA1Notation().match(/[A-Z]/)[0];
 const lastRow        = sheet.getRange(`${columnAlphabet}:${columnAlphabet}`).getValues().filter(String).length;
 
 console.log(`列のアルファベット ${columnAlphabet}`);
 console.log(`${columnAlphabet} 列の 最終行は、${lastRow}`);
 
 //見出しを除いて、2行目からキーワードを取得
 const range = sheet.getRange(2, column, lastRow -1, 1);
 const array = range.getValues().flat();
 
 console.log(`1次元配列の内容 ${array}`);
 console.log(`取得した範囲 ${range.getA1Notation()}`);
 
 //リツイートを除外するため、-rtをしている。
 const newArray = array.map(element => element + ' -rt');
 
 console.log(newArray);
 return newArray;
}




/* 
* シート上の重複を削除する
* 
* 参考URL: https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent
* @param  {number} 列
* @return 
*
*/

function getRecentTweets_(column){
 
 const token      = getToken_();
 const apiOptions = {
   headers : {
     'Authorization': `Bearer ${token}`
   },
     method : 'get',
       muteHttpExceptions : true
 };

 //シートから、読み込む列を選択
 const keyWords = getKeyWordsList_(column);
 
 let results    = [];

 //keyWordsごとに、エンドポイントURLが変わるため、for文を回す。
 for(let i = 0; i < keyWords.length; i++){
   
   //シートから読み取ったキーワード
   const keyWord      = keyWords[i];
   console.log(keyWord);
   
   //キーワードを中心に検索
   const apiUrl = 'https://api.twitter.com/2/tweets/search/recent?query=' + keyWord + '&max_results=20&tweet.fields=author_id,created_at,text,public_metrics,source,id';
   const responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
   console.log(apiUrl);
   //console.log(`HTTPデータ: ${responseApi.getContentText()}`);
   
   
   //HTTP リスポンスが200以外だったら、処理を終了
   if (responseApi.getResponseCode() !== 200) return '';
   const objects = JSON.parse(responseApi.getContentText());
   
   if (!objects) return;
   //console.log(objects);
   
   const tweets = objects.data;
   if(!tweets){continue}
   console.log(tweets);
   
   
   for(const tweet of tweets){
     
     //ツイートの投稿日時
     const date         = new Date(tweet.created_at);
     const formatedDate = Utilities.formatDate(date, 'Tokyo/Asia', 'yyyy/MM/dd E HH:mm');
     let tweetContents  = formatedDate.split(' ');
     
     //ツイート内容
     const text      = tweet.text;
     const source    = tweet.source;
     
     //ユーザーネームと、プロフィール画像を取得
     const authorId  = tweet.author_id;
     const userInfo  = findUserName_(authorId);
     const userName  = userInfo[0];
     const userImage = userInfo[1];
     
     //ツイートURL
     const tweetId   = tweet.id;
     const tweetUrl  = 'https://twitter.com/' + userName + '/status/' + tweetId;
     
     //リツイートやふぁぼ
     const detail    = tweet.public_metrics;
     const retweet   = detail.retweet_count;
     const favorite  = detail.like_count;
     
     console.log(userName, userImage, text, source, retweet, favorite);
     
     //投稿日時の1次元配列にユーザー名、投稿内容、デバイス、リツイート数、ふぁぼ数などを追加する
     tweetContents.push(userName, userImage, text, source, retweet, favorite, tweetUrl);
     
     //2次元配列を生成
     results.push(tweetContents);
     
   }//for tweets
 }//for_i
 //console.log(`配列の内容 ${results}`);

 console.log(results);
 return results
}//end



/* 
* author IDからユーザー名と、ユーザーのプロフィールアイコン画像を取得する
* プロフィールアイコンは、文字列を結合して、iMAGE関数として、セルに挿入する。
* キーワードからツイートを収集する場合に、ユーザー名が必要となるので、配列で返している
* 
* @param  {string} Twitterのauthor ID
* @return {object} ['ユーザー名','IMAGE関数の数式']
*
*/

function findUserName_(authorId){
 const apiUserInfo  = 'https://api.twitter.com/2/users?ids=' + authorId + '&user.fields=profile_image_url';
 const token        = getToken_();
 const apiOptions   = {
   headers : {
     'Authorization': `Bearer ${token}`
   },
   method : 'get',
   muteHttpExceptions : true
 };

 const responseApi = UrlFetchApp.fetch(apiUserInfo, apiOptions);
 console.log(responseApi.getContentText());

 const objects   = JSON.parse(responseApi.getContentText());
 const user      = objects.data[0];
 const userName  = user.username;
 const userImage = `=IMAGE("${user.profile_image_url}",1)`;
 const newValues = [userName, userImage];

 console.log(newValues);
 return newValues
}//end

特定ユーザーの最新ツイートを取得

このスクリプトを書き上げるのが、一番大変でした。

/* 
* try   特定ユーザーのTweetを1人ずつシートへ書き出す
* catch エラーが起きた段階で、Slackへ通知する
*
*/

function setUserTweetsToSheet() {
 try{
   //ユーザー1人ずつシートへ書き込む
   setTweets();
   
 }catch(error){
   //エラーが起きたらSlackへ通知
   sendErrorToSlack_(error);
   
 }//catch
}//end



/* 
* シートへtweet内容を書き込む
*
*/

function setTweets(){
 
 const token  = getToken_();
 const option = {
   headers : {
     'Authorization': `Bearer ${token}`
   },
   method : 'get',
   muteHttpExceptions : true
 };
 
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheetName   = 'Twitter';
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const userList    = getUserNameFromSheet_('userList'); //['ユーザー名','ユーザ名','ユーザ名',....]
 
 //ユーザーを、1人ずつ書き出すようにする。
 userList.map(userName => {
   
   const values  = getUserTweets_(userName, option);
   
   //Tweetが無かったら、処理を終了する。
   if(!values){return}

   //最終行へ書き込み
   const lastRow = sheet.getLastRow() + 1;
   sheet.getRange(lastRow, 1, values.length, values[0].length).setValues(values);
   console.log(`tweet数 : ${values.length}`);

   return
 });//map

 //重複を削除 シート名と列を指定する
 removeDuplicates_(sheetName, 6);
 
 //シートの水平・垂直を整える。無くても動作可
 setStyle_(sheetName);
 
 //投稿日時の新しいツイートが上に来るようにsortする。
 const range = sheet.getRange('A2:J');
 range.sort([
   {column: 1, ascending: false}
 ]);
}//end



/* 
* 対象のユーザーを取得する
* 
* @param  {string} シート名
* @return {object} 1次元配列
*
*/

function getUserNameFromSheet_(sheetName){
 const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadsheet.getSheetByName(sheetName);
 const lastRow     = sheet.getLastRow();
 
 //A2から最終行まで
 const userArray   = sheet.getRange(2, 1, lastRow -1, 1).getValues().flat();
 console.log(`1次元配列: ${userArray}`);

 return userArray
}



/* 
* 対象のユーザーのツイート取得する
* 
* @param  {string} ユーザー名
* @return {object} tokenなどの情報
*
*/

function getUserTweets_(userName, option){
 const apiUrl   = 'https://api.twitter.com/2/tweets/search/recent?query=' + 'from:' + userName + '&max_results=10&tweet.fields=author_id,created_at,text,public_metrics,source,id';

 const response = UrlFetchApp.fetch(apiUrl, option);
 const json     = JSON.parse(response.getContentText());
 const tweets   = json.data;
 
 //Tweetが何もなければリターンする。
 if(!tweets){return}

 console.log(`json :\n ${json}`);
 console.log(`tweets :\n ${tweets}`);

 //日付のフォーマットを整える
 const formatDate = (date, number, format) => {
   date.setDate(date.getDate() + number);
   return Utilities.formatDate(date, 'JST', format);
 }

 let results = [];

 for(const tweet of tweets){

   //投稿日時の1次元配列を生成する
   const date       = new Date(tweet.created_at);
   const stringDate = formatDate(date, 0, 'yyyy/MM/dd E HH:mm');
   let tweetDetail  = stringDate.split(' ');

   console.log(`tweetDetail : ${tweetDetail}`);

   //ツイート内容
   const text   = tweet.text;
   const source = tweet.source;
   
   //ユーザーネームと、プロフィール画像を取得
   //キーワード検索時にユーザー名を取得するために配列で返している。
   const authorId  = tweet.author_id;
   const userInfo  = findUserName_(authorId); //['userName', '=IMAGE関数']
   const userImage = userInfo[1];
   
   //ツイートURL
   const tweetId  = tweet.id;
   const tweetUrl = 'https://twitter.com/' + userName + '/status/' + tweetId;
   
   //リツイートやふぁぼ
   const detail   = tweet.public_metrics;
   const retweet  = detail.retweet_count;
   const favorite = detail.like_count;
   
   //console.log(userName, userImage, text, source, retweet, favorite);
   
   //投稿日時を1次元配列化し、その配列に、ユーザー名、アイコン、投稿内容、リツイート数等、情報を足していく。
   tweetDetail.push(userName, userImage, text, source, retweet, favorite, tweetUrl);

   //2次元配列を生成する。
   results.push(tweetDetail);
 }//for
 return results;
}//end



/* 
* もし、setTweets実行時にエラーが起こった際に、Slackへエラー内容を通知する
* 
* @param  {string} エラー内容
*
*/

function sendErrorToSlack_(error){
 //もし、エラーが起きたら、その時点で、Slackへ通知する。
 const string  = '<@Slack ID>' + '\nファンクション名:setUserTweetsToSheet を正常に終了する事ができませんでした。\n' + 'エラー内容は下記の通りです。\n'+ error;

 const payload = { "text" : string }
 const options = {
   "method" : "POST",
   "contentType" : 'application/json; charset=utf-8',
   "payload" : JSON.stringify(payload)
 }

 //Slackの#Twitterへ送信する。
 const webhook = 'https://hooks.slack.com/services/************';
 UrlFetchApp.fetch(webhook, options);

}

書き上げた当初は、全員分のユーザーのツイートを配列に入れてから、まとめてシートに書き出していたのですが、それだと途中でエラーが起こると最新の投稿内容が1つもシートに記載されません。なので、今回のリライトでは、ユーザー1人分のツイートを取得したら、シートに書き込んでいます。

そして、もしエラーが起きた際には、Slackへ通知されるようにしました。これによりエラーが起きる前の内容までは、シートに書き足されるようになりました。

画像6


この記事が気に入ったらサポートをしてみませんか?