論文検索を自動化してメール通知（GAS/Pubmed）

# 背景
新しい論文は日々パブリッシュされますが、毎日検索してチェックするのは面倒です。とはいえ、**最新の論文には常にキャッチアップしていたい**。ということで、GASを用いて**検索を自動化**して、新規論文があれば**定期的にメールで通知してくれる**機能を実装しました。

コード全文はこちら。

https://qiita.com/kujira_0120/items/1d3a4aa8a094919b6d3b

# ポイント

* 面倒な論文検索を**完全自動化**
* GASのトリガー機能で**定期実行**できる（例えば毎朝）
* もちろんPCを**起動しなくてOK**
* Abstractの**翻訳付き**
* APIキーの**取得不要**
* そして**環境構築が不要**


# 流れ

GASを用いて以下を定期実行する。
1. 【**検索**】指定ワードでPubmed検索し、新しい順にPMIDを指定件数だけ取得する（PMIDはPubmed上の識別子）
1. 【**評価**】取得したPMIDが新規かどうかを評価する（前回実行時のログと比較）
1. 【**情報取得**】新規だと判断された論文の情報を取得（タイトル、Abstract、ジャーナル名など）
1. 【**翻訳**】Abstractを日本語に翻訳
1. 【**メール**】論文情報を指定したメールアドレスに送信
1. 【**ログ**】PMIDなどをログとして残す

任意の条件（例えば、毎朝〇時、〇時間に一度など）で**トリガーを設定して定期実行**する。


# 実装

