X(旧Twitter)における複数のユーザー別投稿履歴をGASからAPI経由で取得してスプレッドシートに展開する(2024年5月時点)
以前投稿した記事「Xにおける特定のユーザーの投稿履歴をPythonからAPI経由で取得する(2024年1月時点)」の続編的なものです。
X developer アカウントで有料($100/月)のBasicサブスクリプションが有効であり、すでに自分でベアラートークンを発行してあることを前提に記載しています。
前回からの発展としてはPythonではなくGASでやる、という点です。
(GAS=Google Apps Script, Googleアカウントがあれば実行可能)
・Googleクラウド上で動くため、PC電源オフでも時間帯指定自動実行可能
・スプレッドシート展開によって加工や共有の手間を削減可能
・スプレッドシートに変数を置くため取得対象の管理が容易
以下の章立てで進めていきます。
1.Googleスプレッドシート準備
新しいスプレッドシートを開きます。
まずはスプレッドシートIDを確認し、どこかにメモしておきます。
次に左下の+ボタンで「シートを追加」。
シートに名前をつけます。
1枚目「config」
2枚目「list」
3枚目以降は取得対象のTwitterアカウント名で作成します。
configシート
configでは以下の情報を管理します。
B2セル:ベアラートークン
B3セル:取得対象アカウント数
※B3セルは[=COUNTA(list!B2:B)]という数式を入れとくと楽ができます。
listシート
listでは以下の情報を管理します。
A列:管理番号
B列:取得対象のTwitterアカウント名
C列:取得済みの最新投稿ID(最終行にある前提)
D列:次に投稿データを貼り付ける行
(アカウント名)シート
3枚目以降のシートではX APIで取得した情報が記載されます。
今回は最低限として以下を取得しています。
1. author id:アカウントを特定できる固有の管理番号です。
2. id:投稿を特定できる固有の管理番号です。
3. created_at:投稿日時(UTC:協定世界時)です。
※(以前の記事でGMT:グリニッジ標準時と勘違いしてました)
4. text:投稿本文です。
2.スクリプト記述
ツールバーの「拡張機能」から「Apps Script」を選択します。
「無題のプロジェクト」という画面が開くので、
右側の「function ~」等と書いてあるエリアにコードを入力していきます。
サンプルコード全文 ※2024年6月6日追記あり
全体としては、こんな感じのコードを書いてます。
function recentPostsByUser() {
const scriptFile = SpreadsheetApp.openById("このスプレッドシートのID");
const configSheet = scriptFile.getSheetByName("config");
const listSheet = scriptFile.getSheetByName("list");
const bearerToken = configSheet.getRange("B2").getValue();
const userCount = configSheet.getRange("B3").getValue();
const endPoint = "https://api.twitter.com/2/tweets/search/recent"
const authorizationCodes = {
method: 'get',
contentType: 'application/json',
muteHttpExceptions: true,
headers:{ Authorization: 'Bearer '+ bearerToken },
}
const maxResults ="&max_results=100"
let userId;
let sinceId;
let nextRow;
let userSheet;
let url;
let responseData;
let jsonData;
let data;
let arrayData;
let numRows;
let endRow;
let sortData;
for (let i = 2; i<=userCount+1;i++){
userId = listSheet.getRange(i,2).getValue();
sinceId = listSheet.getRange(i,3).getValue();
nextRow = listSheet.getRange(i,4).getValue();
userSheet = scriptFile.getSheetByName(userId);
try{
if(sinceId == "id"){
url = endPoint +"?query=from:" + userId + "&tweet.fields=author_id,id,created_at,text" + maxResults;
}else{
url = endPoint +"?query=from:" + userId + "&tweet.fields=author_id,id,created_at,text&since_id=" + sinceId + maxResults;
}
responseData = UrlFetchApp.fetch(url, authorizationCodes);
jsonData =JSON.parse(responseData.getContentText());
data = jsonData.data
arrayData = [];
for(const objects of data ){
let {author_id,id,created_at,text} = objects ;
arrayData.push([ author_id ,id ,created_at ,text ])
};
numRows = arrayData.length;
userSheet.insertRows(nextRow,numRows); //※2024年6月6日追記
userSheet.getRange(nextRow,1, numRows,4).setValues(arrayData);
endRow = listSheet.getRange(i,4).getValue();
sortData = userSheet.getRange(2,1,endRow,4);
sortData.sort({column:2,ascending: true});
}catch(e){
console.log('エラーを検知しました。');
console.log('エラー内容:'+e.message);
} //try catch
} //for i
}
名称の指定
function の直後に実行するスクリプトの名前を指定します。
デフォルトではmyFunctionになっています。
今回は「recentPostsByUser」という名前に変更しました。
この名称を指定することで、{ } に括られた範囲のコードが実行されます。
function recentPostsByUser() {
(ここに実行コードを記載する)
}
定数の宣言
const の直後に定数を指定します。
scriptFileという定数では、スプレッドシートIDを指定することでコードを実行するスプレッドシートを特定しています。(事故防止)
const scriptFile = SpreadsheetApp.openById("このスプレッドシートのID");
上記のスプレッドシートのIDという文字列を、先ほどメモした文字列に置き換えてください。今回の例だと、こんな感じです。
const scriptFile = SpreadsheetApp.openById("15lk8zO2GwZX_5rp2WqDW6HKFOJP0sp6sMcfXOw4VIhc");
configSheetという定数では、「config」を指定することでベアラートークンと対象アカウント数の記載されたシートを特定しています。
const configSheet = scriptFile.getSheetByName("config");
listSheetという定数では、「list」を指定することで対象アカウントIDや最新投稿IDなどが記載されたシートを特定しています。
const listSheet = scriptFile.getSheetByName("list");
bearerTokenという定数では、configシートの「B2」セルを指定することでベアラートークンの文字列を読み込んでいます。
const bearerToken = configSheet.getRange("B2").getValue();
userCountという定数では、configシートの「B3」セルを指定することでデータ取得対象のアカウント数を読み込んでいます。
const userCount = configSheet.getRange("B3").getValue();
endPointという定数では、XのAPIに接続するためのURLを指定しています。
※このURL以降に有料アカウント認証や検索コードなどをつらつらと埋め込んで長文のURLを作成し、そこにアクセスすることで欲しいデータを取得することになります。
const endPoint = "https://api.twitter.com/2/tweets/search/recent"
authorizationCodesという定数では、XのAPIから情報を取得するために必要な認証コードや要求事項が埋め込まれています。
method : 'get' でデータ取得を要求
contentType: 'aplicaton/json' でデータ形式にjsonを指定
muteHttpExceptions: true でエラー時も応答内容を取得
headers: では認証にベアラートークンを渡しています
const authorizationCodes = {
method: 'get',
contentType: 'application/json',
muteHttpExceptions: true,
headers:{ Authorization: 'Bearer '+ bearerToken },
}
maxResultsという定数では、X developer Basic($100/月)の限界である最大100件の投稿データを要求する文章を埋め込んでいます。
const maxResults ="&max_results=100"
変数の宣言
let の直後に変数を指定します。
繰り返し処理で使用する変数を片っ端から宣言してあります。
※説明の都合と、繰り返し処理前に宣言することで不測のエラーを回避。
userId : アカウント名です。繰り返し処理のたびに入れ替えていきます。
sinceId : すでに取得済みの投稿データのうち最新の投稿IDです。
nextRow : アカウントごとのシートで次に投稿データを貼り付ける行です。
userSheet : アカウントごとに用意した(3枚目以降の)シートです。
url : 必要な情報をあれこれ詰め込んだ長文のURLです。
responseData : Xからの応答です。投稿データかエラーメッセージです。
jsonData : responseDataを加工用に成型したものです。(※自信ない)
data : jsonDataをGAS用に変換したものです。(※自信ない)
arrayData : スプレッドシートに貼り付けるため加工したものです。
numRows : dataの総行数です。
endRow : 投稿データをスプレッドシートに貼り付けた後の最終行です。
sortData : スプレッドシートの並び順を揃えなおす範囲です。
let userId;
let sinceId;
let nextRow;
let userSheet;
let url;
let responseData;
let jsonData;
let data;
let arrayData;
let numRows;
let endRow;
let sortData;
繰り返し処理の変数取得
for の直後に繰り返し処理の条件を指定しています。
let i = 2 でlistの2行目から始めるという指定をします。
i<=userCount+1では繰り返し回数として対象アカウント数を指定します。
i++では1行ごとに処理するよう指定しています。
※処理を繰り返す中でiという変数が2から始まって1ずつ増えていき、対象アカウント数よりも多くなった時点で繰り返し処理を終了します。
for (let i = 2; i<=userCount+1;i++){
(ここに繰り返し実行するコードを記載する)
}
繰り返し処理の最初に以下の変数を入れ替えていきます。
user ID, SinceID, nextRow, UserSheet
userId = listSheet.getRange(i,2).getValue();
sinceId = listSheet.getRange(i,3).getValue();
nextRow = listSheet.getRange(i,4).getValue();
userSheet = scriptFile.getSheetByName(userId);
エラー検知時の振る舞い指定
try ~ catch 構文でエラー検知時の振る舞いを指定しています。
今回のコードではXのAPIに対して「(userID)が(sinceID)以降に投稿したデータを最大100件送ってください」という要求をしています。
対象のアカウントが(sinceId)以降なにも投稿していない場合、XのAPIは「その条件では送るデータがありません」というエラーで応答します。
初期状態ではエラーを検知するとスクリプトの処理が中断されるため、例えば対象10アカウントのうち3番目でエラーを検知すると4~10番目のアカウントの投稿データが取得できないという困った事態に陥ります。
「エラーを検知した場合は実行ログにエラー内容を残して次に進む」という構文を入れることで中断せずに処理を続行しています。
try{
(ここにエラーを検知する可能性があるコードを記載する)
}catch(e){
console.log('エラーを検知しました。');
console.log('エラー内容:'+e.message);
} //try catch
初めてデータを取得するアカウントかどうかで分岐
次に変数と文字列をつなげて長文のURLを生成します。
if (sinceId == "id") で、初めて投稿データを取得するアカウントなのか、すでに投稿データを取得済みのアカウントなのか、分岐を設けています。
初めて投稿データを取得するアカウントの場合
投稿IDの情報がないため、「list」シートのC列にはタイトル行の「id」が表示されているはずです。「sinceId」の変数が取得した値が「id」だった場合には、検索条件から「投稿IDが(sinceId)以降の」という指定を外しています。
すでにデータを取得済みのアカウントの場合
URLに &since_id=" + sinceId という文字列を入れることで、検索条件に「投稿IDが(sinceId)以降の」という指定を加えています。
if(sinceId == "id"){
url = endPoint +"?query=from:" + userId + "&tweet.fields=author_id,id,created_at,text" + maxResults;
}else{
url = endPoint +"?query=from:" + userId + "&tweet.fields=author_id,id,created_at,text&since_id=" + sinceId + maxResults;
XのAPIから投稿データを取得
やっとここでXのAPIに投稿データを要求し、応答をもらいます。
responseData = UrlFetchApp.fetch(url, authorizationCodes);
応答の成型と加工
XのAPIからもらった応答をスプレッドシートに貼り付けるように成型、加工していきます。
jsonData = Json.parse(responseData.getContentText())
data = jsonData.data
上記で応答データ内をGASで加工できるように変換します。
※私はこの箇所がまだ理解しきれていないです。難しい…。
arrayData = []; for(const objects of data ){ let {author_id,id,created_at,text} = objects ; arrayData.push([ author_id ,id ,created_at ,text ]) };
上記でスプレッドシートに貼り付けたいデータだけを取り出し、スプレッドシートの並び順に沿って列を整えます。
今回は加工データにarrayDataという名前をつけて、左からauthor_id, id, coreated_at, textの順で並べています。
jsonData =JSON.parse(responseData.getContentText());
data = jsonData.data
arrayData = [];
for(const objects of data ){
let {author_id,id,created_at,text} = objects ;
arrayData.push([ author_id ,id ,created_at ,text ])
};
スプレッドシートへの貼付
まず、arrayData.lengthで加工後のデータが何行あるかを取得して変数のnumRowsに入れておきます。
numRows = arrayData.length;
次に、スプレッドシートにおける範囲を正確に指定して、加工したデータを貼り付けます。
貼り付ける先はuserSheetで指定するシートです。
これは繰り返し処理の最初にアカウント名と同じシートを指定しています。
※2024年6月6日追記
自動取得を続けていると「スプレッドシートの行数が足りない」というエラーが発生したため、「取得したデータの行数と同じだけスプレッドシートに行追加する」という処理を追加しました。
insertRows(追加の起点となる行, 追加する行数) で指定しています。
userSheet.insertRows(nextRow,numRows); //※2024年6月6日追記
範囲はgetRange( 開始行, 開始列, 行数, 列数)で指定するのですが、
開始行 : nextRow(繰り返し処理開始時に取得)
開始列 : 1 (A列)
行数 : numRows (ついさっき取得)
列数 : 4 (A,B,C,Dの4列)
という構成になっています。
貼り付け自体はsetValues(貼り付けるデータ)で行います。
これは先ほど加工したarrayDataを指定しています。
userSheet.getRange(nextRow,1, numRows,4).setValues(arrayData);
スプレッドシートを投稿id順に並替
貼り付けたデータは「新しい投稿が上に」並んでいます。
しかし「最新投稿IDは最終行にある前提」でスプレッドシートを用意しました。そのため並び替え処理が必要になります。
並び替えの範囲を正確に指定しないといけないので、
endRow = listSheet.getRange(i,4).getValue();
上記の通り「新しく取得した投稿データを貼り付けた後」の最終行を取得しておきます。(正確にはnextRowと同じ数式を使いまわしているため最終行+1になっています)
sortData = userSheet.getRange(2,1,endRow,4);
絞り込み範囲を指定します。
最初の列:1(A列)
最初の行:2
行数:endRow(さっき取得した最終行)
列数:4(A,B,C,Dの4列)
sortData.sort({column:2,ascending: true});
sort({並び替えの基準に使う列, 昇順か否か})で並び替えを行います。
基準に使うのは投稿IDのB列なので「2」。
最も数字の大きい投稿IDを一番下に持ってきたいので昇順を指定します。
endRow = listSheet.getRange(i,4).getValue();
sortData = userSheet.getRange(2,1,endRow,4);
sortData.sort({column:2,ascending: true});
以降は繰り返し処理です。listシートにアカウントを10個指定してあれば10回繰り返し処理が行われます。
プロジェクトの保存
忘れないようにプロジェクトを保存しましょう。
保存せずに画面移動するとスクリプト編集内容が消えてしまいます。
承認とデバッグ
このままではまだスクリプトを自動実行することはできません。
何かを自動で(=勝手に)実行するというのは「ウィルス」と紙一重ですので、何度も確認を要求されます。
ここからは自己責任でお願いします。
手動で「デバッグ」ボタンを押します。
すると、承認を求められますので「権限を確認」します。
さらにどのアカウントで承認するかを聞かれますので、自分のアカウントを選択します。
さらに「このアプリはGoogleで確認されていません」(自作なので)という画面が出ます。わかりにくいですがここは「詳細」を押すと次に進めます。
「リスクを理解し、デベロッパー(自分)を信頼できる場合のみ、実行してください。」と警告されます。
「(安全ではないページ)に移動」を押すと先に進めます。
ようやく承認画面です。「許可」を押すと先に進めます。
これでデバッグが実行されます。
もしエラーが発生するようなら問題個所を探して修正する必要があります。
3.時間指定での自動実行
問題なくスクリプトが稼働するようなら、定期的に実行するよう設定をします。
トリガーの追加
AppsScript画面の左側で「トリガー」を押します。
まだ真っ白な画面なので、「トリガーを追加」ボタンを押します。
トリガー設定画面が表示されます。
イベントのソースを選択:時間主導型
定期的に実行し続けるのであれば「イベントのソースを選択」で時間指定型を選びます。
時間ベースのトリガーのタイプを選択
さらに「時間ベースのトリガーのタイプを選択」で時間ベースのタイマーを選び…。(対象アカウントの更新頻度によっては日付ベースのタイマーでもよいと思います)
時間の間隔を選択
「時間の間隔を選択(時間)」でn時間おきを選択します。
私の場合は1日に最大230件投稿する極端なアカウントを対象にしていたため、「APIから1回で取得できるのは最大100件」の制約内で漏れなく取得するために短時間で設定しています。
トリガーの設定を保存
以下のような感じでトリガーの設定ができたら、「保存」を押します。
トリガーを追加すると、以下の画面のように実行状況が表示されます。
以上です。あとはスプレッドシートにデータが溜まっていくので、それぞれの目的に応じて利用します。
1人2人でも「やってみようかな」と思う方がいれば嬉しいです。