bioRxivでバズってる論文を抽出してSNSにメッセージを流してみた

きっかけ

近年、NatureやCell, Scienceに論文が投稿される前に、preprintサーバであるBioRxivに投稿する流れが増えてきています(2015年以降、急激に増加。ちなみに、Natureなどの多くの主要ジャーナルが、preprintサーバへ事前に投稿することを許可しているので、BioRxivへの投稿はいわゆる二重投稿に引っかかりません)。

また、BioRxivへの投稿が増えている背景には、オープンイノベーションを推進するという目的だけでなく、論文のPriorityを確保することや研究の宣伝を行う目的なども相まってのことだと考えられます。

とにかく、これからのサイエンスでは最新の研究動向を理解する上で、BioRxivの論文をフォローしていくことはますます重要になってくるでしょう。しかしながら、その論文の本数が多すぎます…。

直近で、preprintサーバに1ヶ月あたり1673本の論文が登録されています(1日あたり約50〜60本)。今後、さらに数が伸びることが予想されます。これを人力で全て確認するのは不可能です…。 image.png (218.9 kB) 元データはこちら。
http://www.prepubmed.org/monthly_stats/

BioRxivに登録されている論文はとにかく玉石混交なので、注目されている論文だけを抽出することはできないか考えました。そんな中で見つけたのが、こちらの記事です。

A Twitter bot to find the most interesting bioRxiv preprints
https://gigabaseorgigabyte.wordpress.com/2017/08/08/a-twitter-bot-to-find-the-most-interesting-biorxiv-preprints/

こちらの記事では、 Altmetric scoreという指標(論文の注目度を数値化したもの)を用いて、BioRxivに投稿された論文の中で、特に注目度の高い記事を抽出し、Tweetするという仕組みを作っていました。

ちなみに、Altmetric scoreとは、TwitterFacebookなどのSNSやNews、Mendeleyへの登録数などの様々な指標を元に算出した論文の注目度を示すスコアです(詳しく解析したわけではないですが、個人的な印象として、BioRxivに登録されている論文のAltmetric scoreは、Tweet数に引っ張られている印象を受けましたが…)。

上記の記事で紹介されているスクリプトをそのまま使うのも手でしたが、後々PubMedに登録されている論文に対しても応用したかったのと、データのストアをSQLデータベースではなく、Pickleで管理しているなど、実装部分で個人的に作り変えたい部分があったので、1から自分で作ってみることにしました。

そんなわけで、上記の記事を参考に、Raspberry Pi上で動作するBioRxivのキュレーションシステムを作って見たというのが今回の内容になります。

サンプル

SNSへのメッセージ出力として、SlackとTwitterに対応させました(現状、個人的に興味のある「Genomics」と「Bioinformatics」の分野に関する論文しかキュレーションの対象にしていません)。

サンプルのTwitter botはこちらになります。
https://twitter.com/BioRxivCurator

image.png (546.9 kB)

ソースコード

BioRxivCurator
https://github.com/Imamachi-n/BioRxivCurator

内容

全体像

システム全体の流れは以下の通りです(内部的なデータ処理の流れとはちょっと違いますが、概略図ということで解釈してもらえば良いです)。 image.png (218.7 kB)

RSS feedで取得した論文データについて、Altmetric scoreを取得し、データをSQLiteデータベースに格納しておきます。Altmetric scoreが一定以上の論文(注目度の高い論文)について、定期的にTwitterやSlackにメッセージを配信します。

また、一定のスコアに達しない論文については、情報をデータベースに保持し続けておき、定期的にAltmetric scoreを確認します。データの保持期限は1ヶ月とします(多くの場合、1ヶ月以降はあまりAltmeric scoreが変動しない傾向にあるため)。それを過ぎた論文データは破棄します(厳密には、フラグ管理により論理削除する形をとります)。

Pythonで実装し、プログラム自体はRaspberry Pi3 ModelB上で動かします。手作業でプログラムを動かすのは面倒なので、ジョブスケジューラであるcronを使って毎日論文を自動でチェックする形にしました。 image.png (171.0 kB) https://commons.wikimedia.org/w/index.php?curid=47497384

Pythonによる実装

まず、Pythonによる実装例を見ていきます。かいつまんで重要な箇所を説明しているだけなので、詳しくは上述のソースコードをご覧ください。

