研究メモブログの目次

【ChatGPT】PubMed から論文を自動取得&日本語要約してメールやLINE で通知してみた

こんにちは~、ガイです。

先週ついにChatGPT に課金しました!月3000 円の月謝は(安くない♪)ので、いろいろ使っていきたいと思っています。

前回記事が長くなりすぎたので分割しましたが、今回は前回の続きのような内容です。

今度はbot ではなくメールとLINE で論文情報を通知してみました。

メール通知の例

LINE 通知の例

個人的にはLINE の小さい画面で長文が来るとウッとなるのでメールがおすすめです。

準備するもの

OpenAI のAPI keys の取得

OpenAI のAPI keys の取得方法は前編と全く同じですので、前編を見た人は飛ばしてください。

API(Application Programming Interface)はソフトウェアやプログラム、Webサービスの間をつなぐインターフェースのことで、API key とはAPI を使用するための認証情報です。

https://platform.openai.com/api-keys のページにアクセスし、+Create new secret key をクリックしてキーを発行します。名前は何でもいいです。大事なのはその次の「Save your key」ページに表示される文字列です。コピーして忘れない場所に保存しておいてください。文字列の保存後、Done でキーの完成です。

https://platform.openai.com/api-keys の画面

Save your key 画面

ここまでは以下のページを参考にしました。ありがとうございます。

ChatGPTによって指定した分野のPubMed論文を要約して毎日メールかLINEしてもらう方法 | 歯と口腔外科の役立つお話

LINE Notifyのトークン発行

以下のページを参考にしました。

LINE Notify アクセストークン - firestorage Biz

LINE Notify を使うための認証情報であるアクセストークンを取得します。

LINE Notify はさまざまなWeb サービスからの通知を、LINEで受信することが出来るサービスです。

LINE Notify にアクセスし、LINEアカウントに登録しているメールアドレスとパスワードか、QR コードでログインします。ログイン後、右上に表示されているアカウント名をクリックし「マイページ」をクリックします。

LINE Notify ログイン後の画面

マイページに入ったら「トークンを発行する」をクリックします。

通知名に表示されても問題ないトークン名を設定し、論文情報の送り先となるトークルームを選択します。

トークン名とLINE 通知送り先を決定する画面


トークンを発行すると文字列が表示されるので、忘れない場所に保存します。

トークン情報

これでLINE Notify のアクセストークンの発行は終了です。

GAS のスクリプト プロパティにメールアドレス・INE Notify のアクセストークン・OpenAI API key を書き込む

スクリプト プロパティを使うとソースコード内に直接値を書かずに秘密情報(例:APIキー、パスワード)を利用できます。参考にしたサイトではベタ貼りされているものがありましたが、怖いので隠します。

GAS ページの左側のタブの一番下の歯車マークをクリックすると、プロジェクトの設定ページが開きます。

プロジェクトの設定ページ

プロジェクトの設定ページの一番下までスクロールするとスクリプト プロパティがあります。

スクリプト プロパティを追加」をクリックします。

スクリプト プロパティの設定画面
  • EMAIL_RECIPIENT にメールアドレス
  • LINE_TOKEN にLINE Notify アクセストーク
  • OPENAI_API_KEY にOpenAI のAPI key

を入力し、 「スクリプト プロパティを保存」で保存します。

プロパティ名(EMAIL_RECIPIENT, LINE_TOKEN, OPENAI_API_KEY)をこのブログと違うものに変えてしまうとコピペコードで実行できなくなるので、注意してください。

これで準備完了です。

GAS スクリプトの準備

コードのコピペ&ペースト

GAS ページの左側にあるタブの中にある<>アイコンをクリックするとエディタが開くので、そこに以下のコピペ用コードLINE のみEmail のみ両方に送るそれぞれあります)をまるっと貼り、赤太字の部分を自分好みにカスタマイズして使います。どこで何をしているかはコード内にコメントしています。

このコードはPUBMED_QUERY に入れた検索タームで過去二週間分のPubMed 論文を検索し、条件に合う論文のタイトルと要約を日本語で書きだします。

また、重複した投稿がなされないよう、POSTED_PAPER_IDS に過去に取得済みの論文id を自動で保存し、同じ内容が投稿されないようチェックしています。取得id は直近二週間分を残して消える仕組みにし、溜まりすぎないようにしました。

PUBMED_QUERY にいれるMesh term はhttps://www.ncbi.nlm.nih.gov/mesh/ から検索できます。検索タームが長すぎるとエラーが出たり、検索条件から外れた論文もなぜか引っ張ってきてしまうようになるので、シンプルにするのがおすすめです。

