Zennの隠しAPIから記事の一覧を取得して、X(Twitter)に定期自動投稿するGASを作成する方法🌟
こんにちは、AIQ株式会社のフロントエンドエンジニアのまさぴょんです!
今回は、ZennのAPIから記事の一覧を取得して、X(Twitter)に定期自動投稿するGASを作成する方法について、ご紹介していきます。
以前、X(Twitter)にTech Blog の記事をランダムに定期・自動投稿するScriptをGASで作る方法について、解説しました。
ただ、こちらは、私の個人 Tech Blog(WordPress)でのやり方なので、Zennの記事の場合は、どうするかについて解説していきます🌟
サービス構成 Overview (Zenn Developer's Guides)
記事の全体像(構成)のSummary
この記事の説明の全体像(構成)のサマリーをまとめさせていただくと、次のとおり。
Zennの記事一覧を取得できる隠しAPIが存在することを発見する🐱
Zennの記事一覧を取得する方法を調査していたら、RSSで情報を取得する方法以外にも、Zennの記事一覧を取得できる隠しAPIが存在することを発見しました👀✨
-
RSSを活用する方法
https://zenn.dev/ユーザー名/feed?all=1
- 上記のような形で、そのユーザーの記事情報をすべて取得することができる。
- データフォーマット・構造は、RSS(XML)です。
-
非公式のZenn APIを使用する方法
https://zenn.dev/api/articles?username=ユーザー名&order=latest
- 上記のような形で、RSS形式で、そのユーザーの記事情報を取得することができる。
- データのフォーマット・構造は、JSONです。
- 記事の数が多い場合は、page番号で、記事一覧を取得する。
https://zenn.dev/api/articles?username=manase&order=latest&page=1
- 上記のページネーションでの取得については、後述します。
調査スクラップ
Zenn APIの記事取得APIは、ページネーションを実装している🌟
先述の調査から、JSON形式で、自分のZennの記事を取得してみたところ、JSONで取得できたのは、記事数は48件でした。
https://zenn.dev/api/articles?username=manase&order=latest
2/4 時点で、私の記事は、57 件を公開中 なので、おそらくページネーションが実装されているAPIだと考えられます。
その証拠に、JSONに、"next_page": 2
というデータがあることを発見しました👀✨
Zenn APIの記事取得APIが返却してくれる JSONのデータ構造は、次のようなものです。
{
"articles": [
{
"id": 237226,
"post_type": "Article",
"title": "【無料】X(Twitter)にTech Blog の記事をランダムに定期・自動投稿するScriptをGASで作る!",
"slug": "0c48837f4c8849",
"comments_count": 0,
"liked_count": 0,
"body_letters_count": 9200,
"article_type": "tech",
"emoji": "🐦",
"is_suspending_private": false,
"published_at": "2024-01-21T15:18:39.222+09:00",
"body_updated_at": "2024-01-21T15:18:39.222+09:00",
"source_repo_updated_at": null,
"pinned": false,
"path": "/aiq_dev/articles/0c48837f4c8849",
"user": {
"id": 42864,
"username": "manase",
"name": "まさぴょん",
"avatar_small_url": "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c"
},
"publication": {
"id": 415,
"name": "aiq_dev",
"display_name": "AIQ Tech Blog (有志)",
"avatar_small_url": "https://res.cloudinary.com/zenn/image/fetch/s--zcjAmyH_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_50/https://storage.googleapis.com/zenn-user-upload/avatar/d936fa57c0.jpeg",
"pro": false,
"avatar_registered": true
}
}
],
"next_page": 2
}
page
を追加してみる🐱
QueryParameterに試しに、QueryParameterにpage
を追加してみたら、ページネーション設定ごとに、記事一覧を取得できることを確認しました🐱
-
https://zenn.dev/api/articles?username=manase&order=latest&page=1
-
"next_page":2
になっている。 - つまり、次のページが存在しており、ページ番号は2であるということ。
-
-
https://zenn.dev/api/articles?username=manase&order=latest&page=2
-
"next_page":null
になっている。 - つまり、次のページは存在しない。
-
GASで、Zennの特定のユーザーの記事一覧を取得してCSV作成・Driveに保存する
Zennの記事取得APIについて、仕様の一部を理解したので、すべての記事を取得できそうだということがわかりました。
それでは、GAS(Google Apps Script)で、Zennの特定のユーザーの記事一覧を取得してCSV作成して、Google Driveに保存する方法について解説していきます。
とりあえず、GAS上で、次のSrcCodeを使ってもらば、OKです🙆♂️
(USER_NAME
とFOLDER_ID
だけ、ご自身の状況に応じて変えてください)
// NOTE: 定数・定義
/** Zenn の API Base URL */
const ZENN_BASE_URL = "https://zenn.dev";
/** Zenn の記事一覧を取得する API Path */
const ZENN_API_ARTICLE_LIST_PATH = "/api/articles?username=";
/** 記事一覧を取得したい User の userName */
const USER_NAME = "manase";
/** 保存する CSV のファイル名 */
const FINE_NAME = "zenn_articles.csv";
// Google Drive の Folder ID
const FOLDER_ID = "";
/**
* NOTE: 該当のページ番号の Zenn の記事一覧を取得する関数
* @param {Number} pageNumber ページ番号
* @returns {Array} zennArticleList: Zenn の記事一覧
* @returns {Number | null} NextPage: 次のページ番号
*/
function getZennArticleList(pageNumber) {
console.log("Zenn の記事一覧を取得する・処理 Start");
/** Zennの記事一覧を取得するAPIのURL */
const userArticleListURL =
ZENN_BASE_URL +
ZENN_API_ARTICLE_LIST_PATH +
USER_NAME +
"&order=latest&page=" +
pageNumber;
console.log("Zenn API Call URL:", userArticleListURL);
try {
/** Zenn API からの レスポンス */
const response = UrlFetchApp.fetch(userArticleListURL);
// console.log("response: ", response);
/** Fetch Result */
const result = JSON.parse(response.getContentText());
// console.log("result: ", result);
/** Zenn の記事一覧リスト */
const zennArticleList = result.articles;
// console.log("Zenn の記事一覧:", zennArticleList);
/**
* 次のページ番号
* - Number or null
* - null の場合は、次のページがないことを意味する
*/
const NextPage = result.next_page;
// console.log("NextPage:", NextPage);
console.log("Zenn の記事一覧を取得する・処理 End");
return {
zennArticleList,
NextPage,
};
} catch (error) {
console.error("Zenn の記事一覧取得 Block でエラーが発生しました", error);
}
}
/**
* NOTE: Zenn のすべての記事一覧を取得する関数
* @returns {Array} Zenn のすべての記事一覧
*/
function getZennAllArticleList() {
console.log("Zenn のすべての記事一覧を取得する・処理 Start");
/**
* NOTE: Zenn の ページ番号
* - Zenn の記事一覧取得 API は PageNation を実装しているため Page番号で、順に一覧取得をする
* - 最初は、1ページ目から取得する
*/
let pageNumber = 1;
/**
* Fetch 制御 Flag
* - true: Fetch を続ける
* - false: Fetch を終了する
*/
let isFetch = true;
/** Zenn のすべての記事一覧リスト */
let zennAllArticleList = [];
try {
/** 最後のページになるまで、Fetchを実行する */
while (isFetch) {
console.log("ページ番号:", pageNumber);
/** 該当のページ番号の Zennの 記事一覧 を取得する */
const results = getZennArticleList(pageNumber);
console.log("Update前の記事の数", zennAllArticleList.length);
console.log("追加で取得した記事の数", results.zennArticleList.length);
/** 既存と取得したリストを Merge する */
const updatedList = [...zennAllArticleList, ...results.zennArticleList];
/** List を Update する */
zennAllArticleList = updatedList;
console.log("Update後の記事の数", zennAllArticleList.length);
/**
* 次のページ番号
* - Number or null
* - null の場合は、次のページがないことを意味する
*/
const NextPage = results.NextPage;
console.log("NextPage:", NextPage);
// まだ、つづきのページがあるかどうかを確認する
if (NextPage === null) {
console.log("最後のページです");
// 最後のページなので、Fetch を終了する
isFetch = false;
} else {
console.log("次のページがあります");
pageNumber = pageNumber + 1;
}
}
console.log("Zenn のすべての記事一覧を取得する・処理 End");
console.log("最終的な記事の総数", zennAllArticleList.length);
return zennAllArticleList;
} catch (error) {
console.error(
"Zenn のすべての記事一覧取得 Block でエラーが発生しました",
error
);
}
}
/**
* NOTE: Object の配列を受け取り CSV形式の文字列に変換する Func
* @param {Array} objArray - Object の配列
* @returns {String} csv - CSV形式の文字列
*/
function convertToCSV(objArray) {
const array = typeof objArray !== "object" ? JSON.parse(objArray) : objArray;
/** 1. Objectの Key を headerとして取り出す */
let str =
`${Object.keys(array[0])
.map((value) => `"${value}"`)
.join(",")}` + "\r\n";
// 2. 各オブジェクトの値をCSVの行として追加する
return array.reduce((str, next) => {
str +=
`${Object.values(next)
.map((value) => `"${value}"`)
.join(",")}` + "\r\n";
return str;
}, str);
}
/**
* NOTE: CSV形式の文字列を Blob Object に変換する
* @param {String} data - CSV形式の文字列
* @param {String} name - ファイル名
* @returns {Blob} blob - Blob Object
*/
function createBlob(csv, fileName) {
const contentType = "text/csv";
const charset = "utf-8";
/** GAS の記法で Blob Object を作成する Type. CSV */
const blob = Utilities.newBlob("", contentType, fileName).setDataFromString(
csv,
charset
);
return blob;
}
/**
* NOTE: Google Drive にファイルを書き込む
* @param {Blob} blob - Blob Object
* @param {String} folderId - Google Drive の Folder ID
* @returns {void}
*/
function writeDrive(blob, folderId) {
// Google Drive の Folder を取得する
const drive = DriveApp.getFolderById(folderId);
// Google Drive にファイルを書き込む
drive.createFile(blob);
}
/**
* NOTE: Main Function
*/
function main() {
// Zenn のすべての記事一覧を取得する
const zennAllArticleList = getZennAllArticleList();
// console.log("Zenn のすべての記事一覧:", zennAllArticleList);
if (zennAllArticleList.length > 0) {
console.log("Zenn に記事が投稿されています");
/** Zenn のすべての記事一覧の中から必要なデータだけを抽出した Zenn の記事一覧 */
const extractedZennArticleList = zennAllArticleList.map((element) => {
return {
title: element.title,
url: `${ZENN_BASE_URL}${element.path}`,
publishedAt: element.published_at,
likesCount: element.liked_count,
};
});
console.log(
"必要なデータだけを抽出した Zenn の記事一覧:",
extractedZennArticleList
);
/** CSV 形式の データ */
const csvData = convertToCSV(extractedZennArticleList);
// CSV ファイルを Blob Object に変換する
const blob = createBlob(csvData, FINE_NAME);
// Google Drive にファイルを書き込む
writeDrive(blob, FOLDER_ID);
} else {
console.log("Zenn に記事が投稿されていません");
}
}
CSVファイルをGoogleスプレッドシートにimportして、X投稿のためのMetaデータを作成する
上記のGAS Scriptを実行すると、次のようなCSVファイルが、Drive上に保存されるはずなので、
これをGoogle スプレッドシートに importして、Xに定期投稿するためのMetaデータを作成します。
ファイルタブからGoogle スプレッドシートに、CSVファイルの importを実施して、次のようなSheetを作成しておきました。
GASの定期実行プログラムの要件定義
Zennの記事一覧のMetaデータが準備できたので、次は、今回作成する定期実行プログラムの要件を定義します。
今回は、次の内容をプログラムで自動化することを要件定義とします。
- Zenn の記事を定期的に、Xに投稿したい。
- 投稿する記事は、ランダムに選びたい。
- 毎日、朝(午前7~8時)と昼(午後11時〜12時)と夕方(午後6~7時)の3回、定期的に投稿をしたい。
GASとX(Twitter)APIの設定をする💪🥺💪✨
Google Apps Script(GAS)を使って、X(Twitter)に自動投稿を行うには、GASとX(Twitter)それぞれの設定が必要です。
こちらの記事を参考に、X(Twitter)APIの登録と、Google Apps Scriptのプロジェクトを作成・OAuth2ライブラリの追加やOAuth2の認証を進めてください。
GoogleのOAuth2の認証
X(Twitter)のOAuth2認証
X(Twitter)にTech Blog の記事をランダムに定期・自動投稿するScript
X(Twitter)にTech Blog の記事をランダムに定期・自動投稿するScriptの内容は、次のとおりです。
こちらのGASのCodeは、こちらの『GAS×スプレッドシートでTwitterに定期自動投稿する方法』のSampleCodeをベースにハッシュタグに使用するカテゴリー情報などを追加しています。
(素晴らしい記事をありがとうございます🙌)
/** ツイッターのクライアント識別子 */
const CLIENT_ID = "";
/** ツイッターのClient Secret */
const CLIENT_SECRET = "";
/** スプレッドシートのID */
const SHEET_ID = "";
/** 編集権限のある Google Acount の G_Mail */
const G_MAIL = "";
// 初回だけ必要な OAuth2・認証処理
function main() {
const service = getService();
Logger.log("初回だけ必要な OAuth2・認証処理 Start");
if (service.hasAccess()) {
Logger.log("認証済みです");
} else {
Logger.log("認証ができていません");
const authorizationUrl = service.getAuthorizationUrl();
Logger.log("次の URL を開いて、認証をします。: %s", authorizationUrl);
}
}
// スプレッドシートからツイッターに投稿する記事をランダムに取得して、ツイート本文を作成する
function autoTweetFromSheet() {
Logger.log("ツイート本文を作成する・処理 Start");
let today = new Date();
let todayStr = Utilities.formatDate(today, "JST", "YYYY/MM/dd");
const mySpreadSheet = SpreadsheetApp.openById(SHEET_ID);
Logger.log(mySpreadSheet);
// 編集権限のある Google Acount の G_Mail を渡す
mySpreadSheet.addEditor(G_MAIL);
const sheetList = mySpreadSheet.getSheets();
Logger.log(sheetList);
// 1番目のスプレッドシートを取得する
const sheet = mySpreadSheet.getSheets()[0];
Logger.log(sheet);
const lastRow = sheet.getLastRow();
const targetRow = makeRundom(lastRow);
/** タイトル */
const title = sheet.getRange(targetRow, 1).getValue();
/** URL */
const link = sheet.getRange(targetRow, 2).getValue();
/** 投稿本文を作成する */
const msg =
`【Tech Blog おすすめ記事紹介 ${todayStr}】\n` +
`\n${title}\n` +
`\n記事はこちら🌟\n${link}\n` +
`\n#Web開発\n#Webエンジニア\n#エンジニアと繋がりたい\n#プログラミング`;
return msg;
}
// 乱数作成
function makeRundom(count) {
let random = Math.random();
random = Math.floor(random * count) + 1;
return random;
}
// X(Service) に X-API, OAuth2 を使って Access する
function getService() {
pkceChallengeVerifier();
const userProps = PropertiesService.getUserProperties();
const scriptProps = PropertiesService.getScriptProperties();
// X-Service作成
return OAuth2.createService("twitter")
.setAuthorizationBaseUrl("https://twitter.com/i/oauth2/authorize")
.setTokenUrl(
"https://api.twitter.com/2/oauth2/token?code_verifier=" +
userProps.getProperty("code_verifier")
)
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setCallbackFunction("authCallback")
.setPropertyStore(userProps)
.setScope("users.read tweet.read tweet.write offline.access")
.setParam("response_type", "code")
.setParam("code_challenge_method", "S256")
.setParam("code_challenge", userProps.getProperty("code_challenge"))
.setTokenHeaders({
Authorization:
"Basic " + Utilities.base64Encode(CLIENT_ID + ":" + CLIENT_SECRET),
"Content-Type": "application/x-www-form-urlencoded",
});
}
// 認証後のCallBack
function authCallback(request) {
const service = getService();
const authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput("Success!");
} else {
return HtmlService.createHtmlOutput("Denied.");
}
}
function pkceChallengeVerifier() {
var userProps = PropertiesService.getUserProperties();
if (!userProps.getProperty("code_verifier")) {
var verifier = "";
var possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
for (var i = 0; i < 128; i++) {
verifier += possible.charAt(Math.floor(Math.random() * possible.length));
}
var sha256Hash = Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256,
verifier
);
var challenge = Utilities.base64Encode(sha256Hash)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
userProps.setProperty("code_verifier", verifier);
userProps.setProperty("code_challenge", challenge);
}
}
function logRedirectUri() {
var service = getService();
Logger.log(service.getRedirectUri());
}
// ツイートを実行する
function sendTweet() {
const payload = {
text: autoTweetFromSheet(),
};
Logger.log("ツイートを実行する");
const service = getService();
if (service.hasAccess()) {
Logger.log("サービスアクセス完了 ツイート処理を続行する");
const url = `https://api.twitter.com/2/tweets`;
const response = UrlFetchApp.fetch(url, {
method: "POST",
contentType: "application/json",
headers: {
Authorization: "Bearer " + service.getAccessToken(),
},
muteHttpExceptions: true,
payload: JSON.stringify(payload),
});
const result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
const sheet = SpreadsheetApp.openById(SHEET_ID).getSheets()[0];
const lastRow = sheet.getLastRow();
let now = new Date();
sheet.getRange(lastRow + 1, 1).setValue(result["data"]["id"]);
sheet.getRange(lastRow + 1, 2).setValue(result["data"]["text"]);
sheet.getRange(lastRow + 1, 3).setValue(now);
} else {
Logger.log("認証ができていません");
var authorizationUrl = service.getAuthorizationUrl();
Logger.log("次の URL を開いて、認証をします。: %s", authorizationUrl);
}
}
GASで、トリガーの設定をする
最後に「毎日、朝, 昼, 夕の3回、定期的に投稿する」ためのGAS・トリガーの設定をしていきます。
まずは、GASのトリガーを選択します。
続いて、朝, 昼, 夕の3タイプの定期実行トリガーを作成していきます。
朝(午前7~8時)のトリガー設定
昼(午後11時〜12時)のトリガー設定
夕方(午後6~7時)のトリガー設定
これで、Blogの記事をランダムに定期・自動投稿する仕組みが完成しました🙌✨
まとめ
個人で、Blogもやっています、よかったら見てみてください。
注意事項
この記事は、AIQ 株式会社の社員による個人の見解であり、所属する組織の公式見解ではありません。
求む、冒険者!
AIQ株式会社では、一緒に働いてくれるエンジニアを絶賛、募集しております🐱🐹✨
詳しくは、Wantedly (https://www.wantedly.com/companies/aiqlab)を見てみてください。
参考・引用
AIQ 株式会社 に所属するエンジニアが技術情報をお届けします。 ※ AIQ 株式会社 社員による個人の見解であり、所属する組織の公式見解ではありません。 Wantedly: wantedly.com/companies/aiqlab
Discussion