【自動化】arXivの新着論文をキーワード検索して、解説をGeminiで生成後、メール配信

##  毎日arXivを調べて論文を読むのは面倒

物理学や数学などの分野では毎日多くの論文がプレプリントサーバ、arXivに投稿されていますね。
みなさん毎日のようにチュックして自身の研究分野の動向を調べ、関心のある論文を読んでいることと思います。
**研究に必要なのはわかっているけれども、やはりチェックして読むのは面倒くさい！** 日本語ならまだしも英語だし、ChatGPTやGeminiに放り込んで和訳させるのもいちいちするのはちょっと手間ですね。
そこでarXivとGeminiのAPIを使ってこれを自動化してしまおう、と考えました。

## 欲しい機能

まずは何が欲しいか考えましょう。
1. 指定したキーワードを含んだ論文を探す
1. 複数カテゴリを検索する（人によってはいらない）
1. LLM (Gemini) を使って見つかった論文の解説を生成
1. 毎日自分宛にメールを送信

ひとまずはこのくらいでしょう。

以下、コードの解説をしていきますが、かなり冗長な気がしています。無駄なところや冗長なところに気づいたら是非是非お教えください。

## 最終成果物

コピペして使いたい人のために初めに最終成果物を掲示しておきます。
コードの詳細な説明などはコードの後ろに続きます。


<details><summary> <b> 最終成果物 </b> </summary> 