ちゃんとBluesky アカウントに投稿されるかどうかはエディタの左上の「▷実行」を押して確認できます。投稿ができるのを確認したら次の自動実行の設定(トリガーの設定)に移ります。

コピペ用コード(LINE のみ送る)

// ChatGPT に渡す命令
const PROMPT_PREFIX = "あなたは神経系と動物行動に詳しい研究者です。与えられた論文の要点を3点のみでまとめ、以下のフォーマットで日本語で出力してください。改行してください。\
【タイトルの日本語訳】 \
・要点1 \
・要点2 \
・要点3";

// PubMed の検索クエリ
const PUBMED_QUERY = '"Behavior, Animal"[MeSH] NOT "Humans"[Mesh]';
// PubMed の対象の記事タイプ
const PUBMED_PUBTYPES = ["Journal Article", "Books and Documents", "Clinical Trial", "Meta-Analysis", "Randomized Controlled Trial", "Review", "Systematic Review"];
// PubMed の検索対象日数
const PUBMED_TERM = 14;
// PubMed の検索時のヒット論文で要約する論文の本数の上限
const MAX_PAPER_COUNT = 1;

// 重複投稿を防ぐ!
// 投稿済み論文IDを取得し、一定期間後に古いデータを削除することで重複投稿を防ぐ
// 投稿済みの論文IDとその投稿日を取得する関数
function getPostedPaperIds() {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    const twoWeeksAgo = new Date();
    twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); // 二週間前の日付を計算

    // 二週間以内のものだけを残します(古いログの削除)
    for (const [id, date] of Object.entries(postedIdsWithDates)) {
        if (new Date(date) < twoWeeksAgo) {
            delete postedIdsWithDates[id]; // 二週間より前のデータは削除
        }
    }

    // 更新したデータをスクリプトのプロパティに保存
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));

    // 更新後のIDのリストを返す
    return Object.keys(postedIdsWithDates);
}

// 論文IDを投稿日とともに保存する関数
function savePostedPaperId(id) {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    
    // 現在の日時をISO形式で保存
    postedIdsWithDates[id] = new Date().toISOString();
    
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));
}

// Google Apps Script エディタでの論文ID ログ出力
function logPostedPaperIds() {
    const postedIds = getPostedPaperIds();
}

// スクリプトのメイン関数
// ここでAPIキーを取得し、PubMedから論文情報を取得してChatGPTに要約させ、LINE に送信
function main() {
    const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");
    const LINE_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_TOKEN");
    if (!OPENAI_API_KEY || !LINE_TOKEN ) { console.error("ERROR: OPENAI_API_KEY or LINE_TOKEN is not set. Please set them in the Script Properties.");
    return;
}
    // スクリプトのメイン関数内で過去14日間の論文IDを取得する部分
    // getPaperIDsOn関数を正しく呼び出す
    const today = new Date(); // 今日の日付
    const fourteenDaysAgo = new Date();
    fourteenDaysAgo.setDate(today.getDate() - 14); // 14日前の日付

    // ここでエラーが発生していた場合、startDate と endDate が正しく Date オブジェクトとして扱われているかを確認
    console.log(`StartDate: ${toYYYYMMDD(fourteenDaysAgo)}, EndDate: ${toYYYYMMDD(today)}`);

    // 更新された関数を使用して、過去14日間の論文IDを取得
    const ids = getPaperIDsOn(fourteenDaysAgo, today);
 

    let output = "【PubMed 新着】";
    let paperCount = 0;
    const postedIds = getPostedPaperIds(); // 既に投稿された論文IDのリストを取得

    for (let i = 0; i < ids.length; i++) {
        const id = ids[i];
        
        if (postedIds.includes(id)) {
            console.log(`ID ${id}は既に投稿されています。スキップします。`);
            continue; // このIDの処理をスキップ
        }
        
        Utilities.sleep(1000); // APIリクエスト間の遅延を導入

        try {
            const summary = getPaperSummaryByID(id);
            if (!summary || !summary.title) {
                console.error(`ID ${id}のsummaryが正しく取得できていないか、タイトルがありません。`);
                continue; // タイトルがなければこのIDの処理をスキップ
            }
            
            if (++paperCount > MAX_PAPER_COUNT) break; // 最大処理数に達したらループを終了

            const abstract = getPaperAbstractByID(id);
            const input = `\ntitle: ${summary.title}\nabstract: ${abstract}`;
            const res = callChatGPT(input, OPENAI_API_KEY);

            const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
            const paragraphs = res.choices.map*1;
            output += `${paragraphs.join("\n")}\n\n${paperUrl}\n\n\n`;
            // リンクカードに挿入する【雑誌の略称】とアブストラクトを定義
            const descriptionWithJournal = `【${summary.source}】${abstract.substring(0, 197) + '...'}`; // 200文字制限を適用
            sendLineNotify(paragraphs.join("\n"), LINE_TOKEN, paperUrl, summary.title, descriptionWithJournal);
            // IDを投稿済みリストに追加
            savePostedPaperId(id);
        } catch (e) {
            console.error(`An error occurred while processing ID ${id}: ${e.message}`);
        }
    }

    if (paperCount === 0) {
        output += "検索条件に合致した新着論文はありませんでした。";
    }
    output = output.trim();
    console.log(output);
    // IDリストをログに出力する
    logPostedPaperIds();

// 現在の日付で新たにPubMedから論文IDを取得
const newIds = getPaperIDsOn(today, today);

// 取得した各IDに対して処理を実行
newIds.forEach(id => {
    // 論文の要約と抄録を取得
    const summary = getPaperSummaryByID(id);
    const abstract = getPaperAbstractByID(id);
    // コンソールに論文の要約を出力
    console.log(`ID ${id}のsummary:`, summary);
    // 要約と抄録が取得できた場合のみ処理を続行
    if (summary && abstract) {
        // PubMed論文のURLを構築
        const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
        // LINE に送信    
        sendLineNotify(paragraphs.join("\n"), LINE_TOKEN, paperUrl, summary.title, descriptionWithJournal);
    }
});
}

