GASとDeepLでPubmed新着論文の検索・翻訳・メール通知を自動化する

# 背景
研究する上で先行研究の把握は何より重要ですが、英語の論文検索を習慣化するのはなかなか難しいものです。そこで、
1. Pubmedの新着論文を取得
2. 論文要旨の翻訳
3. 翻訳文のメール送付

を自動で実行するスクリプトを[ChatGPT](https://chat.openai.com/)の力を借りて作成してみました。
※作成したスクリプトと使用方法を確認されたい方は[方法](https://qiita.com/drafts/99317c05c9f141a40f61/edit#%E6%96%B9%E6%B3%95)を飛ばして[結果](https://qiita.com/drafts/99317c05c9f141a40f61/edit#%E7%B5%90%E6%9E%9C)をご確認ください。

# 方法
### 1. 使用するサービスの選定
まず、どのようなサービスを利用すれば上記①～③を自動化できるのかChatGPT4に質問してみました。
※ChatGPT4への質問は2023年4月から5月にかけて実施しました。そのため、2024年3月現在では回答が異なる可能性があります。
#### プロンプト
```
私は下記のようなアプリを作成したいと考えています。私はあまりプログラミングに詳しくありません。初心者でも可能な下記アプリの作成方法を教えてください。必要に応じて複数の案を提示してください。

1. pubmedに特定のキーワードに関連する論文が掲載されたらメールで通知する。
2. そのメールにはタイトル、著者名、URL、英語の要旨、日本語の要旨を記載する。
```
#### 回答
```
初心者でも作成できるような方法をいくつか提案します。プログラミングの知識が少ない場合は、既存のツールやサービスを利用することで、アプリの作成を容易にできます。

方法1: オンラインサービスを使う IFTTT（If This Then That）のようなオンラインサービスを使って、Pubmedからの通知を設定し、メールで受け取ることができます。
1. IFTTT（https://ifttt.com/）にアクセスし、アカウントを作成します。
2. 新しいアプレットを作成します。
3. トリガー（if this）として、RSSフィードを選択し、PubmedのRSSフィードURLを入力します（キーワードで検索した後に、検索結果ページのRSSリンクを取得できます）。
4. アクション（then that）として、メールを選択し、必要な情報（タイトル、著者名、URL、英語の要旨）を含めるように設定します。
5. 日本語の要旨については、IFTTTでは対応していないため、英語の要旨をDeepL(https://www.deepl.com/translator)等の翻訳サービスで手動で翻訳する必要があります。

方法2: Google Apps Scriptを使う Google Apps Scriptを使って、Gmailでメールを送信することができます。以下の手順で進めてください。
1. Googleアカウントにログインし、Google Apps Script（https://script.google.com/）にアクセスします。
2. 新しいプロジェクトを作成し、スクリプトエディタを開きます。
3. スクリプトエディタで、PubmedのAPIを使用して、キーワードに関連する論文を検索し、必要な情報（タイトル、著者名、URL、英語の要旨）を取得するコードを書きます。
4. 日本語の要旨については、Google翻訳APIを使って英語の要旨を翻訳するコードを書きます。
5. 取得した情報を元に、Gmailでメールを送信するコードを書きます。
6. トリガーを設定して、定期的にスクリプトが実行されるようにします。

これらの方法を試して、自分に合った方法でアプリを作成してみてください。プログラミングに慣れていない場合は、方法1のオンラインサービスを利用することが簡単でおすすめです。
```

### 2. DeepLの利用
方法1は手動で翻訳する必要があるため、Google Apps Scriptを利用する方法2を採用します。ただし、google翻訳よりDeepLの方が自然な日本語訳になることが多いので、翻訳にDeepLを使用する方法を尋ねてみます。
#### プロンプト
```
「方法2」について、google翻訳APIではなくDeepLを使用して英語を日本語に翻訳する方法はありませんか？
```
#### 回答
```
もちろん、Google Apps ScriptでDeepL APIを使用して英語を日本語に翻訳することができます。以下の手順で進めてください。

1. DeepL APIキーを取得する DeepL APIキーを取得するために、DeepLの開発者ページ（https://www.deepl.com/pro#developer）でプランにサインアップします。APIキーをメモしておきます。
2. Google Apps Scriptでプロジェクトを作成する Googleアカウントにログインし、Google Apps Script（https://script.google.com/）にアクセスして新しいプロジェクトを作成し、スクリプトエディタを開きます。
3. スクリプトエディタで、PubmedのAPIを使用して、キーワードに関連する論文を検索し、必要な情報（タイトル、著者名、URL、英語の要旨）を取得するコードを書きます。
4. DeepL APIを使用して英語の要旨を日本語に翻訳する関数を作成します。以下のサンプルコードを参考にしてください（`YOUR_API_KEY`を取得したDeepL APIキーに置き換えてください）。
```

```jsx
function translateWithDeepL(text) {
  const apiKey = 'YOUR_API_KEY';
  const sourceLang = 'EN';
  const targetLang = 'JA';
  const apiUrl = `https://api.deepl.com/v2/translate?auth_key=${apiKey}&text=${encodeURIComponent(text)}&source_lang=${sourceLang}&target_lang=${targetLang}`;

  const options = {
    'method': 'POST',
    'muteHttpExceptions': true
  };

  const response = UrlFetchApp.fetch(apiUrl, options);
  const jsonResponse = JSON.parse(response.getContentText());

  if (jsonResponse.translations && jsonResponse.translations.length > 0) {
    return jsonResponse.translations[0].text;
  } else {
    return null;
  }
}
```
```
#回答の続き
1. 取得した情報を元に、Gmailでメールを送信するコードを書きます。英語の要旨を日本語に翻訳するために、上記で作成した関数を使用します。
2. トリガーを設定して、定期的にスクリプトが実行されるようにします。

この方法で、Google Apps Scriptを使ってDeepL APIを活用し、英語の要旨を日本語に翻訳してメールで通知することができます。
```

### 3. Pubmed APIの仕様把握、DeepL APIキーの取得
上記のシステムはPubmed APIとDeepL APIを利用します。DeepL APIを利用するにはDeepL APIに登録してキーを取得する必要があります。下記は日本語でのPubmed APIの解説とDeepL APIのキー取得に関する公式のヘルプページですのでご参照ください。
※DeepL APIは無料版と有料版があり、1ヶ月に翻訳可能な文字数の上限が異なります。
取得する論文数にも寄りますが、私の場合は無料版で十分でした（数ヶ月運用した際の文字数を[結果](https://qiita.com/drafts/99317c05c9f141a40f61/edit#%E7%B5%90%E6%9E%9C)に記載しています）。


[Pythonで論文情報をまとめてゲットする① ~ PubMed APIについて ~](
https://lifesciencehack-ai.hatenablog.com/entry/Python%E3%81%A7%E8%AB%96%E6%96%87%E6%83%85%E5%A0%B1%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E3%82%B2%E3%83%83%E3%83%88%E3%81%99%E3%82%8B%E2%91%A0PubMedAPI%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)

[DeepL API用のAPIキー](https://support.deepl.com/hc/ja/articles/360020695820-DeepL-API%E7%94%A8%E3%81%AEAPI%E3%82%AD%E3%83%BC)


### 4. Google Apps Scriptの作成
今までの質問を踏まえてGoogle Apps Scriptを作成してもらいます。結果として、下記のようなスクリプトが出力されましたが、**このままでは動きません**。また、過去に検索した文献は除外する仕組みも必要です。そのため、意図したスクリプトになるよう 「**実行→エラー→エラー文を提示し、修正するよう指示**」を繰り返していきます（過程は長いので省略）。
#### プロンプト
```
「方法2」の「スクリプトエディタで、PubmedのAPIを使用して、キーワードに関連する論文を検索し、必要な情報（タイトル、著者名、URL、英語の要旨）を取得するコード」を作成してください。
```
#### 回答
```
PubmedのAPI（Entrez Programming Utilities: E-utilities）を使用して、キーワードに関連する論文を検索し、必要な情報を取得する方法について説明します。以下に、Google Apps ScriptでPubMedを検索するサンプルコードを示します。
```
```javascript
// 検索するキーワード
const keyword = 'cancer';

// Pubmed APIを使って論文を検索する
function searchPubmed() {
  const searchResults = fetchPubmedIds(keyword);
  const articleDetails = fetchArticleDetails(searchResults);
  sendEmail(articleDetails);
}

// PubMedのIDを取得する
function fetchPubmedIds(keyword) {
  const baseUrl = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/';
  const searchUrl = `${baseUrl}esearch.fcgi?db=pubmed&term=${encodeURIComponent(keyword)}&retmode=json&retmax=10`;

  const searchResponse = UrlFetchApp.fetch(searchUrl);
  const searchJson = JSON.parse(searchResponse.getContentText());

  return searchJson.esearchresult.idlist;
}

// 論文の詳細を取得する
function fetchArticleDetails(pubmedIds) {
  const baseUrl = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/';
  const idString = pubmedIds.join(',');
  const summaryUrl = `${baseUrl}esummary.fcgi?db=pubmed&id=${idString}&retmode=json`;

  const summaryResponse = UrlFetchApp.fetch(summaryUrl);
  const summaryJson = JSON.parse(summaryResponse.getContentText());
  const articleList = [];

  for (let id in summaryJson.result) {
    if (id === 'uids') continue;
    const articleData = summaryJson.result[id];
    const article = {
      title: articleData.title,
      authors: articleData.authors.map(author => author.name).join(', '),
      url: `https://pubmed.ncbi.nlm.nih.gov/${id}/`,
      abstract: fetchArticleAbstract(id)
    };
    articleList.push(article);
  }

  return articleList;
}

// 論文の要旨を取得する
function fetchArticleAbstract(pubmedId) {
  const baseUrl = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/';
  const abstractUrl = `${baseUrl}efetch.fcgi?db=pubmed&id=${pubmedId}&retmode=text&rettype=abstract`;

  const abstractResponse = UrlFetchApp.fetch(abstractUrl);
  const abstractText = abstractResponse.getContentText();

  return abstractText.trim();
}
// 以下省略
```

### 5. スクリプトの概要
ChatGPTによる修正を繰り返し、最終的には下記のような流れで論文検索から翻訳、メール送付までを実行するスクリプトになりました。
1. pubmed APIを利用して特定のキーワードに関する新着論文を検索する。
2. 新着論文のPubmedIDを取得し、Google スプレッドシートに記録された過去の検索結果と照合する。過去に翻訳した論文は除外する。
3. 新着論文の要旨を取得。
4. DeepL APIを利用して要旨を日本語に翻訳。
5. 体裁を整えて指定したGmailアドレスに送信。
6. 送信した文献の一覧表をGoogle スプレッドシートに記載（次回の照合に利用）。
7. この一連の流れをGoogle Apps Scriptのトリガー機能で1日に1度実行する。

# 結果
### 1. スクリプトの貼り付け
最終的なスクリプトは下記の通りです。検索したい論文の内容等に合わせて修正し、[Google Apps Script](https://script.google.com/)の「新しいプロジェクト」にコピー&ペーストします。
![スクリーンショット 2024-03-24 012508.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363491/543dcc92-ab86-7904-6409-e74fa3b7ffd0.png)
**修正箇所**
- 2行目：検索結果を記録するGoogle スプレッドシートのIDを記載。Google スプレッドシートのIDとはスプレッドシートのURL「`https://docs.google.com/spreadsheets/d/xxx/edit#gid=0`」のうち`d`と`edit#gid=0edit`に挟まれたxxx部分のことです。
- 3行目：Google スプレッドシートのシート名を記載。
- 6行目：取得したDeepL APIキーを記載。
- 107行目：翻訳文を送付するGmailアドレスを記載。
- 142行目：論文を検索するキーワードを記載。「microbiome」などの単語でもヒットしますが、より詳細な条件を設定したい場合はandやorなどを用いた検索も可能です。Pubmedの検索キーワードの詳細については下記の資料などをご参照ください。
[東京大学医学図書館　Pubmedの使い方](https://www.lib.m.u-tokyo.ac.jp/manual/pubmedmanual.pdf)
- 144行目：検索する論文数の上限値を記載。私は「10」に設定していますが、分野や検索キーワードによってヒットする新着論文の数は変わりますので、目的に応じて設定してください。

```javascript
// Google Spreadsheet settings
const SPREADSHEET_ID = 'Google スプレッドシートのID';
const SHEET_NAME = 'スプレッドシートのシート名';

// DeepL API settings
const DEEPL_API_KEY = '取得したDeepL APIキー';

function searchPapers(keyword, maxResults) {
  // Get the current date
  var currentDate = new Date();
  
  // Calculate the date 30 days ago
  var pastDate = new Date();
  pastDate.setDate(currentDate.getDate() - 30);
  
  // Format the dates in YYYY/MM/DD format
  var formattedCurrentDate = currentDate.toISOString().slice(0,10).replace(/-/g,"/");
  var formattedPastDate = pastDate.toISOString().slice(0,10).replace(/-/g,"/");
  
  // Add the date range to the search term
  const apiUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${keyword}+AND+(${formattedPastDate}[PDAT]:${formattedCurrentDate}[PDAT])&retmax=${maxResults}&retmode=json`;
  const response = UrlFetchApp.fetch(apiUrl);
  const idList = JSON.parse(response.getContentText()).esearchresult.idlist;
  return idList;
}

function getPaperAbstract(id) {
  const response = UrlFetchApp.fetch(`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${id}&retmode=xml`, {muteHttpExceptions: true});
  const document = XmlService.parse(response.getContentText());
  const root = document.getRootElement();
  const abstractTextElement = root
    .getDescendants().filter(function(item) {
      return item.getType() == XmlService.ContentTypes.ELEMENT;
    })
    .map(function(item) {
      return item.asElement();
    })
    .filter(function(element) {
      return element.getName() == 'AbstractText';
    })[0];

  // Return the abstract text, or an empty string if the abstract is not available
  return abstractTextElement ? abstractTextElement.getText() : '';
}

function getPaperDetails(id) {
  var response = UrlFetchApp.fetch(`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${id}&retmode=json`, {muteHttpExceptions: true});
  var data = JSON.parse(response.getContentText());

  // Log the response data
  Logger.log('Response data for paper ID ' + id + ': ' + JSON.stringify(data));

  var result = data.result[id];
  var paperDetails = {
    title: result.title,
    authors: result.authors.map(function(author) { return author.name; }).join(', '),
    journal: result.source,
    abstract: getPaperAbstract(id),  // Call the new function here
    url: 'https://pubmed.ncbi.nlm.nih.gov/' + id
  };

  return paperDetails;
}

function translateAbstract(abstract) {
  const deeplResponse = UrlFetchApp.fetch('https://api-free.deepl.com/v2/translate', {
    method: 'post',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    payload: {
      'auth_key': DEEPL_API_KEY,
      'text': abstract,
      'target_lang': 'JA'
    }
  });
  
  const translatedText = JSON.parse(deeplResponse.getContentText()).translations[0].text;
  return translatedText;
}

function checkIfListed(id) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
  const data = sheet.getDataRange().getValues();
  
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === id) {
      return true;
    }
  }
  
  return false;
}