```python: arxiv_alerter.py
# -*- coding: utf-8 -*-
"""
arXiv Keyword Alerter

This script searches for new arXiv papers matching specific keywords (with OR logic)
across multiple categories, generates a summary using the Gemini API, and sends an email notification.
It's designed to be run automatically.
"""
import os
import smtplib
import ssl
from email.mime.text import MIMEText
from email.header import Header
from datetime import datetime, timedelta, timezone
import time
import requests
import xml.etree.ElementTree as ET
import google.generativeai as genai
from dotenv import load_dotenv
from bs4 import BeautifulSoup
# --- 0. SETUP ---
# .envファイルから環境変数を読み込む (ローカルテスト用)
load_dotenv(dotenv_path='config.env')

# --- 1. CONFIGURATION LOADING ---
def get_env_var(var_name):
    """
    環境変数を取得する。見つからない場合はエラーメッセージを表示して終了する。
    """
    value = os.environ.get(var_name)
    if value is None:
        print(f"エラー: 必須の環境変数 '{var_name}' が設定されていません。")
        exit(1)
    return value


# arXiv用
SEARCH_KEYWORDS = get_env_var("SEARCH_KEYWORDS")
SEARCH_CATEGORY = get_env_var("SEARCH_CATEGORY")

# Gemini API用
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")

# メール送信用
SMTP_SERVER = get_env_var("SMTP_SERVER")
SMTP_PORT = int(get_env_var("SMTP_PORT"))
SMTP_USER = get_env_var("SMTP_USER")
SMTP_PASSWORD = get_env_var("SMTP_PASSWORD") 
MAIL_FROM = get_env_var("MAIL_FROM")
MAIL_TO = get_env_var("MAIL_TO")
MAIL_SUBJECT = get_env_var("MAIL_SUBJECT")


# --- 2. GEMINI API SETUP ---
if GEMINI_API_KEY:
    try:
        genai.configure(api_key=GEMINI_API_KEY)
        print("Gemini APIキーが正常に設定されました。")
    except Exception as e:
        print(f"Gemini APIキーの設定中にエラー: {e}")
        GEMINI_API_KEY = None
else:
    print("警告: 環境変数 'GEMINI_API_KEY' が設定されていません。")


def generate_summary_with_gemini(paper_info, full_text):
    """Geminiを使用して論文の概要を生成する。"""
    if not GEMINI_API_KEY:
        return "（Geminiによる解説はスキップされました：APIキーが未設定です）"

    model = genai.GenerativeModel('gemini-2.0-flash-lite')

    prompt = f"""以下のarXiv論文について、内容を専門外の人が読んでも理解できるように、重要なポイントを箇条書きで分かりやすく解説してください。

# 論文情報
- **タイトル:** {paper_info['title']}
- **著者:** {", ".join(paper_info['authors'])}

# 論文本文（またはアブストラクト）
{full_text[:30000]}

# 解説のポイント
1. この研究が解決しようとしている問題点は何か？
2. どのような新しい手法やアプローチを用いたか？
3. この研究の最も重要な発見や結論は何か？
4. この発見が将来どのような影響を与える可能性があるか？

以上の点を踏まえて、日本語で解説を生成してください。
"""
    try:
        response = model.generate_content(prompt)
        time.sleep(1)
        return response.text
    except Exception as e:
        print(f"Gemini APIでの解説生成中にエラー: {e}")
        return f"（Geminiによる解説生成中にエラーが発生しました: {e}）"

# --- 3. ARXIV SEARCH & CONTENT FETCHING ---


def fetch_paper_full_text(html_url):
    """論文のHTMLページから本文テキストを抽出する。"""
    try:
        print(f"  > HTML版の本文を取得中: {html_url}")
        response = requests.get(html_url, timeout=30)
        response.raise_for_status()
        soup = BeautifulSoup(response.content, 'lxml')

        content_div = soup.find('div', class_='ltx_page_content')
        if content_div:
            for tag in content_div.find_all(['header', 'footer', 'nav']):
                tag.decompose()
            return content_div.get_text(separator=' ', strip=True)
        else:
            return soup.body.get_text(separator=' ', strip=True)

    except requests.RequestException as e:
        print(f"  > HTMLの取得に失敗: {e}")
        return None
    except Exception as e:
        print(f"  > HTMLの解析中にエラー: {e}")
        return None


def search_arxiv():
    """arXiv APIでキーワードに合致する過去24時間の論文を検索する。"""
    print(f"キーワード '{SEARCH_KEYWORDS}' で論文を検索中...")

    # タイトルとアブストラクトから検索するので、各キーワードを (ti:"..." OR abs:"...") の形式にする
    keywords = [
        f'(ti:"{k.strip()}" OR abs:"{k.strip()}")' for k in SEARCH_KEYWORDS.split(',')]
    # 各キーワード検索を "OR" で連結し、全体をカッコで囲む
    keyword_query = f"({' OR '.join(keywords)})"

    query = keyword_query

    if SEARCH_CATEGORY and SEARCH_CATEGORY.lower() != 'all':
        categories = [f'cat:{c.strip()}' for c in SEARCH_CATEGORY.split(',')]
        category_query = f"({' OR '.join(categories)})"
        query += f' AND {category_query}'

    params = {
        'search_query': query,
        'sortBy': 'submittedDate',
        'sortOrder': 'descending',
        'max_results': 25
    }

    try:
        response = requests.get(
            'http://export.arxiv.org/api/query?', params=params, timeout=30)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"arXiv APIへのリクエスト中にエラー: {e}")
        return []

    root = ET.fromstring(response.content)
    ns = {'atom': 'http://www.w3.org/2005/Atom'}

    found_papers = []
    yesterday = datetime.now(timezone.utc) - timedelta(days=1)

    for entry in root.findall('atom:entry', ns):
        published_str = entry.find('atom:published', ns).text
        published_dt = datetime.strptime(
            published_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)

        if published_dt > yesterday:
            arxiv_id = entry.find('atom:id', ns).text.split('/abs/')[-1]
            paper_info = {
                'id': arxiv_id,
                'title': entry.find('atom:title', ns).text.strip(),
                'summary': entry.find('atom:summary', ns).text.strip(),
                'authors': [author.find('atom:name', ns).text for author in entry.findall('atom:author', ns)],
                'link': entry.find('atom:id', ns).text,
                'html_link': f"https://arxiv.org/html/{arxiv_id}",
                'published': published_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
            }
            found_papers.append(paper_info)

    print(f"{len(found_papers)}件の新しい論文が見つかりました。")
    return found_papers

# --- 4. EMAIL NOTIFICATION ---


def send_email(subject, body):
    """メールを送信する。"""
    if not all([SMTP_SERVER, SMTP_USER, SMTP_PASSWORD, MAIL_FROM, MAIL_TO]):
        print("メール設定が不完全なため、送信をスキップします。")
        return

    print(f"{MAIL_TO} 宛にメールを送信中...")
    msg = MIMEText(body, 'plain', 'utf-8')
    msg['Subject'] = Header(subject, 'utf-8')
    msg['From'] = MAIL_FROM
    msg['To'] = MAIL_TO

    try:
        context = ssl.create_default_context()
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls(context=context)
            server.login(SMTP_USER, SMTP_PASSWORD)
            server.send_message(msg)
        print("メールが正常に送信されました。")
    except Exception as e:
        print(f"メール送信中にエラーが発生しました: {e}")

# --- 5. MAIN EXECUTION ---


def main():
    """メイン処理"""
    papers = search_arxiv()
    if not papers:
        print("処理対象の論文はありませんでした。")
        return

    full_email_body = f"キーワード「{SEARCH_KEYWORDS}」に関する新しい論文が {len(papers)}件 見つかりました。\n\n"

    for i, paper in enumerate(papers, 1):
        print(f"--- 論文 {i}/{len(papers)} の処理を開始: {paper['title']} ---")

        full_text = fetch_paper_full_text(paper['html_link'])

        if not full_text:
            print("  > HTML版の取得に失敗したため、アブストラクトを要約します。")
            full_text = paper['summary']

        summary_ja = generate_summary_with_gemini(paper, full_text)

        full_email_body += f"""
==================================================
論文 {i}: {paper['title']}
==================================================

著者: {", ".join(paper['authors'])}
投稿日: {paper['published']}
リンク: {paper['link']}

--- Geminiによる解説 ---
{summary_ja}
--------------------------

--- Original Abstract ---
{paper['summary']}
--------------------------

"""
    send_email(MAIL_SUBJECT, full_email_body)


if __name__ == "__main__":
    # beautifulsoup4とlxmlのインストールを促す
    try:
        from bs4 import BeautifulSoup
    except ImportError:
        print("エラー: 必要なライブラリがインストールされていません。")
        print("pip install beautifulsoup4 lxml python-dotenv")
        exit(1)
    main()

```

