arXivの論文を爆速で検索し、読む環境をPythonだけで構築する

# はじめに
こんにちは！ 
Iwaken Lab.のいっちー( https://twitter.com/P_ichikura )と申します。

「3Dスキャンを用いた文化や歴史の保存」をテーマに3Dスキャン旅をやっています。
とはいえ、一方では筑波大学の大学院生（博士後期課程在籍）をしており、旅先で研究をすることもしばしばです。

理系大学院生の皆様はすでにたくさんの論文を読んでいると思いますが、論文を調べることはかなり手間になりますよね。そのうえで概要をいまいち理解していない論文を読むことも精神的にはかなり辛いです。

そこで、自分が研究をするうえで毎月キャッチアップをしている方法の一部をご紹介しようと思います。

# 実装内容について
今回はarXivという「プレプリント」を読むことができるプラットフォームのAPIを活用して、該当する論文の検索および概要を作成する方法を紹介します。

**「プリプリント」とは？**
　調査対象分野や目的に応じて、適切に学術文献において査読前の段階で、オープンアクセスできるプラットフォームに登録・公開される論文原稿のことを指します。

# 開発環境について
本環境は研究用PCとして普段使いしている環境がWindows 11で、強いグラフィックボードも搭載していないので、以下の環境で実装しています。

---
+ Windows 11
+ Miniconda
    + Python 3.10.0
 
使用するモジュール
+  argparse
+　feedparser
+　openai
+　arxiv（API接続のため、検討した）  
+　その他　Pythonの標準モジュール　
---

# 開発手順
## 開発環境の構築
1. minicondaをインストールする。
以下のURLからwindows11をダウンロードしましょう。
https://docs.conda.io/projects/miniconda/en/latest/ 
2. Anaconda Prompt(miniconda)を開き、任意の作業ディレクトリに移動する。
3. functionという関数を格納するフォルダーを作成する。
    ```
    $ mkdir function
    ```
4. Visualstudio Codeを開く。
    ```
    $ code .
    ```

## 関数の設定
### 引数の設定
+ コマンドラインで引数を設定するために関数を設定します。
今回は、検索キーワードを意味する"context"、検索数を意味する"number"という強制的な引数のみを使用しています。
+ 関数を記載するファイルは".\function\def_argument.py"です。

    ```
    import sys
    import argparse
    
    def get_args():
        # オブジェクト生成
        parser = argparse.ArgumentParser()
    
        # 引数設定
        if sys.stdin.isatty():
            parser.add_argument("context", help="please set me", type=str)
            parser.add_argument("number", help="please set me", type=int)
    
        # 結果を受ける
        args = parser.parse_args()
    
        return(args)
    
    def main():
        args = get_args()
    
        print(args.context)
        print(args.number)
    
    if __name__ == '__main__':
        main()
    
    ```

### arXivを用いた論文検索
+ arXiv APIを用いて、検索を行います。先ほど作成した検索用のキーボードを参照する形にしています。
ここではarXiv APIの細かい設定は説明しないので、興味がある方は公式ページを見てください。
+ 関数を記載するファイルは".\function\search_papers.py"です。
 
    ```
    from def_argument import get_args
    import os
    import feedparser
    import datetime
    
    def search_feedparser(args):
        # ベースとなるURLを作成
        base_url = "https://export.arxiv.org/api/query?search_query="
        # クエリを指定
        query = "cat:" + args.context
        # 検索件数
        max_results = args.number
        # 結果を新しいものから順にソートする
        sort_results = "&sortBy=lastUpdatedDate&sortOrder=descending&max_results={max_results}".format(max_results=max_results)
    
        d = feedparser.parse(base_url + query + sort_results)
    
        # entriesに検索結果が格納されている
        for entry in d.entries:
            # id
            # 例: 'http://arxiv.org/abs/1909.07581v2' ← ID + v2 みたいに"vn"としてバージョン情報がついているので"v"以前を取り出す
            arxiv_id = os.path.basename(entry.id).split("v")[0]
            # タイトル
            title = entry.title
            # リンクURL
            link = entry.link
            # サマリー. 改行を削除
            summary = entry.summary.replace("\n", "")
            # 第一版提出日時
            # time.struct_time 形式で保存されているのでdatetime形式に変更（https://cortyuming.hateblo.jp/entry/20100919/p1）
            published = datetime(*entry.published_parsed[:6])
            # 更新日時
            updated = datetime(*entry.updated_parsed[:6])
            # バージョン
            version = int(link[-1])
            # 著者
            authors = dict(authors=[author["name"] for author in entry.authors])
            # カテゴリー
            categories = dict(categories=[tags["term"] for tags in entry.tags])
            # コメント
            # コメントがない場合もあるので、try~exceptで処理
            try:
                comment = entry.arxiv_comment
            except AttributeError:
                comment = None
    
    def download_pdf(all_results):
        # PDFのダウンロード
        for r in all_results:
            paper = next(arxiv.Client().results(arxiv.Search(id_list=["9201301v1"])))
            # Download the PDF to a specified directory with a custom filename.
            paper.download_pdf(dirpath="./data", filename= r.id + ".pdf")
    ```


### OpenAI APIを用いた論文の概要を作成する
+ OpenAI APIを用いて論文の概要を作成していきます。
+ 関数を記載するファイルは".\function\def_abstruct.py"です。
 
    ```
    import os
    import arxiv
    from openai import OpenAI
    import random
    
    #OpenAIのapiキー
    os.environ["OPENAI_API_KEY"] = '<OpenAI API Key>'
    
    def get_summary(result):
        system = """与えられた論文の要点を3点のみでまとめ、以下のフォーマットで日本語で出力してください。```
        タイトルの日本語訳
        ・要点1
        ・要点2
        ・要点3
        ```"""
    
        text = f"title: {result.title}\nbody: {result.summary}"
        # クライアントの準備
        client = OpenAI()
        # arxiv APIで最新の論文情報を取得する
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {'role': 'system', 'content': system},
                {'role': 'user', 'content': text}
            ],
            temperature=0.25,
        )
        summary = response['choices'][0]['message']['content']
        title_en = result.title
        title, *body = summary.split('\n')
        body = '\n'.join(body)
        date_str = result.published.strftime("%Y-%m-%d %H:%M:%S")
        message = f"発行日: {date_str}\n{result.entry_id}\n{title_en}\n{title}\n{body}\n"
    
        return message
    
    def main():
        #queryを用意、今回は、三種類のqueryを用意
        query ='ti:%22 Deep Learning %22'
    
        search = arxiv.Search(
            query=query,  # 検索クエリ（
            max_results=10,  # 取得する論文数
            sort_by=arxiv.SortCriterion.SubmittedDate,  # 論文を投稿された日付でソートする
            sort_order=arxiv.SortOrder.Descending,  # 新しい論文から順に取得する
        )
    
        #searchの結果をリストに格納
        result_list = []
        for result in search.results():
            result_list.append(result)
        print("result_list: \n", result_list)
        #ランダムにnum_papersの数だけ選ぶ
        num_papers = 3
        results = random.sample(result_list, k=num_papers)
    
        # 論文情報を表示する
        for i,result in enumerate(results):
            message = "今日の論文です！ " + str(i+1) + "本目\n" + get_summary(result)
            print("messege:", message)
    
    if __name__ == "__main__":
        main()
    ```


## 関数を統合する
+ main.pyというPythonファイルを現在のディレクトリで作り、統合します。
コードは下記に記載します。
    ```
    from function.def_argument import get_args
    from function.search_papers import search_papers, search_feedparser
    from function.def_abstruct import get_summary
    
    def main():
        # 引数の定義
        args = get_args()
        # 表示
        print(args.context)
        print(args.number)
        results = search_feedparser(args=args)
        # 論文情報を表示する
        for i,result in enumerate(results):
            message = "今日の論文です！ " + str(i+1) + "本目\n" + get_summary(result=result)
            print("messege:", message)
    
    if __name__ == "__main__":
        main()
    
    ```

+ コマンドラインで実行できるので、以下のようなコマンドで実行しましょう。
例として、検索キーワードに"AI"、検索数に10と設定しています。  
    ```
    $ python .\main.py "AI" 10
    ```
 

# 今後の更新について
+ Streamlit というWebアプリケーションでの実装
直感的な操作を可能にするために検討している実装です 
+ arXiv APIの不調
ネットワーク環境の影響もあると思いますが、環境を長時間運用するとarXiv APIにアクセスができなくなるという不調が発生しているので、ネットワーク環境および実装の問題を更新したいと思っています。

# 参考サイト
- ライブラリの公式サイト
    [arxiv](https://pypi.org/project/arxiv/)


- 引数の設定
    [pythonでコマンドライン引数を使うサンプル - Qiita](https://qiita.com/stkdev/items/e262dada7b68ea91aa0c)
    

- 最新の論文をChatGPTで要約して毎朝Slackに共有してくれるbotを作る！
    [最新の論文をChatGPTで要約して毎朝Slackに共有してくれるbotを作る！](https://zenn.dev/ozushi/articles/ebe3f47bf50a86)


- 検索の引数設定を確認するためのAPI keyのManual
    [arXiv API User's Manual - arXiv info](https://info.arxiv.org/help/api/user-manual.html#subject_classifications)


# 最後に
最後までお読みいただきありがとうございました！
引き続き、来年は記事を備忘録的に書いていきたいですね！
良いクリスマス、良いお年を～