function addPaperToSheet(id, paperDetails, executionDate) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
  sheet.appendRow([
    id,
    executionDate,
    paperDetails.journal,
    paperDetails.title,
    paperDetails.authors,
    paperDetails.abstract,
    paperDetails.abstractJP,
    paperDetails.url
  ]);
}
function sendEmail(subject, body) {
  const email = '宛先となるGmailのアドレス';
  GmailApp.sendEmail(email, subject, body);
}

function getExistingPaperIds() {
  var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
  if (!sheet) {
    Logger.log('Cannot find the sheet');
    return [];
  }
  var range = sheet.getRange(2, 1, sheet.getLastRow()-1, 1); // Assumes IDs are in column 1
  var values = range.getValues();
  
  // Flatten array and remove any 'undefined' values
  var existingIds = [].concat.apply([], values).filter(function (el) {
    return el != null;
  });
  
  Logger.log('Existing paper IDs: ' + existingIds);
  return existingIds;
}

function checkIfListed(id) {
  var existingIds = getExistingPaperIds();
  for (var i = 0; i < existingIds.length; i++) {
    if (existingIds[i].toString() === id.toString()) {
      Logger.log('Paper ID ' + id + ' is already listed');
      return true;
    }
  }
  Logger.log('Paper ID ' + id + ' is new');
  return false;
}