</details>

## 実装

機能1, 2 についてはarXivのAPIを使うだけですね。
機能3 はarXivの新しい機能であるHTML表示のページをAPI経由でGeminiに渡せば良さそうです。
機能4 にはGithub Actionsを使っていきましょう。

GeminiのAPIで使えるモデルはいくつもありますが、今回は制限が緩く、そこそこの性能が出るgemini-2.0-flash-liteを使っていきましょう。
2025年6月から gemini-2.5-flash-lite-preview-06-17 が使えるようになっているのでこっちを使っても良いかもしれません。なんなら gemini-2.5-flash でもレート制限にかからない程度な気がするのでこっちでも良いかもしれませんね。気になる方はどれも無料なのでローカルで実行して試してみましょう！


```python: arxiv_alerter.py 
# あとで使うモジュールたちもまとめて読み込む
import os
import smtplib
import ssl
from email.mime.text import MIMEText
from email.header import Header
from datetime import datetime, timedelta, timezone
import time
import requests
import xml.etree.ElementTree as ET
import google.generativeai as genai
from dotenv import load_dotenv
from bs4 import BeautifulSoup
```




### 新着論文の検索

まずは検索したいカテゴリ、キーワードを設定し、現在時刻から24時間前までにarXivへ投稿された論文を検索しましょう。

セキュリティ上の問題があるので、GeminiのAPIキーは環境変数として扱います。ローカルでのテストの際に環境変数を設定するのも面倒なので、config.envにまとめたものをdotenvで読み込みましょう。まずはarXiv用の設定だけやっていきましょう。