// 論文の要約とリンクをLINE に送信する関数
function sendLineNotify(content, token, paperUrl, title, description) {
  const messageContent = `${title}\n${description}\nURL: ${paperUrl}\n${content}`;
  const lineNotifyApi = 'https://notify-api.line.me/api/notify';
  const options = {
    "method": "post",
    "headers": {"Authorization": "Bearer " + token},
    "payload": "message=" + encodeURIComponent(messageContent),
    "muteHttpExceptions": true
  };
  UrlFetchApp.fetch(lineNotifyApi, options);
}

// PubMedから論文のIDを取得する関数や、取得したIDに基づいて論文の要約や詳細情報を取得する関数など、スクリプト全体で共通して使用されるヘルパー関数群

// 日付をYYYY/MM/DD形式に変換する関数
function toYYYYMMDD(date) {
    console.log(`toYYYYMMDD - Received date: ${date}, Type: ${typeof date}`);
    if (!(date instanceof Date)) {
        console.error("toYYYYMMDD - Error: Not a Date object");
        return null;
    }        
    // 月と日が1桁の場合、先頭に0を追加する
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    return `${date.getFullYear()}/${month}/${day}`;
}

// 指定されたPubMedの論文IDに基づき、論文の要約情報を取得する関数
function getPaperSummaryByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=${id}`;
    console.log(url);
    const res = JSON.parse(UrlFetchApp.fetch(url).getContentText());
    return res.result[id];
}

// 指定されたPubMedの論文IDに基づき、論文の抄録を取得する関数
function getPaperAbstractByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id=${id}`;
    const xml = UrlFetchApp.fetch(url).getContentText();
    const match = xml.match(/<Abstract>(.*?)<\/Abstract>/);
    return match ? match[1] : "";
}

// 指定された論文のタイプが設定した条件に合致するかをチェックする関数
function checkPubtype(pubtypes) {
    const common = pubtypes.filter(x => PUBMED_PUBTYPES.indexOf(x) !== -1);
    return common.length > 0;
}