function getNewPapers() {
  var query = ['論文検索キーワード'].join(" ");
  var keyword = encodeURIComponent(query);
  var maxResults = ヒットする論文数の上限値;

  // New papers array
  var newPapers = [];

  var paperIds = searchPapers(keyword, maxResults);
  Logger.log('Search complete, paper IDs: ' + paperIds);
  
  for (var i = 0; i < paperIds.length; i++) {
    var id = paperIds[i];
    if (checkIfListed(id)) {
      Logger.log('Paper ID ' + id + ' is already listed, skipping');
      continue;
    }

    var paperDetails = getPaperDetails(id);
    Logger.log('Details fetched for paper ID ' + id + ': ' + JSON.stringify(paperDetails));
    
    var translatedAbstract = translateAbstract(paperDetails.abstract);
    Logger.log('Translated abstract for paper ID ' + id + ': ' + translatedAbstract);
    
    paperDetails.abstractJP = translatedAbstract;
    var executionTime = new Date();
    var dateOnlyString = executionTime.toISOString().slice(0, 10);
    addPaperToSheet(id, paperDetails, dateOnlyString);
    Logger.log('Paper ID ' + id + ' added to the sheet');

    // Add paper details to newPapers array
    newPapers.push(paperDetails);
  }

  // Prepare email content
  var todaysDate = new Date();
  var emailSubject = 'New Papers on ' + todaysDate;
  var emailBody = 'Here are the new papers:\n\n';
  
  // Add each paper to the email body
  for (var j = 0; j < newPapers.length; j++) {
    var paper = newPapers[j];
    emailBody += 'Title: ' + paper.title + '\n';
    emailBody += 'Authors: ' + paper.authors + '\n';
    emailBody += 'Journal: ' + paper.journal + '\n';
    emailBody += 'URL: ' + paper.url + '\n';
    emailBody += 'Abstract (Translated): '+ '\n' + paper.abstractJP + '\n\n';
  }

  // Send one email with all new papers
  sendEmail(emailSubject, emailBody);
  Logger.log('Email sent with subject: ' + emailSubject);
}