```text: config.env
# 検索したいキーワードをカンマ区切りで指定
SEARCH_KEYWORDS="hoge, huga"

# 検索対象のカテゴリ (例: quant-ph, cs.AI, hep-thなど。'all'で全カテゴリ)
SEARCH_CATEGORY="astro-ph.CO, gr-qc, hep-th, hep-ph"
```

```python: arxiv_alerter.py
# .envファイルから環境変数を読み込む (ローカルテスト用)
load_dotenv(dotenv_path='config.env')

def get_env_var(var_name):
    """
    環境変数を取得する。見つからない場合はエラーメッセージを表示して終了する。
    """
    value = os.environ.get(var_name)
    if value is None:
        print(f"エラー: 必須の環境変数 '{var_name}' が設定されていません。")
        exit(1)
    return value

# 環境変数から諸々を読み込む
# arXiv用
SEARCH_KEYWORDS = get_env_var("SEARCH_KEYWORDS")
SEARCH_CATEGORY = get_env_var("SEARCH_CATEGORY")

# 論文を検索
def search_arxiv():
    """arXiv APIでキーワードに合致する過去24時間の論文を検索する。"""
    print(f"キーワード '{SEARCH_KEYWORDS}' で論文を検索中...")

    # タイトルとアブストラクトから検索するので、各キーワードを (ti:"..." OR abs:"...") の形式にする
    keywords = [
        f'(ti:"{k.strip()}" OR abs:"{k.strip()}")' for k in SEARCH_KEYWORDS.split(',')]
    # 各キーワード検索を "OR" で連結し、全体をカッコで囲む
    keyword_query = f"({' OR '.join(keywords)})"

    query = keyword_query

    if SEARCH_CATEGORY and SEARCH_CATEGORY.lower() != 'all':
        categories = [f'cat:{c.strip()}' for c in SEARCH_CATEGORY.split(',')]
        category_query = f"({' OR '.join(categories)})"
        query += f' AND {category_query}'

    params = {
        'search_query': query,
        'sortBy': 'submittedDate',
        'sortOrder': 'descending',
        'max_results': 25
    }

    try:
        response = requests.get(
            'http://export.arxiv.org/api/query?', params=params, timeout=30)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"arXiv APIへのリクエスト中にエラー: {e}")
        return []

    root = ET.fromstring(response.content)
    ns = {'atom': 'http://www.w3.org/2005/Atom'}

    found_papers = []
    
        yesterday = datetime.now(timezone.utc) - timedelta(days=1) # 前日の同じ時間

    for entry in root.findall('atom:entry', ns):
        published_str = entry.find('atom:published', ns).text
        published_dt = datetime.strptime(
            published_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
        # 24時間以内を論文を対象にする
        if published_dt > yesterday:
            arxiv_id = entry.find('atom:id', ns).text.split('/abs/')[-1]
            paper_info = {
                'id': arxiv_id,
                'title': entry.find('atom:title', ns).text.strip(),
                'summary': entry.find('atom:summary', ns).text.strip(),
                'authors': [author.find('atom:name', ns).text for author in entry.findall('atom:author', ns)],
                'link': entry.find('atom:id', ns).text,
                'html_link': f"https://arxiv.org/html/{arxiv_id}",
                'published': published_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
            }
            found_papers.append(paper_info)

    print(f"{len(found_papers)}件の新しい論文が見つかりました。")
    return found_papers
```

### Geminiで要約を作成

これで過去24時間の論文を検索できました。
それではいよいよGeminiを使って要約を作成していきましょう。