// 指定された日付に基づき、PubMedから論文のIDを取得する関数
// 過去二週間の論文IDを取得する関数(更新版)
// getPaperIDsOn関数内でのendDateの扱いを確認
function getPaperIDsOn(startDate, endDate) {
    console.log(`getPaperIDsOn - StartDate: ${startDate}, EndDate: ${endDate}`); // デバッグログ
    console.log(`getPaperIDsOn - StartDate type: ${typeof startDate}, EndDate type: ${typeof endDate}`); // データタイプ確認
    const query = encodeURIComponent(PUBMED_QUERY);
    
    // ここでstartDateとendDateをtoYYYYMMDDに渡す
    const startDateFormatted = toYYYYMMDD(startDate);
    const endDateFormatted = toYYYYMMDD(endDate);
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&sort=pub_date&term=${query}&mindate=${startDateFormatted}&maxdate=${endDateFormatted}`;
    console.log(url);
    try {
        const response = UrlFetchApp.fetch(url);
        const data = JSON.parse(response.getContentText());
        const ids = data.esearchresult.idlist;
        return ids;
    } catch (e) {
        console.error("Failed to fetch paper IDs: " + e.message);
        return []; // エラーが発生した場合は空のリストを返します
    }
}


// OpenAIのChatGPT APIを呼び出し、指定された入力に基づいて応答を取得する関数
function callChatGPT(input, apiKey) {
    const url = "https://api.openai.com/v1/chat/completions";
    const options = {
        method: "post",
        headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json",
        },
        payload: JSON.stringify({
            model: "gpt-3.5-turbo", // ここを利用可能なモデルに変更
            messages: [{ role: "user", content: PROMPT_PREFIX + "\n" + input }],
        }),
    };
    
    try {
        const response = UrlFetchApp.fetch(url, options);
        if (response.getResponseCode() === 200) {
            return JSON.parse(response.getContentText());
        } else {
            console.error(`Failed to call OpenAI API: ${response.getContentText()}`);
        }
    } catch (e) {
        console.error(`An error occurred: ${e.message}`);
    }
}

コピペ用コード(Email のみ送る)

// ChatGPT に渡す命令
const PROMPT_PREFIX = "あなたは神経系と動物行動に詳しい研究者です。与えられた論文の要点を3点のみでまとめ、以下のフォーマットで日本語で出力してください。改行してください。\
【タイトルの日本語訳】 \
・要点1 \
・要点2 \
・要点3";

// PubMed の検索クエリ
const PUBMED_QUERY = '"Behavior, Animal"[MeSH] NOT "Humans"[Mesh]';
// PubMed の対象の記事タイプ
const PUBMED_PUBTYPES = ["Journal Article", "Books and Documents", "Clinical Trial", "Meta-Analysis", "Randomized Controlled Trial", "Review", "Systematic Review"];
// PubMed の検索対象日数
const PUBMED_TERM = 14;
// PubMed の検索時のヒット論文で要約する論文の本数の上限
const MAX_PAPER_COUNT = 1;

// 重複投稿を防ぐ!
// 投稿済み論文IDを取得し、一定期間後に古いデータを削除することで重複投稿を防ぐ
// 投稿済みの論文IDとその投稿日を取得する関数
function getPostedPaperIds() {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    const twoWeeksAgo = new Date();
    twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); // 二週間前の日付を計算

    // 二週間以内のものだけを残します(古いログの削除)
    for (const [id, date] of Object.entries(postedIdsWithDates)) {
        if (new Date(date) < twoWeeksAgo) {
            delete postedIdsWithDates[id]; // 二週間より前のデータは削除
        }
    }

    // 更新したデータをスクリプトのプロパティに保存
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));

    // 更新後のIDのリストを返す
    return Object.keys(postedIdsWithDates);
}

// 論文IDを投稿日とともに保存する関数
function savePostedPaperId(id) {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    
    // 現在の日時をISO形式で保存
    postedIdsWithDates[id] = new Date().toISOString();
    
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));
}

// Google Apps Script エディタでの論文ID ログ出力
function logPostedPaperIds() {
    const postedIds = getPostedPaperIds();
}

// スクリプトのメイン関数
// ここでAPIキーを取得し、PubMedから論文情報を取得してChatGPTに要約させ、Email に送信
function main() {
    const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");
    const EMAIL_RECIPIENT = PropertiesService.getScriptProperties().getProperty("EMAIL_RECIPIENT");
    const EMAIL_SUBJECT = "PubMed 新着論文の要約";
    if (!OPENAI_API_KEY || !EMAIL_RECIPIENT) {
        console.error("ERROR: OPENAI_API_KEY or EMAIL_RECIPIENT is not set. Please set them in the Script Properties.");
        return;
    }
    // スクリプトのメイン関数内で過去14日間の論文IDを取得する部分
    // getPaperIDsOn関数を正しく呼び出す
    const today = new Date(); // 今日の日付
    const fourteenDaysAgo = new Date();
    fourteenDaysAgo.setDate(today.getDate() - 14); // 14日前の日付

    // ここでエラーが発生していた場合、startDate と endDate が正しく Date オブジェクトとして扱われているかを確認
    console.log(`StartDate: ${toYYYYMMDD(fourteenDaysAgo)}, EndDate: ${toYYYYMMDD(today)}`);

    // 更新された関数を使用して、過去14日間の論文IDを取得
    const ids = getPaperIDsOn(fourteenDaysAgo, today);
 

    let output = "【PubMed 新着】";
    let paperCount = 0;
    const postedIds = getPostedPaperIds(); // 既に投稿された論文IDのリストを取得

    for (let i = 0; i < ids.length; i++) {
        const id = ids[i];
        
        if (postedIds.includes(id)) {
            console.log(`ID ${id}は既に投稿されています。スキップします。`);
            continue; // このIDの処理をスキップ
        }
        
        Utilities.sleep(1000); // APIリクエスト間の遅延を導入

        try {
            const summary = getPaperSummaryByID(id);
            if (!summary || !summary.title) {
                console.error(`ID ${id}のsummaryが正しく取得できていないか、タイトルがありません。`);
                continue; // タイトルがなければこのIDの処理をスキップ
            }
            
            if (++paperCount > MAX_PAPER_COUNT) break; // 最大処理数に達したらループを終了

            const abstract = getPaperAbstractByID(id);
            const input = `\ntitle: ${summary.title}\nabstract: ${abstract}`;
            const res = callChatGPT(input, OPENAI_API_KEY);

            const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
            const paragraphs = res.choices.map*2;
            output += `${paragraphs.join("\n")}\n\n${paperUrl}\n\n\n`;
            // リンクカードに挿入する【雑誌の略称】とアブストラクトを定義
            const descriptionWithJournal = `【${summary.source}】${abstract.substring(0, 197) + '...'}`; // 200文字制限を適用
            // メールの本文を組み立てる
            const emailBody = `タイトル: ${summary.title}
                              \n要約:\n${paragraphs.join("\n")}
                              \nURL: ${paperUrl}
                              \n${descriptionWithJournal}`; 
            // sendEmail関数を呼び出してメールを送信
            sendEmail(EMAIL_RECIPIENT, EMAIL_SUBJECT, emailBody);
            
            // IDを投稿済みリストに追加
            savePostedPaperId(id);
        } catch (e) {
            console.error(`An error occurred while processing ID ${id}: ${e.message}`);
        }
    }

    if (paperCount === 0) {
        output += "検索条件に合致した新着論文はありませんでした。";
    }
    output = output.trim();
    console.log(output);
    // IDリストをログに出力する
    logPostedPaperIds();

// 現在の日付で新たにPubMedから論文IDを取得
const newIds = getPaperIDsOn(today, today);

// 取得した各IDに対して処理を実行
newIds.forEach(id => {
    // 論文の要約と抄録を取得
    const summary = getPaperSummaryByID(id);
    const abstract = getPaperAbstractByID(id);
    // コンソールに論文の要約を出力
    console.log(`ID ${id}のsummary:`, summary);
    // 要約と抄録が取得できた場合のみ処理を続行
    if (summary && abstract) {
        // PubMed論文のURLを構築
        const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
    }
});
}

// 論文の要約とリンクをメールに送信する関数
function sendEmail(recipient, subject, body) {
  GmailApp.sendEmail(recipient, subject, body);
}

// PubMedから論文のIDを取得する関数や、取得したIDに基づいて論文の要約や詳細情報を取得する関数など、スクリプト全体で共通して使用されるヘルパー関数群

// 日付をYYYY/MM/DD形式に変換する関数
function toYYYYMMDD(date) {
    console.log(`toYYYYMMDD - Received date: ${date}, Type: ${typeof date}`);
    if (!(date instanceof Date)) {
        console.error("toYYYYMMDD - Error: Not a Date object");
        return null;
    }        
    // 月と日が1桁の場合、先頭に0を追加する
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    return `${date.getFullYear()}/${month}/${day}`;
}

// 指定されたPubMedの論文IDに基づき、論文の要約情報を取得する関数
function getPaperSummaryByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=${id}`;
    console.log(url);
    const res = JSON.parse(UrlFetchApp.fetch(url).getContentText());
    return res.result[id];
}