```

### 2. 実行するタイミングの設定
Google Apps Scriptのトリガー機能を利用して自分の意図するタイミングで実行されるように設定します。まず、Google Apps Scriptの左サイドバーから「トリガー」をクリックします。
![スクリーンショット 2024-03-24 012847_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363491/74b4d861-aa8d-9c47-9115-a4ae5fd74e5f.png)
次に「トリガーを追加」をクリックし、実行する関数を「getNewPapers」、時間ベースのトリガーのタイプを「日付ベースのタイマー」、時間は「午前7時～8時」を選択します。この設定で毎日7~8時に本スクリプトが実行され、翻訳された新着論文の要旨がGmailに送付されます。
![スクリーンショット 2024-03-24 013318_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363491/6cfdb726-e24c-d740-4873-de9c361faa56.png)



### 3. 実行結果
スクリプトが実行されると新着論文のタイトル、著者、雑誌、URL、日本語要旨が記載されたメールがGmailアドレスに送信されます。新着論文が0件だった場合は空のメールが送信されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363491/014fa878-8b7a-41f1-ed02-b8ab799634ee.png)

Google スプレッドシートには下記のように検索結果が記録されます。このうちA列のPubmeIDによって過去に翻訳を送付した文献か否かを判断しています。
- A列：PubmedID
- B列：検索日
- C列：雑誌名
- D列：論文タイトル
- E列：著者名
- F列：要旨（英語）
- G列：要旨（日本語）
- H列：論文URL
![スクリーンショット 2024-03-24 014306.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363491/1e442aaf-03b3-8641-934b-722e20e56d70.png)

数ヶ月運用した結果、1ヶ月あたりの翻訳文字数は6～8万文字でした。無料版のDeepL APIは毎月50万文字まで利用できるため、無料版で十分に対応できます。

# 感想
このシステムのおかげで、最新の研究論文を追うハードルが下がりました。今では、毎朝のルーティーンとして寝起きの布団の中で新着論文をチェックできています。研究する上で「やる気」は重要ですが、同時に習慣化するシステム作りも重要であると実感しました。

# 変更履歴
- 検索キーワードにスペースが入っても対応できるよう、143行目にURLエンコーディングを追加（2024年4月17日）。