この記事では主に、**APIを用いてPubmedから情報を取得する方法**について、解説を入れながら載せています。
自動化に際して、**前回ログの読み込み**や**ログの出力**も必要不可欠ですが、そこら辺の解説は割愛しています。気になる方は[コード全文](https://qiita.com/kujira_0120/items/1d3a4aa8a094919b6d3b)を参照してください。


### 目次
1. [Pubmed検索(PMID)](#1-pubmed検索pmidの取得)
1. [論文情報の取得](#2-論文情報の取得)
1. [Abstractの取得と翻訳](#3-abstractの取得と翻訳)
1. [メールの送信](4-メールの送信) 


<br>


:::note warn
実装の中で**Pubmed API**を利用しますが、2024年2月時点の仕様に基づきます。変更された場合はコードの修正が必要になります。ちなみに、APIキーを取得せずに利用できますが、取得すると秒あたりのアクセス上限が増えます（3回/secから10回/sec）。
:::

**補足**：`Google Apps Script(GAS)`は`JavaScript(JS)`をもとにした言語なので、基本的には同じように記述します。Google検索のときは`「~~　javascript」`みたいに検索すると、情報がたくさん出てきます。ただし、JSにあるけどGASには無い関数とかもあるので注意してください。

## 1. Pubmed検索(PMIDの取得)

参考サイト
https://qiita.com/iwashi-kun/items/bd0d772c6db0c0023e30
https://www.y-shinno.com/pubmed-api/

APIにアクセスするためのリンク。
`term=`：検索ワード
`retmax=`：取得件数
`retmode=`：取得形式。今回はjsonを指定。

```
https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term={検索ワード}&retmax={件数}&retmode=json

//APIキーを取得済の場合`&api_key=~~~`を追加する。
https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=%s&retmax=%s&retmode=json&api_key=****************************
```

`UrlFetchApp.fetch()`メソッドでAPIに接続し、JSON形式で取得する。
`'esearchresult'` の次の階層 `'idlist'` を指定して、ヒットした論文のPMIDを取得する。

```js
// 指定したword でPubmed検索 
let word = '********'; //検索ワード
let num = 10; //取得するPMIDの件数

let query = Utilities.formatString('https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=%s&retmax=%s&retmode=json',word,num);

//APIに接続、JSON形式で取得、読み取り可能な形式に変換
let response = UrlFetchApp.fetch(query).getContentText();
let response_json=JSON.parse(response);

pmids = response_json['esearchresult']['idlist'].slice();
```
## 2. 論文情報の取得

上記で取得したPMIDをもとに論文の情報(JSON形式)を取得します。必要な情報だけパースして取り出し、取り出し用オブジェクトに格納します（なにかと使い勝手が良いのでオブジェクト形式を採用）。ちなみに、for文でループすると遅いので`map()`を使っています。


以下のコードでは次を取得している。
* パブリッシュ日
* タイトル
* ジャーナル名
* issn

最終的には、各論文の情報が入ったオブジェクトを格納した配列が返される。
中身のイメージはこんな感じ。
`[{論文1の情報}, 
{論文2の情報}, 
{論文3の情報}, 
{論文4の情報}, …]`
```js
let data = pmids.map((id, i) => {
  Utilities.sleep(0.35 * 1000)//アクセス制限回避のために一時停止

  //pmidを用いてそれぞれの論文から情報を取得するためのurlを作成
  let url= 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id='+id;

  json = JSON.parse(UrlFetchApp.fetch(url).getContentText());

  let obj = {
    pmid: id,
    pubdate: json['result'][id]['pubdate'],
    title: json['result'][id]['title'],
    journal_name: json['result'][id]['fulljournalname'],
    issn: json['result'][id]['issn'],
    link: 'https://pubmed.ncbi.nlm.nih.gov/'+id+'/',
}); 
```

## 3. Abstractの取得と翻訳
同様にAPIを用いて、Abstractの取得する。
```js
let abst = pmids.map((id, i) => {
  Utilities.sleep(0.35 * 1000)

  let url ='https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=XML&id='+id;
  let response_abst = UrlFetchApp.fetch(url).getContentText();
  let document = XmlService.parse(response_abst);
  let body = XmlService.getPrettyFormat().format(document);
});
```
### 文字列の整形と翻訳

上記コードでAbstractは取得できるが、このままだとHTMLタグなど余計な文字列が含まれていて邪魔。なので、以下で定義する`processText`関数によって除去します。

次に、`LanguageApp.translate()`メソッドで翻訳する。
DeepL、Google翻訳などに比べて精度は高くないだろうけど、使い勝手が良いのでこれを採用。

↓個人的に感じているメリット
* API不要
* 課金なし
* 色々書かず一行の記述で済む
* なんだかんだGoogleが提供している機能なので性能に問題なさそう


コードは以下の通りです。Abstract原文と日本語訳を格納したオブジェクトを、論文の件数分格納した配列が返されます。
中身のイメージはこんな感じ。
`[{論文1 Abstractと日本語訳}, 
{論文2 Abstractと日本語訳}, 
{論文3 Abstractと日本語訳}, 
{論文4 Abstractと日本語訳}, …]`

```js
let abst = pmids.map((id, i) => {
  Utilities.sleep(0.35 * 1000)

  let url ='https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=XML&id='+id;
  let response_abst=UrlFetchApp.fetch(url).getContentText();
  let document = XmlService.parse(response_abst);
  let body = XmlService.getPrettyFormat().format(document);

  let processed = processText(body);
  let jpn = LanguageApp.translate(processed, 'en','ja');

  let obj = {
    abst: processed,
    abst_jpn: jpn
});


function processText(body){
    //abstrastの両端の文字位置を取得
    let start =[];
    let end =[];
    start = body.indexOf('<Abstract>')+10 ;
    end= body.indexOf('</Abstract>') ;

    //abstract の切り出し
    let raw_text = body.substring(start,end);
    let processed = raw_text.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'').replace(/[\r\n]+/g,'');

    while( processed.indexOf("  ") >=0){
      processed = processed.replace("  ","").slice()
    }
  return processed;
}
```

論文情報が格納されている`data`と、Abstractが格納されている`abst`をくっつける。
```js
data = data.map((each, i) => {
    data[i] = Object.assign(data[i],abst[i]);
    return data[i];
});
```
ここで得られる`data`を、メール送信時に用いる。


## 4. メールの送信

`GmailApp.sendEmail()`でメールを送信する。


基本的なパラメータは以下の通り。
`GmailApp.sendEmail(recipient, subject, body, options)`

| 名前 | 型 |説明|
|:--|:--|:--|
|recipient|String|受信者のアドレス|
|subject|String|件名（最大半角 250 文字（全角 125 文字）
|body|String|メールの本文|
|options|Object|※[ドキュメント](https://developers.google.com/apps-script/reference/gmail/gmail-app?hl=ja#advanced-parameters_1)参照|

以下のコードでは`options`でHTML形式を指定している。

```js
//メールの送信を実行する関数
function sendEmail(data, meta){

  //メールの送信設定
  subject='New paper';

//メール文面の作成
  let body = Utilities.formatString(
    'PMIDS: %s<br>Journal: %s<br>Publish date: %s<br><br>Tile:<br><b>%s</b><br><br>%s<br><br>Abstract:<br>%s<br><br>%s',
    data.pmid,
    data.journal_name,
    data.pubdate,
    data.title,
    data.link,
    data.abst,
    data.abst_jpn
    );

  address = meta.email;
  //Gmailの送信
  GmailApp.sendEmail(address,subject,body,{htmlBody:body});
  console.log(address);
}
```


<details><summary>送信メール例（画像）</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655949/1450e759-4de4-e69b-78b4-1b7853a838c4.png)
</details>


**補足**：`GmailApp`クラスには他にもいろいろな使い方があって便利です。
https://developers.google.com/apps-script/reference/gmail/gmail-app?hl=ja


# 最後に
Pumed検索とメール送信の実装は上記に書いた通りです。これをもとにして自動化＆定期実行するためのコードは、[こちら](https://qiita.com/kujira_0120/items/1d3a4aa8a094919b6d3b)を参照してください。