RSS feedの取得

BioRxivのRSS feedはこちらから取得できます。直近の30 articlesを取得できます。
https://www.biorxiv.org/alertsrss

論文の絞り込みとしては、
http://connect.biorxiv.org/biorxiv_xml.php?subject=all
で全体から取得するか、
http://connect.biorxiv.org/biorxiv_xml.php?subject=キーワード
で特定の分野の論文のみを取得する方法があります。

後者の場合、+でつなげて
http://connect.biorxiv.org/biorxiv_xml.php?subject=genomics+bioinformatics
のように複数の分野からRSSを取得することも可能です。

PythonRSS feedから得たデータをパースするために、今回はfeedparserライブラリを使いました。

import feedparser

feed = feedparser.parse(
        "http://connect.biorxiv.org/biorxiv_xml.php?subject={0}".format("+".join(subjects)))

 for pub in feed["items"]:
    <処理を書く>
     # pub["dc_identifier"] # DOI
     # pub["title"]          # 論文のタイトル
     # pub["link"].split('?')[0]  # 論文のURL
     # pub["updated"]        # 更新日

以上のように、feedparser.parse()に先ほど取得したRSS feedのURLを渡すことで、ディクショナリ型にパースされたRSSデータを取得できます。

各論文のデータは、itemsごとに格納されているので、Forループで各論文の情報を取得します。例えば、dc_identifierにはDOI、titleには論文のタイトル、linkには論文のリンク、updatedには更新日が格納されています。DOIはAltmetric scoreの取得の際に必要となるので、必ず取得しておきます。

実際にどんなデータが入っているか確認したい場合は、RSS feed URLに直接アクセスして、中身のXMLファイルを覗いてみるといいです。 ブラウザによって見え方が異なります。以下の例では、Google Chromeを使って表示しています。 image.png (389.8 kB)

Altmetric scoreの取得

RSS feedから取得したDOIを使って、Altmetric scoreを取得します。 Altmetric scoreの取得には、AltmetricのAPIを使用します。
https://api.altmetric.com/

ソースコード中では、Altmetric APIの薄いラッパークラスを作っていますが、要はAltmetric scoreの取得先URLに対してGETしてるだけです。

import requests
import json

# GET request
doi = "10.1038/480426a"
request = requests.get("https://api.altmetric.com/v1/doi/{0}".format(doi))
response = json.loads(request.text)
# response["context"]['journal']['pct']    # Altmetric scoreを「0〜100」に正規化した値。
# response["score"]            # Altmetric score

Altmetric APIは、
リクエストURL: https://api.altmetric.com/v1/doi/DOI
に対してGETすると、JSON形式でデータを返してくれます。

そこで、JSON形式のデータをjson.load()を使ってディクショナリ型に変換します。 あとは、キーを指定して欲しいデータを取り出すだけです。

こちらも、実際にどんなデータが入っているか確認したい場合は、リクエストURLに直接アクセスして、中身のJSONファイルを覗いてみるといいです。
ブラウザによって見え方が異なります。以下の例では、Firefoxを使って表示しています。Firefoxではこのように、 JSON形式のデータを見やすく表示してくれます。 image.png (298.8 kB) 今回は、赤枠で囲った値を取得しています。

SQLiteデータベースへの格納

順番は前後しますが、ここでまとめてSQLiteデータベースの処理を説明します。

