
Discordでの情報収集環境を整える(arXiv編)
情報収集をうまくやりたい!
生成AIを学んでいると直面する問題。それな、とにかくとんでもなく情報が多い!そして流れるのがとんでもなく早い!という問題です。
生成AIは技術の進歩が著しく早く、それこそ一日単位で状況が変わることがざらにあります。
筆者はXで情報を収集していますがどうしても流れてしまい、キャッチアップが上手くできていないのが実情です。
そんな中、IizukaさんのポストでDiscordをプラットフォームとした情報収集術が公開(しかも無料!)されていましたので今回はこちらの中でarXivの最新論文の取得の実装をしてみます。
Iizukaさん、ありがとうございます!
今日自動化の仕組みたくさん作ったので、よかったら参考にしてみてください。
— Hiroya Iizuka (@0317_hiroya) April 25, 2024
全部無料で、Discordで情報を一元管理できるようになってます👇
Discordをプラットフォームとした情報収集の足掛かりとして、今回はDiscordに論文の翻訳・要約が自動的に投稿できるように進めていきます。
最終的には以下の画像のようにDiscordに投稿できるようになります。

arXivの論文翻訳&要約をDiscordに自動的に投稿できるようにしてみよう!
この仕組みの実装はGAS(GoogleAPPScript)とDiscordのWebhook機能によって実現します。
本投稿はIizukaさんが公開してくださっている記事をベースに作成しています。
Discordのwebhookを取得しよう!
まずはDiscordで投稿先のチャンネルのwebhookを取得(作成)します。
尚、本記事ではDiscordのアカウント登録や自サーバの作成、投稿先のチャンネルの作成については割愛します。
Discordのアプリの自サーバから「サーバの設定」から連携サービスを選択します。
「新しいウェブフック」を選択しウェブフックを作成します。
ウェブフックの名前や投稿先のチャンネルを選択して、「ウェブフックURLをコピー」を選択して作成したURLをコピーしてください。



GASでarXivの論文を取得しよう!
次にGASでコードを作成します。