そのためには本文の情報が欲しいですね。しかしarXiv APIで本文が取得できない（はず）です。最近投稿された論文はHTMLも生成されているので、このHTMLから本文を取得しましょう。
使うのはBeautifulSoupですね。
```python: arxiv_alerter.py
def fetch_paper_full_text(html_url):
    """論文のHTMLページから本文テキストを抽出する。"""
    try:
        print(f"  > HTML版の本文を取得中: {html_url}")
        response = requests.get(html_url, timeout=30)
        response.raise_for_status()
        soup = BeautifulSoup(response.content, 'lxml')

        content_div = soup.find('div', class_='ltx_page_content')
        if content_div:
            for tag in content_div.find_all(['header', 'footer', 'nav']):
                tag.decompose()
            return content_div.get_text(separator=' ', strip=True)
        else:
            return soup.body.get_text(separator=' ', strip=True)

    except requests.RequestException as e:
        print(f"  > HTMLの取得に失敗: {e}")
        return None
    except Exception as e:
        print(f"  > HTMLの解析中にエラー: {e}")
        return None
```
これで本文が取得できたので Gemini を使っていきましょう。
まずは環境変数から GEMINI_API_KEY を読み込みましょう。このAPIキーは漏出すると良くないので、config.envには書かない方が安全ですね。
```zsh
export GEMINI_API_KEY=<API_KEY>
```
をターミナルで実行しておきましょう。

```python: arxiv_alerter.py
# arXiv用
SEARCH_KEYWORDS = get_env_var("SEARCH_KEYWORDS")
SEARCH_CATEGORY = get_env_var("SEARCH_CATEGORY")

# Gemini API用
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
```
トークン数に制限があるので、本文の頭から30000字だけ読み込んでいます。
```python: arxiv_alerter.py
# まずはAPIキーが設定されているかチェック
if GEMINI_API_KEY:
    try:
        genai.configure(api_key=GEMINI_API_KEY)
        print("Gemini APIキーが正常に設定されました。")
    except Exception as e:
        print(f"Gemini APIキーの設定中にエラー: {e}")
        GEMINI_API_KEY = None
else:
    print("警告: 環境変数 'GEMINI_API_KEY' が設定されていません。")

# 要約生成
def generate_summary_with_gemini(paper_info, full_text):
    """Geminiを使用して論文の概要を生成する。"""
    if not GEMINI_API_KEY:
        return "（Geminiによる解説はスキップされました：APIキーが未設定です）"

    model = genai.GenerativeModel('gemini-2.0-flash-lite')

    prompt = f"""以下のarXiv論文について、内容を専門外の人が読んでも理解できるように、重要なポイントを箇条書きで分かりやすく解説してください。

# 論文情報
- **タイトル:** {paper_info['title']}
- **著者:** {", ".join(paper_info['authors'])}

# 論文本文（またはアブストラクト）
{full_text[:30000]}

# 解説のポイント
1. この研究が解決しようとしている問題点は何か？
2. どのような新しい手法やアプローチを用いたか？
3. この研究の最も重要な発見や結論は何か？
4. この発見が将来どのような影響を与える可能性があるか？

以上の点を踏まえて、日本語で解説を生成してください。
"""
    try:
        response = model.generate_content(prompt)
        time.sleep(1)
        return response.text
    except Exception as e:
        print(f"Gemini APIでの解説生成中にエラー: {e}")
        return f"（Geminiによる解説生成中にエラーが発生しました: {e}）"
```

# メール送信
これで`response.text`に要約が代入されました。あとはこの要約をメールで送信しましょう！
Gmailをコードから送信するにはアプリパスワードというものを取得しないといけません。これはGoogle アカウントのパスワードとは別物です。
https://support.google.com/mail/answer/185833?hl=ja#zippy=%2Cアプリ-パスワードが必要となる理由 
を参照して取得しましょう。このパスワードも漏洩すると良くないので、環境変数に設定しましょう。
```zsh
export SMTP_PASSWORD="ここにご自身のSMTPパスワード"
```
でいいですね。それではメールを送る設定を済ませましょう。
メアドとかは漏れてもそこまで大きな問題ではないかなと思うのでconfig.envに書いてしまいましょうか。セキュリティ意識のしっかりしているあなたはこれらも環境変数に設定しましょう！！