// 指定されたPubMedの論文IDに基づき、論文の抄録を取得する関数
function getPaperAbstractByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id=${id}`;
    const xml = UrlFetchApp.fetch(url).getContentText();
    const match = xml.match(/<Abstract>(.*?)<\/Abstract>/);
    return match ? match[1] : "";
}

// 指定された論文のタイプが設定した条件に合致するかをチェックする関数
function checkPubtype(pubtypes) {
    const common = pubtypes.filter(x => PUBMED_PUBTYPES.indexOf(x) !== -1);
    return common.length > 0;
}

// 指定された日付に基づき、PubMedから論文のIDを取得する関数
// 過去二週間の論文IDを取得する関数(更新版)
// getPaperIDsOn関数内でのendDateの扱いを確認
function getPaperIDsOn(startDate, endDate) {
    console.log(`getPaperIDsOn - StartDate: ${startDate}, EndDate: ${endDate}`); // デバッグログ
    console.log(`getPaperIDsOn - StartDate type: ${typeof startDate}, EndDate type: ${typeof endDate}`); // データタイプ確認
    const query = encodeURIComponent(PUBMED_QUERY);
    
    // ここでstartDateとendDateをtoYYYYMMDDに渡す
    const startDateFormatted = toYYYYMMDD(startDate);
    const endDateFormatted = toYYYYMMDD(endDate);
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&sort=pub_date&term=${query}&mindate=${startDateFormatted}&maxdate=${endDateFormatted}`;
    console.log(url);
    try {
        const response = UrlFetchApp.fetch(url);
        const data = JSON.parse(response.getContentText());
        const ids = data.esearchresult.idlist;
        return ids;
    } catch (e) {
        console.error("Failed to fetch paper IDs: " + e.message);
        return []; // エラーが発生した場合は空のリストを返します
    }
}