GASには以下のコードを記述します。コード自体はIizukaさんのコードをそのまま使わせていただいており、コメントだけ追記しております。
// OpenAI の API keys (https://platform.openai.com/account/api-keys)
// スクリプトプロパティにAPIキーを保存するには、スクリプトエディタで「ファイル」>「プロジェクトのプロパティ」>「スクリプトプロパティ」を選択し、OPENAI_API_KEYとしてAPIキーを追加してください。
const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY"); // OpenAI APIキーをスクリプトプロパティから取得
const OPENAI_API_ENDPOINT = "https://api.openai.com/v1/chat/completions" // OpenAI APIのエンドポイントURL
const OPENAI_API_MODEL = "gpt-3.5-turbo-0125" // 使用するGPTモデル
const WEBHOOK_URL = "" // 上でコピーしたWebhook urlを貼り付けてください。 // Discord Webhook URL
// 検索クエリ
const QUERY = "検索したいキーワードを入力する"; // arXivで検索するクエリ
// 検索対象日数
const TERM = 1; // 検索対象の日数
// 検索時のヒット論文で要約する論文の本数の上限
const MAX_PAPER_COUNT = 3; // 要約する論文の最大数
// ChatGPT に渡す命令
const PROMPT_PREFIX = "あなたは情報教育、テクノロジーに詳しい教師です。\n以下の論文を、タイトルと要約の2点をそれぞれ改行で分けて、専門用語を使わず、簡素で平易な日本語で説明してください。\n要点は箇条書きでお願いします。\n## 出力形式 \nタイトル: {タイトル}\n要約: {要約}"; // ChatGPTに渡すプロンプト
function getDateBeforeDays(days) {
const date = new Date(); // 現在の日付を取得
date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); // タイムゾーンオフセットを調整
date.setDate(date.getDate() - days); // 指定された日数前の日付に設定
return date.toISOString().split("T")[0]; // YYYY-MM-DD形式の日付文字列を返す
}
function getArxivPapers(query, fromDate, toDate) {
const options = {
muteHttpExceptions: true, // HTTPエラーを無視するオプション
};
const url = `http://export.arxiv.org/api/query?search_query=${query}&start=0&max_results=20&sortBy=submittedDate&sortOrder=descending` // arXiv APIのURL
const xmlText = UrlFetchApp.fetch(url, options).getContentText(); // arXiv APIからXMLを取得
const document = XmlService.parse(xmlText); // XMLをパース
const root = document.getRootElement(); // ルート要素を取得
const entries = root.getChildren("entry", XmlService.getNamespace("http://www.w3.org/2005/Atom")); // entry要素を取得
const papers = entries
.map((entry) => {
const title = entry.getChild("title", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText(); // タイトルを取得
const abstract = entry.getChild("summary", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText(); // 要約を取得
const id = entry.getChild("id", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText(); // IDを取得
const published = entry.getChild("published", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText();// 公開日を取得
return {
title: title,
abstract: abstract,
url: id,
published: published,
};
}).filter((paper) => {
const publishedDate = new Date(paper.published); // 公開日をDate型に変換
const from = new Date(fromDate); // 検索開始日をDate型に変換
const to = new Date(toDate); // 検索終了日をDate型に変換
to.setDate(to.getDate() + 1); // 検索終了日の翌日に設定
return publishedDate >= from && publishedDate < to; // 公開日が検索期間内かどうかを判定
});
console.log(`フィルタリング後の論文数: ${papers.length}`); // フィルタリング後の論文数をログ出力
return papers; // 論文情報の配列を返す
}
function callChatGPT(input) {
const messages = [
{
role: "user",
content: PROMPT_PREFIX + "\n" + input, // ユーザーの入力とプロンプトを結合
},
];
const options = {
"method": "post", // POSTリクエスト
"headers": {
"Authorization": `Bearer ${OPENAI_API_KEY}`, // APIキーを認証ヘッダーに設定
"Content-Type": "application/json", // リクエストボディのContent-Typeを設定
},
"payload": JSON.stringify({
model: OPENAI_API_MODEL, // 使用するGPTモデルを指定
messages, // メッセージを指定
}),
};
return JSON.parse(UrlFetchApp.fetch(OPENAI_API_ENDPOINT, options).getContentText()); // OpenAI APIにリクエストを送信し、レスポンスをJSONとしてパース
}
function main() {
if (!OPENAI_API_KEY) {
console.log("ERROR: OPEN_API_KEY を指定してください"); // APIキーが設定されていない場合はエラーメッセージをログ出力
return;
}
const fromDate = getDateBeforeDays(TERM); // 検索開始日を取得
const toDate = getDateBeforeDays(0); // 検索終了日を取得
console.log(`検索範囲の開始日: ${fromDate}`); // 検索開始日をログ出力
console.log(`検索範囲の終了日: ${toDate}`); // 検索終了日をログ出力
const arxivPapers = getArxivPapers(QUERY, fromDate, toDate); // arXivから論文情報を取得
console.log(`取得された論文数: ${arxivPapers.length}`); // 取得された論文数をログ出力
const papers = arxivPapers; // 論文情報を変数に代入
let output = "新着論文のお知らせ\n\n"; // 出力メッセージの先頭部分
let paperCount = 0; // 処理済み論文数のカウンター
if (papers.length === 0) {
output += "指定された期間内に、検索クエリに一致する新しい論文は見つかりませんでした。\n\n"; // 該当する論文がない場合のメッセージ
} else {
for (const paper of papers) {
if (++paperCount > MAX_PAPER_COUNT) break; // 処理済み論文数が上限に達した場合はループを抜ける
console.log(`論文${paperCount}: ${paper.title}`); // 処理中の論文のタイトルをログ出力
const title = paper.title; // 論文のタイトル
const abstract = paper.abstract; // 論文の要約
const url = paper.url; // 論文のURL
const input = "\n" + "title: " + title + "\n" + "abstract: " + abstract; // ChatGPTへの入力
const res = callChatGPT(input); // ChatGPTにリクエストを送信
console.log(`ChatGPTからのレスポンス: ${JSON.stringify(res, null, 2)}`); // ChatGPTからのレスポンスをログ出力
const paragraphs = res.choices.map((c) => c.message.content.trim()); // レスポンスから段落を抽出
output += `${paragraphs.join("\n")}\n\n${url}\n\n\n`; // 出力メッセージに段落とURLを追加
}
}
output = output.trim(); // 出力メッセージの前後の空白を削除
console.log(`最終結果: ${output}`); // 最終的な出力メッセージをログ出力
const payload =
{
"content": output // Discordに送信するメッセージ
};
const options =
{
"method": "post", // POSTリクエスト
"payload": payload // リクエストボディ
};
UrlFetchApp.fetch(WEBHOOK_URL, options); // DiscordのWebhook URLにリクエストを送信
}
OpenAIのAPIキーが必要になりますので、キーを取得していない方はhttps://platform.openai.com/account/api-keys より取得しましょう。
OpenAIのAPIキーは左の歯車アイコンの「プロジェクトの設定」にてOPENAI_API_KEYをプロパティとして作成して取得したキーを設定してください。

APIキーの設定までできたらテスト実行をしてみましょう。
main関数を指定して実行してみましょう。

上手く動作すると、こんな感じに正常終了しDiscordに投稿されます。
今回は期間内に指定したキーワードの論文が見つからなかったので、その旨の通知が表示されています。


スクリプト実行時に権限を求められますが、「このアプリは確認されていません」とびっくりするメッセージが表示されることがあります。その際は以下のサイトに書いてある通り、「詳細」→「メッセージダイアログ(安全ではないページ)に移動」と進んでいくと実行できます。
定期実行の設定しよう!
自動的に実行してもらうためにタイマーを左側の時計アイコンの「トリガー」で設定します。
今回は毎日7~8時に実行して通知してもらうように設定し、保存します。

デプロイしよう!
ここまで来たら準備は完了ですので、あとはデプロイするだけです。「デプロイ」→「新しいデプロイ」を選択し説明文を追加してデプロイしたら完了です。


定期実行の確認しよう!
最後に、設定した時間にDicordに投稿されたら成功です!

いかがでしたでしょうか?
筆者はGASを使うのは初めてでしたが簡単に実装することができました。
次はTwitterやNoteからの投稿情報をDiscordに投稿する仕組みを作成したいと思います。
Xでフォローしている人の投稿を、Discordへ飛ばす。https://t.co/0HprsNyKMx
— Hiroya Iizuka (@0317_hiroya) April 25, 2024
noteでフォローしている人の記事の更新を、Discordへ飛ばす。https://t.co/b5VBv1RCnG
— Hiroya Iizuka (@0317_hiroya) April 25, 2024
ここまでお読みいただき、ありがとうございました!