```text: config.env
# --- メール送信設定 (Gmailの場合の例) ---
# Gmail以外のサービスを使う場合は、プロバイダの指示に従ってください
SMTP_SERVER="smtp.gmail.com"
SMTP_PORT="587"
# 送信元となるご自身のGmailアドレス
SMTP_USER="メールアドレス"
# 送信元となるご自身のGmailアドレス
MAIL_FROM="メールアドレス"
# 送信先メールアドレス
MAIL_TO="メールアドレス"
# メールの件名
MAIL_SUBJECT="【arXiv Alerter】注目論文が見つかりました"
```

そしてこれらの変数を読み込みます。
```python: arxiv_alerter.py
# arXiv用
SEARCH_KEYWORDS = get_env_var("SEARCH_KEYWORDS")
SEARCH_CATEGORY = get_env_var("SEARCH_CATEGORY")

# Gemini API用
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")

# メール送信用
SMTP_SERVER = get_env_var("SMTP_SERVER")
SMTP_PORT = int(get_env_var("SMTP_PORT"))
SMTP_USER = get_env_var("SMTP_USER")
SMTP_PASSWORD = get_env_var("SMTP_PASSWORD") 
MAIL_FROM = get_env_var("MAIL_FROM")
MAIL_TO = get_env_var("MAIL_TO")
MAIL_SUBJECT = get_env_var("MAIL_SUBJECT")
```
メールを送る関数はこちら。
```python: arxiv_alerter.py
def send_email(subject, body):
    """メールを送信する。"""
    if not all([SMTP_SERVER, SMTP_USER, SMTP_PASSWORD, MAIL_FROM, MAIL_TO]):
        print("メール設定が不完全なため、送信をスキップします。")
        return

    print(f"{MAIL_TO} 宛にメールを送信中...")
    msg = MIMEText(body, 'plain', 'utf-8')
    msg['Subject'] = Header(subject, 'utf-8')
    msg['From'] = MAIL_FROM
    msg['To'] = MAIL_TO

    try:
        context = ssl.create_default_context()
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls(context=context)
            server.login(SMTP_USER, SMTP_PASSWORD)
            server.send_message(msg)
        print("メールが正常に送信されました。")
    except Exception as e:
        print(f"メール送信中にエラーが発生しました: {e}")
```

### main関数
これで基本的な実装はすみました。それではmain関数を書いていきましょう
メールのテンプレートもここに書いていきます。
```python: arxiv_alerter.py
def main():
    """メイン処理"""
    papers = search_arxiv()
    if not papers:
        print("処理対象の論文はありませんでした。")
        return

    full_email_body = f"キーワード「{SEARCH_KEYWORDS}」に関する新しい論文が {len(papers)}件 見つかりました。\n\n"

    for i, paper in enumerate(papers, 1):
        print(f"--- 論文 {i}/{len(papers)} の処理を開始: {paper['title']} ---")

        full_text = fetch_paper_full_text(paper['html_link'])

        if not full_text:
            print("  > HTML版の取得に失敗したため、アブストラクトを要約します。")
            full_text = paper['summary']

        summary_ja = generate_summary_with_gemini(paper, full_text)

        full_email_body += f"""
==================================================
論文 {i}: {paper['title']}
==================================================

著者: {", ".join(paper['authors'])}
投稿日: {paper['published']}
リンク: {paper['link']}

--- Geminiによる解説 ---
{summary_ja}
--------------------------

--- Original Abstract ---
{paper['summary']}
--------------------------

"""
    send_email(MAIL_SUBJECT, full_email_body)

if __name__ == "__main__":
    main()
```

## 実行の自動化(Github actions の利用)
これで完成しました。実行するたびに新着論文の要約を生成してメールを送ってくれます。

しかし、毎日実行するのも面倒だし、それなら普通に検索するのとあまり変わらないですね。Githubをつかって実行を自動化して、毎日決まった時間に来るようにしましょう！