// OpenAIのChatGPT APIを呼び出し、指定された入力に基づいて応答を取得する関数
function callChatGPT(input, apiKey) {
    const url = "https://api.openai.com/v1/chat/completions";
    const options = {
        method: "post",
        headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json",
        },
        payload: JSON.stringify({
            model: "gpt-3.5-turbo", // ここを利用可能なモデルに変更
            messages: [{ role: "user", content: PROMPT_PREFIX + "\n" + input }],
        }),
    };
    
    try {
        const response = UrlFetchApp.fetch(url, options);
        if (response.getResponseCode() === 200) {
            return JSON.parse(response.getContentText());
        } else {
            console.error(`Failed to call OpenAI API: ${response.getContentText()}`);
        }
    } catch (e) {
        console.error(`An error occurred: ${e.message}`);
    }
}

コピペ用コード(LINE もEmail も送る)

// ChatGPT に渡す命令
const PROMPT_PREFIX = "あなたは神経系と動物行動に詳しい研究者です。与えられた論文の要点を3点のみでまとめ、以下のフォーマットで日本語で出力してください。改行してください。\
【タイトルの日本語訳】 \
・要点1 \
・要点2 \
・要点3";

// PubMed の検索クエリ
const PUBMED_QUERY = '"Behavior, Animal"[MeSH] NOT "Humans"[Mesh]';
// PubMed の対象の記事タイプ
const PUBMED_PUBTYPES = ["Journal Article", "Books and Documents", "Clinical Trial", "Meta-Analysis", "Randomized Controlled Trial", "Review", "Systematic Review"];
// PubMed の検索対象日数
const PUBMED_TERM = 14;
// PubMed の検索時のヒット論文で要約する論文の本数の上限
const MAX_PAPER_COUNT = 1;

// 重複投稿を防ぐ!
// 投稿済み論文IDを取得し、一定期間後に古いデータを削除することで重複投稿を防ぐ
// 投稿済みの論文IDとその投稿日を取得する関数
function getPostedPaperIds() {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    const twoWeeksAgo = new Date();
    twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); // 二週間前の日付を計算

    // 二週間以内のものだけを残します(古いログの削除)
    for (const [id, date] of Object.entries(postedIdsWithDates)) {
        if (new Date(date) < twoWeeksAgo) {
            delete postedIdsWithDates[id]; // 二週間より前のデータは削除
        }
    }

    // 更新したデータをスクリプトのプロパティに保存
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));

    // 更新後のIDのリストを返す
    return Object.keys(postedIdsWithDates);
}

// 論文IDを投稿日とともに保存する関数
function savePostedPaperId(id) {
    const postedData = PropertiesService.getScriptProperties().getProperty("POSTED_PAPER_IDS");
    let postedIdsWithDates = postedData ? JSON.parse(postedData) : {};
    
    // 現在の日時をISO形式で保存
    postedIdsWithDates[id] = new Date().toISOString();
    
    PropertiesService.getScriptProperties().setProperty("POSTED_PAPER_IDS", JSON.stringify(postedIdsWithDates));
}

// Google Apps Script エディタでの論文ID ログ出力
function logPostedPaperIds() {
    const postedIds = getPostedPaperIds();
}

