bioRxivでバズってる論文を抽出してSNSにメッセージを流してみた
きっかけ
近年、NatureやCell, Scienceに論文が投稿される前に、preprintサーバであるBioRxivに投稿する流れが増えてきています(2015年以降、急激に増加。ちなみに、Natureなどの多くの主要ジャーナルが、preprintサーバへ事前に投稿することを許可しているので、BioRxivへの投稿はいわゆる二重投稿に引っかかりません)。
また、BioRxivへの投稿が増えている背景には、オープンイノベーションを推進するという目的だけでなく、論文のPriorityを確保することや研究の宣伝を行う目的なども相まってのことだと考えられます。
とにかく、これからのサイエンスでは最新の研究動向を理解する上で、BioRxivの論文をフォローしていくことはますます重要になってくるでしょう。しかしながら、その論文の本数が多すぎます…。
直近で、preprintサーバに1ヶ月あたり1673本の論文が登録されています(1日あたり約50〜60本)。今後、さらに数が伸びることが予想されます。これを人力で全て確認するのは不可能です…。
元データはこちら。
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とは、TwitterやFacebookなどの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
ソースコード
BioRxivCurator
https://github.com/Imamachi-n/BioRxivCurator
内容
全体像
システム全体の流れは以下の通りです(内部的なデータ処理の流れとはちょっと違いますが、概略図ということで解釈してもらえば良いです)。
RSS feedで取得した論文データについて、Altmetric scoreを取得し、データをSQLiteデータベースに格納しておきます。Altmetric scoreが一定以上の論文(注目度の高い論文)について、定期的にTwitterやSlackにメッセージを配信します。
また、一定のスコアに達しない論文については、情報をデータベースに保持し続けておき、定期的にAltmetric scoreを確認します。データの保持期限は1ヶ月とします(多くの場合、1ヶ月以降はあまりAltmeric scoreが変動しない傾向にあるため)。それを過ぎた論文データは破棄します(厳密には、フラグ管理により論理削除する形をとります)。
Pythonで実装し、プログラム自体はRaspberry Pi3 ModelB上で動かします。手作業でプログラムを動かすのは面倒なので、ジョブスケジューラであるcronを使って毎日論文を自動でチェックする形にしました。 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を取得することも可能です。
PythonでRSS 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を使って表示しています。
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形式のデータを見やすく表示してくれます。
今回は、赤枠で囲った値を取得しています。
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をクリックします。
Create a Slack Appという画面が出てくるので、
App Name: 好きな名前を入力する。
Development Slack Workspace: Slackのメッセージを流したいワークスペースを選択。
を入力して、Create Appをクリックします。
2. スコープの設定
登録したアプリの権限の範囲を設定します。 Install your app to your workspaceを開くと、権限のスコープを設定してくださいとメッセージが出ているので、permission scopeをクリックします。
今回は、Slackへメッセージを送るだけなので、ChatのSend messages as xxxxxxを選択します。
すると、選択したPermission Scopeが表示されます。
状態を保存するために、Save Changesを必ずクリックしてください。
3. アプリをSlackにインストールする
最後に、アプリをSlackにインストールします。
先ほど設定したメッセージを送る権限を付与していいか聞かれるので、Authrizeをクリックします。
インストールが完了すると、OAuth Access Tokenを取得できます。 この文字列を使うので、コピーして保存しておきます。
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同様、TwitterのAPIを介してデータのやり取りをします。そのため、Twitterについてもアクセストークンを取得する必要がありmす。以下ではその取得方法について説明します。
1. Twitterアプリの作成
まず、Twitter Application Managementにアクセスします。アカウントにログインしていない場合は、ツイート対象となるアカウントにログインしてください。
https://apps.twitter.com/
Create New Appをクリックします。
Name: アプリ名を記載する。
Description: アプリの説明を記載する。
Website: ソースコードやアプリが入手できるURLを入力。
Create your Twitter application
をクリックし、アプリを作ります。
すると、次のようなページに遷移します。
2. アクセストークンの取得
Keys and Access Tokensタブをクリックし、 Consumer Key (API Key)とConsumer Secret (API Secret)を確認します。 これらはAPIでのやり取りに必要となるので、コピーして保存しておきます。
続いて、Your Access TokenでCreate my access tokenをクリックします。
Access TokenとAccess Token Secretを取得します。 これらも同様に、APIでのやり取りに必要となるので、コピーして保存しておきます。
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に引き継がれています。アップデートが最近されていないので、TwitterのAPIの仕様変更で動かなくなるリスクがあります。
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