Github actionsの基本は他の記事を参考にしてもらうとして、ここでは実行に必要なYMLファイルを作っていきます。

 ### YMLファイルの作成

 まずはYMLファイルに盛り込む要件ですが
 - 必要なライブラリをキャッシュする: これをすると2回目以降の実行が早くなる（自動実行だし気にしなくても良いけど）
 - 漏れても良い環境変数の設定
 - 毎日10時に実行
 程度で良いでしょう。
 github actionsではconfig.envを使わずに、YMLに書いていきます。ただ、congif.envにAPIキーやパスワードを書いていないので、ここは抜かしても良いですね。

まずは必要なライブラリをまとめます。これを読み込んでinstallしてもらうわけですね。
 ```text: requirements.text
 requests
google-generativeai
beautifulsoup4
lxml
python-dotenv
```
そしてarxiv_alerter.ymlというファイルを作成して、 
```
.github/workflows/arxiv_alerter.yml 
```
に配置しましょう。その中身は次のとおりです。

```yml: arxiv_alerter.yml
# GitHub Actions Workflow: arXiv Keyword Alerter

name: arXiv Keyword Alerter

on:
  schedule:
    # 毎日 UTC 1時 (JST 10時) に実行
    - cron: '0 1 * * *'
  workflow_dispatch:

jobs:
  run-arxiv-alerter:
    runs-on: ubuntu-latest

    steps:
      # ステップ1: リポジトリのコードをチェックアウト
      - name: Checkout repository
        uses: actions/checkout@v4

      # ステップ2: Python環境をセットアップ
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      # --- ▼▼▼ ここからがキャッシュ処理 ▼▼▼ ---
      # ステップ3: pipのキャッシュパスを取得
      - name: Get pip cache dir
        id: pip-cache
        run: |
          echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT

      # ステップ4: キャッシュの復元・保存
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          # キャッシュするファイルのパス
          path: ${{ steps.pip-cache.outputs.dir }}
          # キャッシュのキー。requirements.txtが変更されるとキーも変わる
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          # 完全一致するキーがない場合に復元を試みるキー
          restore-keys: |
            ${{ runner.os }}-pip-
      # --- ▲▲▲ キャッシュ処理ここまで ▲▲▲ ---

      # ステップ5: 必要なPythonライブラリをインストール
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          # requirements.txtからライブラリをインストールする
          pip install -r requirements.txt

      # ステップ6: Pythonスクリプトを実行
      - name: Run arXiv Alerter script
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
          # ここから下はconfig.envに記載してもスッキリして良い。
          SEARCH_KEYWORDS: "HOGE,HUGA"
          SEARCH_CATEGORY: "hoge,fuga"
          SMTP_SERVER: "smtp.gmail.com"
          SMTP_PORT: "587"
          SMTP_USER: "メールアドレス"
          MAIL_FROM: "メールアドレス"
          MAIL_TO: "メールアドレス"
          MAIL_SUBJECT: "【arXiv Alerter】注目論文が見つかりました"
        run: python arxiv_alerter.py
```

はい、これで大体終わりましたがAPIキーとかを設定してないですね。上のファイルでは
```python: arxiv_alerter.py
EMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
```
としていますね。ここでgithubが提供している、安全に環境変数を管理するための機能を使っています。
リポジトリのconfig -> secrets and variables -> Aactionsに行ってRepository secretsを新規作成しましょう。名前はコード内に記載した変数名と同じにするよう気をつけてください。
- GEMINI_API_KEY
- SMTP_PASSWORD
ですね。secretsの詳細はドキュメントを参照してください。

https://docs.github.com/ja/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions

## 完成！
これで時間になると自動実行され、新着論文があればメールを送ってくれるようになります。
論文サーベイが面倒なあなたも、新着論文が多すぎて普通に読んではいられないあなたも、これで毎日の快適な論文チェックライフを過ごしてください！