背景
新しい論文は日々パブリッシュされますが、毎日検索してチェックするのは面倒です。とはいえ、最新の論文には常にキャッチアップしていたい。ということで、GASを用いて検索を自動化して、新規論文があれば定期的にメールで通知してくれる機能を実装しました。
コード全文はこちら。
ポイント
- 面倒な論文検索を完全自動化
- GASのトリガー機能で定期実行できる(例えば毎朝)
- もちろんPCを起動しなくてOK
- Abstractの翻訳付き
- APIキーの取得不要
- そして環境構築が不要
流れ
GASを用いて以下を定期実行する。
- 【検索】指定ワードでPubmed検索し、新しい順にPMIDを指定件数だけ取得する(PMIDはPubmed上の識別子)
- 【評価】取得したPMIDが新規かどうかを評価する(前回実行時のログと比較)
- 【情報取得】新規だと判断された論文の情報を取得(タイトル、Abstract、ジャーナル名など)
- 【翻訳】Abstractを日本語に翻訳
- 【メール】論文情報を指定したメールアドレスに送信
- 【ログ】PMIDなどをログとして残す
任意の条件(例えば、毎朝〇時、〇時間に一度など)でトリガーを設定して定期実行する。
実装
この記事では主に、APIを用いてPubmedから情報を取得する方法について、解説を入れながら載せています。
自動化に際して、前回ログの読み込みやログの出力も必要不可欠ですが、そこら辺の解説は割愛しています。気になる方はコード全文を参照してください。
目次
実装の中で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を取得する。
// 指定した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の情報}, …]
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の取得する。
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と日本語訳}, …]
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
をくっつける。
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 | ※ドキュメント参照 |
以下のコードではoptions
でHTML形式を指定している。
//メールの送信を実行する関数
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);
}
送信メール例(画像)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655949/1450e759-4de4-e69b-78b4-1b7853a838c4.png)補足:GmailApp
クラスには他にもいろいろな使い方があって便利です。
https://developers.google.com/apps-script/reference/gmail/gmail-app?hl=ja
最後に
Pumed検索とメール送信の実装は上記に書いた通りです。これをもとにして自動化&定期実行するためのコードは、こちらを参照してください。