1. テーブルの作成(CREATE TABLE

まず、テーブルを作成します。 ここでは、CREATE TABLE IF NOT EXISTSとすることで、システムの初回起動時にのみテーブルが作成される仕組みになっています。 また、論文のDOIをprimary keyに指定します(各論文に対してユニークな文字列が割り振られるため)。

import sqlite3

sqlite3_file = "./storeAltmetrics.sqlite3"
with sqlite3.connect(sqlite3_file) as conn:
        c = conn.cursor()

        # Create biorxiv_altmetrics_log table
        sql = """CREATE TABLE IF NOT EXISTS biorxiv_altmetrics_log
                (doi TEXT,
                 title TEXT,
                 link TEXT,
                 update_date TEXT,
                 altmetric_score INTEGER,
                 altmetric_pct INTEGER,
                 altmetric_flg INTEGER,
                 PRIMARY KEY(doi)
                )"""
        c.execute(sql)
        conn.commit()

基本的に、Javaなどの言語と書き方は似てます。 sqlite3.connect()でデータベースに接続します。ここでは、sqlite3_fileが格納先のデータベースのファイルだとします。 続いて、cursorオブジェクトを作成し、execute()メソッドでSQL文を実行します。最後に、変更点を保存するために、Connectionオブジェクトのcommit()メソッドを実行します(コミット)。

テーブルのフィールドについてです。 RSS feedから取得したデータをdoi, title, link, update_dateにそれぞれ格納します。 また、Altmetric APIから取得したスコアを、altmetric_score, altmetric_pctにそれぞれ格納します。

フラグ 意味
0 PCTが90未満
1 PCTが90以上
-1 PCTが90未満で1ヶ月が経過(解析対象から除外)
2. RSS feedから得た論文データの格納(INSERT INTO

RSS feedから取得した論文のデータ(論文のタイトル、論文のURL、DOI、更新日)をデータベースのテーブルに格納します。 ここでは、INSERT OR IGNORE INTOとすることで、テーブルに格納されていない論文についてのみ登録する形になっています。

with sqlite3.connect(sqlite3_file) as conn:
            c = conn.cursor()

            # Insert article info into biorxiv_altmetrics_log if not already exists
            sql = """INSERT OR IGNORE INTO biorxiv_altmetrics_log
                     VALUES(?,?,?,?,?,?,?)"""
            doi_info = [tuple([p.doi, p.title, p.url, p.date, 0, 0, 0])
                        for p in RSS_data_list]
            c.executemany(sql, doi_info)
            conn.commit()

ここでは、RSS_dataクラスのインスタンスRSS_data_listに論文のデータが格納されていることを想定しています。 データを渡す際には、タプル型である必要があります。

("value1,")
("value1", "value2", "value3")

また、タプル型のデータのリストを作成し、cursorオブジェクトのexecutemany()メソッドを使うことで、一気に複数のレコードをテーブルに格納することができます。最後は、connectionオブジェクトのcommit()メソッドを使って、変更点をコミットします。

ちなみに、突然出てきたRSS_data_listですが、下記の通りに定義しています。

class RSS_data(object):
    def __init__(self, doi, title, url, date):
        self.doi = doi
        self.title = title
        self.url = url
        self.date = date

RSS_data(doi=pub["dc_identifier"],
         title=pub["title"],
         url=pub["link"].split('?')[0],
         date=pub["updated"]))

またここでは、pubは上述したfeedparser.parse()メソッドで取得したRSS feedから得たデータをディクショナリ型で格納したオブジェクトだとします。

3. Altmeric scoreを取得する論文データの抽出(SELECT

続いて、過去に取得した論文データを含めて、一定のAltmetric scoreを超えていない論文のリストを取得します。
先ほど説明した通り、altmetric_flgが「0」である論文がAltmetric scoreを検索する対象となります。

with sqlite3.connect(sqlite3_file) as conn:
            c = conn.cursor()

            # Select target doi from biorxiv_altmetrics_log
            sql = """SELECT doi, title, link, update_date from biorxiv_altmetrics_log
                     WHERE altmetric_flg = 0"""
            c.execute(sql)

            # Store doi data as target_doi_data object
            target_doi_list = []
            for doi_info in c.fetchall():
                target_doi_list.append(target_doi_data(
                    doi=doi_info[0], title=doi_info[1], url=doi_info[2], date=doi_info[3]))

cursorオブジェクトのexecute()メソッドでSQL文を実行した後、検索した結果はcursorオブジェクトに格納されます。fetchall()メソッドを使うことで、条件に一致した全てのレコードをリストとして取得できます。

class target_doi_data(object):
    def __init__(self, doi, title, url, date):
        self.doi = doi
        self.title = title
        self.url = url
        self.date = date

取得したレコードの情報は、上記target_doi_dataオブジェクトのリストとして格納しておきます。

4. Altmetric scoreの値の格納(UPDATE

最後に、Altmetric scoreの更新を行います。APIから取得したスコアをテーブルに書き込みます。最後に忘れずに、変更をコミットします。

with sqlite3.connect(sqlite3_file) as conn:
            c = conn.cursor()

            # insert altmetric score into biorxiv_altmetrics_log
            sql = """UPDATE biorxiv_altmetrics_log
                     SET altmetric_score = ?,
                          altmetric_pct = ?,
                          altmetric_flg = ?
                     WHERE doi = ?"""
            c.execute(sql, tuple([altmetrics_data.altmetric_score,
                                  altmetrics_data.pct, altmetrics_data.flg,
                                  doi]))
            conn.commit()

Slackのアクセストークンを取得する

SlackのAPIを介してデータのやり取りをします。そのため、Slackのアクセストークンを取得する必要があります。以下ではその取得方法について説明します。

基本的に以下の記事を参考にしました。
https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21

1. アプリの登録

まず、以下のURLにアクセスします。Slackにログインしていない場合、ログインしてください。
https://api.slack.com/apps

Create an Appをクリックします。 image.png (250.5 kB)

Create a Slack Appという画面が出てくるので、
App Name: 好きな名前を入力する。
Development Slack Workspace: Slackのメッセージを流したいワークスペースを選択。
を入力して、Create Appをクリックします。 image.png (194.2 kB)

2. スコープの設定

登録したアプリの権限の範囲を設定します。 Install your app to your workspaceを開くと、権限のスコープを設定してくださいとメッセージが出ているので、permission scopeをクリックします。 image.png (280.0 kB)

今回は、Slackへメッセージを送るだけなので、ChatのSend messages as xxxxxxを選択します。 image.png (233.3 kB)

すると、選択したPermission Scopeが表示されます。
状態を保存するために、Save Changesを必ずクリックしてください。 image.png (181.5 kB)

3. アプリをSlackにインストールする

最後に、アプリをSlackにインストールします。 image.png (168.1 kB)

先ほど設定したメッセージを送る権限を付与していいか聞かれるので、Authrizeをクリックします。 image.png (81.0 kB)

インストールが完了すると、OAuth Access Tokenを取得できます。 この文字列を使うので、コピーして保存しておきます。 image.png (119.7 kB)

Slackへのメッセージ配信

Slackへのメッセージ送信は、公式のPythonライブラリであるSlackClientを使います。
https://slackapi.github.io/python-slackclient/basic_usage.html#sending-a-message

使い方は簡単で、先ほど取得したSlackのアクセストークslack_tokenを指定し、spi_call()メソッドに、送り先のチャンネルchannelと送信するテキストtextを指定するだけです。エラーハンドリングについては、上記のURLの記事を参照してください。

slack_token = "xxxxxxxx"
channel = "@imamachi"
text = "Hello World !!"

sc = SlackClient(slack_token)
    response = sc.api_call(
        "chat.postMessage",
        channel=channel,
        text=message
    )

Twitterのアクセストークンを取得する

Slack同様、TwitterAPIを介してデータのやり取りをします。そのため、Twitterについてもアクセストークンを取得する必要がありmす。以下ではその取得方法について説明します。

1. Twitterアプリの作成

まず、Twitter Application Managementにアクセスします。アカウントにログインしていない場合は、ツイート対象となるアカウントにログインしてください。
https://apps.twitter.com/

Create New Appをクリックします。 image.png (84.9 kB)

Name: アプリ名を記載する。
Description: アプリの説明を記載する。
Website: ソースコードやアプリが入手できるURLを入力。
Create your Twitter applicationをクリックし、アプリを作ります。 image.png (343.5 kB)

image.png (55.1 kB)

すると、次のようなページに遷移します。 image.png (223.4 kB)

2. アクセストークンの取得

Keys and Access Tokensタブをクリックし、 Consumer Key (API Key)Consumer Secret (API Secret)を確認します。 これらはAPIでのやり取りに必要となるので、コピーして保存しておきます。 image.png (318.7 kB)

続いて、Your Access TokenでCreate my access tokenをクリックします。 image.png (65.5 kB)

Access TokenAccess Token Secretを取得します。 これらも同様に、APIでのやり取りに必要となるので、コピーして保存しておきます。 image.png (85.4 kB)

Twitterへのメッセージ配信

Twitterへのツイートは、tweepyというライブラリを使います。 こちらも使い方は非常に簡単で、先ほど取得した各種の値を設定し、メッセージをupdate_status()メソッドに指定するだけです。

こちらも参照。
https://review-of-my-life.blogspot.jp/2017/07/python-cloud9-tweepy.html

consumer_key = "xxxxxxxx"
consumer_secret = "xxxxxxxx"
access_token = "xxxxxxxx"
access_token_secret = "xxxxxxxx"
message = "Hello World !!"

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_secret)
api = tweepy.API(auth)
api.update_status(message)

追記: 余談ですが、TweepyはメインのContributorがいなくなり、他のmaintainerに引き継がれています。アップデートが最近されていないので、TwitterAPIの仕様変更で動かなくなるリスクがあります。
https://qiita.com/utgwkk/items/4beed333e8262c675028

Raspberry Pi3の用意

以下のサイトを参考にRaspberry Pi3のOSをインストールしたMicro SDカードを用意します。
https://getpocket.com/redirect?url=http%3A%2F%2Fhirazakura.hatenablog.com%2Fentry%2Fraspberrypi%2Fsetup%2Ffirst&formCheck=3f0b1d005779f9a1ac22bfac92159bea

続いて、アップデートをかけて、リブートします(再起動)。

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo rpi-update
$ sudo reboot

テキストエディタが入っていないので、VSCodeをインストールします。
https://code.headmelted.com/ 管理者権限でないとインストールできないので注意してください。

$ wget -O - https://code.headmelted.com/installers/apt.sh 
$ chmod 755 apt.sh
$ sudo apt.sh

以下でVisual Studio Codeを実行できます。

$ code-oss

SQLiteのクライアントアプリをインストール

SQLiteデータベースをGUIで見たい場合、以下のクライアントアプリがオススメです。
http://sqlitebrowser.org/

Raspberry Pi上では、以下のコマンドでインストール可能です。

$ sudo apt-get install sqlitebrowser

cronの設定

Raspberry Piで作成したPythonスクリプトを定期的に自動で実行するために、cronを使います。 cronはUnix OS系に標準で搭載されているジョブスケジューリングのプログラムです。

cronについての記事はこちらを参照のこと。
https://qiita.com/hikouki/items/e744b3a4d356d2af12cf

ここでは、簡単な設定例を示したいと思います。 まず、cronのconfigureファイルcron.confを作成します。 スペース区切りで日時を指定、最後に実行コマンドを指定します。 行の最後に改行が必要なので、入れ忘れないように。

分 時 日 月 曜日 <実行コマンド> [改行]

あと、念のため、エラーになった時にエラーログを確認できるように、標準出力でエラーログを取っておきます。

0 11 * * * bash /home/pi/Desktop/BioRxivCurator/src/startup4RaspberryPi.sh > /home/pi/Desktop/error.txt 2>&1

cronのconfigureファイルを作成したら、以下のコマンドを実行します。

$ crontab ./cron.conf

設定内容を確認したい場合は、

$ crontab -e

で設定内容を確認・編集できます。

間違って登録してしまった場合、

$ crontab -r

で全ての設定を削除することができます(このコマンドの実行には注意が必要です)。

cron使用時の注意点

だいたいのエラーは、上記pythonへのパスに起因した問題です。

相対パスに注意

cronが実行するコマンドは、実行ユーザーのホームディレクトリで実行されます。

特に今回の場合、pythonのコードの中に相対パスを設定している箇所があるので、実行コマンド中で、cdでカレントディレクトリをソースコードの置いてあるディレクトリに移動しておきます。

$ cd /home/pi/Desktop/BioRxivCurator/src

Minicondaを使用している場合

python絶対パスで指定する必要があります。そうしないと、cronは標準でインストールされているpythonを使おうとします。

/home/pi/miniconda3/bin/python /.main.py --yaml_setting_file ./production.yaml

原因がわからないエラーの場合

cronで実行するコマンドの最後に、エラー出力先を指定しておくと良いです。

0 11 * * * bash /home/pi/Desktop/BioRxivCurator/src/startup.sh > /home/pi/Desktop/error.txt 2>&1

こうすることで、指定したファイルerror.txtにエラーログが出力される。

また、/var/log/syslog内にもcron実行時のログが残るので、そちらも参照すると原因の特定につながります。

cron.confが登録できない

コマンドの打ち間違え(こんなミスをするのは私だけかもしれませんが…)。

$ cron ./cron.conf
cron: can't lock /var/run/crond.pid, otherpid may be 3505: Resource temporarily unavailable

正しくは、cronではなくcrontabです。

$ crontab ./cron.conf

どうしようもなくなったら、とりあえず、リブート(再起動)してみましょう。

$ reboot