// スクリプトのメイン関数
// ここでAPIキーを取得し、PubMedから論文情報を取得してChatGPTに要約させ、LINE に送信
function main() {
    const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");
    const LINE_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_TOKEN");
    const EMAIL_RECIPIENT = PropertiesService.getScriptProperties().getProperty("EMAIL_RECIPIENT");
    const EMAIL_SUBJECT = "PubMed 新着論文の要約"; // 件名を適宜設定してください
    if (!OPENAI_API_KEY || !LINE_TOKEN || !EMAIL_RECIPIENT) { console.error("ERROR: OPENAI_API_KEY or LINE_TOKEN or EMAIL_RECIPIENT is not set. Please set them in the Script Properties.");
    return;
}
    // スクリプトのメイン関数内で過去14日間の論文IDを取得する部分
    // getPaperIDsOn関数を正しく呼び出す
    const today = new Date(); // 今日の日付
    const fourteenDaysAgo = new Date();
    fourteenDaysAgo.setDate(today.getDate() - 14); // 14日前の日付

    // ここでエラーが発生していた場合、startDate と endDate が正しく Date オブジェクトとして扱われているかを確認
    console.log(`StartDate: ${toYYYYMMDD(fourteenDaysAgo)}, EndDate: ${toYYYYMMDD(today)}`);

    // 更新された関数を使用して、過去14日間の論文IDを取得
    const ids = getPaperIDsOn(fourteenDaysAgo, today);
 

    let output = "【PubMed 新着】";
    let paperCount = 0;
    const postedIds = getPostedPaperIds(); // 既に投稿された論文IDのリストを取得

    for (let i = 0; i < ids.length; i++) {
        const id = ids[i];
        
        if (postedIds.includes(id)) {
            console.log(`ID ${id}は既に投稿されています。スキップします。`);
            continue; // このIDの処理をスキップ
        }
        
        Utilities.sleep(1000); // APIリクエスト間の遅延を導入

        try {
            const summary = getPaperSummaryByID(id);
            if (!summary || !summary.title) {
                console.error(`ID ${id}のsummaryが正しく取得できていないか、タイトルがありません。`);
                continue; // タイトルがなければこのIDの処理をスキップ
            }
            
            if (++paperCount > MAX_PAPER_COUNT) break; // 最大処理数に達したらループを終了

            const abstract = getPaperAbstractByID(id);
            const input = `\ntitle: ${summary.title}\nabstract: ${abstract}`;
            const res = callChatGPT(input, OPENAI_API_KEY);

            const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
            const paragraphs = res.choices.map*3;
            output += `${paragraphs.join("\n")}\n\n${paperUrl}\n\n\n`;
            // 【雑誌の略称】とアブストラクトを定義
            const descriptionWithJournal = `【${summary.source}】${abstract.substring(0, 197) + '...'}`; // 200文字制限を適用
            sendLineNotify(paragraphs.join("\n"), LINE_TOKEN, paperUrl, summary.title, descriptionWithJournal);
            // メールの本文を組み立てる
            const emailBody = `タイトル: ${summary.title}
                              \n要約:\n${paragraphs.join("\n")}
                              \nURL: ${paperUrl}
                              \n${descriptionWithJournal}`; 
            // sendEmail関数を呼び出してメールを送信
            sendEmail(EMAIL_RECIPIENT, EMAIL_SUBJECT, emailBody);
            
            // IDを投稿済みリストに追加
            savePostedPaperId(id);
        } catch (e) {
            console.error(`An error occurred while processing ID ${id}: ${e.message}`);
        }
    }

    if (paperCount === 0) {
        output += "検索条件に合致した新着論文はありませんでした。";
    }
    output = output.trim();
    console.log(output);
    // IDリストをログに出力する
    logPostedPaperIds();

// 現在の日付で新たにPubMedから論文IDを取得
const newIds = getPaperIDsOn(today, today);

// 取得した各IDに対して処理を実行
newIds.forEach(id => {
    // 論文の要約と抄録を取得
    const summary = getPaperSummaryByID(id);
    const abstract = getPaperAbstractByID(id);
    // コンソールに論文の要約を出力
    console.log(`ID ${id}のsummary:`, summary);
    // 要約と抄録が取得できた場合のみ処理を続行
    if (summary && abstract) {
        // PubMed論文のURLを構築
        const paperUrl = `https://pubmed.ncbi.nlm.nih.gov/${id}`;
        // LINE に送信    
        sendLineNotify(paragraphs.join("\n"), LINE_TOKEN, paperUrl, summary.title, descriptionWithJournal);
    }
});
}

 

// 論文の要約とリンクをLINE に送信する関数
function sendLineNotify(content, token, paperUrl, title, description) {
  const messageContent = `${title}\n${description}\nURL: ${paperUrl}\n${content}`;
  const lineNotifyApi = 'https://notify-api.line.me/api/notify';
  const options = {
    "method": "post",
    "headers": {"Authorization": "Bearer " + token},
    "payload": "message=" + encodeURIComponent(messageContent),
    "muteHttpExceptions": true
  };
  UrlFetchApp.fetch(lineNotifyApi, options);
}

// 論文の要約とリンクをメールに送信する関数
function sendEmail(recipient, subject, body) {
  GmailApp.sendEmail(recipient, subject, body);
}

 

// PubMedから論文のIDを取得する関数や、取得したIDに基づいて論文の要約や詳細情報を取得する関数など、スクリプト全体で共通して使用されるヘルパー関数群

// 日付をYYYY/MM/DD形式に変換する関数
function toYYYYMMDD(date) {
    console.log(`toYYYYMMDD - Received date: ${date}, Type: ${typeof date}`);
    if (!(date instanceof Date)) {
        console.error("toYYYYMMDD - Error: Not a Date object");
        return null;
    }        
    // 月と日が1桁の場合、先頭に0を追加する
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    return `${date.getFullYear()}/${month}/${day}`;
}

// 指定されたPubMedの論文IDに基づき、論文の要約情報を取得する関数
function getPaperSummaryByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=${id}`;
    console.log(url);
    const res = JSON.parse(UrlFetchApp.fetch(url).getContentText());
    return res.result[id];
}

// 指定されたPubMedの論文IDに基づき、論文の抄録を取得する関数
function getPaperAbstractByID(id) {
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id=${id}`;
    const xml = UrlFetchApp.fetch(url).getContentText();
    const match = xml.match(/<Abstract>(.*?)<\/Abstract>/);
    return match ? match[1] : "";
}

// 指定された論文のタイプが設定した条件に合致するかをチェックする関数
function checkPubtype(pubtypes) {
    const common = pubtypes.filter(x => PUBMED_PUBTYPES.indexOf(x) !== -1);
    return common.length > 0;
}

// 指定された日付に基づき、PubMedから論文のIDを取得する関数
// 過去二週間の論文IDを取得する関数(更新版)
// getPaperIDsOn関数内でのendDateの扱いを確認
function getPaperIDsOn(startDate, endDate) {
    console.log(`getPaperIDsOn - StartDate: ${startDate}, EndDate: ${endDate}`); // デバッグログ
    console.log(`getPaperIDsOn - StartDate type: ${typeof startDate}, EndDate type: ${typeof endDate}`); // データタイプ確認
    const query = encodeURIComponent(PUBMED_QUERY);
    
    // ここでstartDateとendDateをtoYYYYMMDDに渡す
    const startDateFormatted = toYYYYMMDD(startDate);
    const endDateFormatted = toYYYYMMDD(endDate);
    const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&sort=pub_date&term=${query}&mindate=${startDateFormatted}&maxdate=${endDateFormatted}`;
    console.log(url);
    try {
        const response = UrlFetchApp.fetch(url);
        const data = JSON.parse(response.getContentText());
        const ids = data.esearchresult.idlist;
        return ids;
    } catch (e) {
        console.error("Failed to fetch paper IDs: " + e.message);
        return []; // エラーが発生した場合は空のリストを返します
    }
}


// OpenAIのChatGPT APIを呼び出し、指定された入力に基づいて応答を取得する関数
function callChatGPT(input, apiKey) {
    const url = "https://api.openai.com/v1/chat/completions";
    const options = {
        method: "post",
        headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json",
        },
        payload: JSON.stringify({
            model: "gpt-3.5-turbo", // ここを利用可能なモデルに変更
            messages: [{ role: "user", content: PROMPT_PREFIX + "\n" + input }],
        }),
    };
    
    try {
        const response = UrlFetchApp.fetch(url, options);
        if (response.getResponseCode() === 200) {
            return JSON.parse(response.getContentText());
        } else {
            console.error(`Failed to call OpenAI API: ${response.getContentText()}`);
        }
    } catch (e) {
        console.error(`An error occurred: ${e.message}`);
    }
}

 

残りは前回記事と同様です。

定期的に投稿するためのトリガーを設定

GAS の左タブから「トリガー」を選択し、クリックします。

トリガー画面

「トリガーを追加」をクリックします。

トリガーの設定画面
  • 実行する関数 → main
  • 実行するデプロイ → Head
  • イベントのソースを選択 → 時間主導型
  • トリガーのタイプを選択 → 時間ベースのタイマー
  • 時間の間隔を選択(時間)→ 何時間ごとに投稿されるかをお好みで変更

で保存します。

トリガーを保存すると、Google アカウント選択画面が立ち上がるので、アカウントを選択します。

Google アカウント選択画面が立ち上がる

次に警告画面が立ち上がります。

警告画面

左下の「Advanced」をクリックし、左下のGo to GAS 名 (unsafe) をさらにクリックします。

次の画面で右下の「Allow」をクリックし、すべての設定は終了です。

あとは設定時間までニコニコ待つだけです。

【New!】サポート機能を導入しました!

月3000 円の月謝は(安くない♪)ので、今回投げ銭機能をブログにつけてみました!すでにお気づきかもしれませんがサイドバーにも同じボタンがあります。

気が向いたら応援してみてください!中の人が喜びます。

参考にしたサイトのまとめ

qiita.com

www.dental-oral-surgery.com

qiita.com

*1:c) => c.message.content.trim(

*2:c) => c.message.content.trim(

*3:c) => c.message.content.trim(