【Python】学振の研究遂行経費報告書を自動で入力する

**追記2022-03-28**
**@kn1cht様から改善案を頂きました。ありがとうございます。**
現状では学振マイページを経由してJシステム（特別研究員 提出Webフォームシステム）にたどり着く仕組みになっていますが、直接Jシステム (https://tyousa.jsps.go.jp/stu21/) にアクセスしても問題ないようです。
なので、該当の箇所を改定しています、githubには既にcommit済みです。

**追記2022-03-31**
**@Hiroyuki1993様から多くの改善案を頂きました。ありがとうございます。** 内容としては下記のとおり。
+ 設定ファイルの分離（JシステムIDなど）
+ READMEの追加（Mac対応含む）
+ 細かいバグの修正
githubには既にcommit済みです。

**追記2022-03-31**
最後のポップアップウィンドウに対しても自動化しました。
また、windowsユーザーのみですが、**GUI化およびexe化を行いました。**
**python環境のない方でも利用可能です。是非活用下さい。**
詳しい使い方は[GUIで使いたい人へ](#guiで使いたい人へ)に記載しています。

**追記2023-03-30**
2022年度でも同じシステムが使われていたので、それに合わせてコードを最新のものに差し替えました。
基本的な使用方法は同じです。githubには既にcommit済みです。

# はじめに

今年度ももう終わり、様々な書類仕事をしなければならない時期が来ました。博士・ポスドクの方の中では、学振の~~しちめんどくさい~~手続きをしている人もいるでしょう。学振には研究遂行経費というものがあり、所得の一部を研究経費として計上できる制度があります。これを利用すると所得税を節税できるので、この制度を利用している学振研究者は多いと思います。購入品目をExcelとかGoogle spread sheetとかを使って管理しているかもしれません。私もその一人です。見返したら大体3~40品目ぐらい購入していました。

さて、そんな研究遂行経費を報告する学振のWebフォームがこちら。

![スクリーンショット 2022-03-23 094849.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269013/ca70ec7e-a533-9fd8-80e8-f3f0f64929e0.png)

　 (　ﾟдﾟ)
＿(＿つ/￣￣￣/＿
　　＼/　　　/
　　　 ￣￣￣

　 ( ﾟдﾟ )
＿(＿つ/￣￣￣/＿
　　＼/　　　/

**手打ち？マジ？**

まぁ色々とひどい。
csvインポーターもない？ほとんどの人はExcelで管理してるって。人によっては**100品目超える場合**もあるんやぞ。品名を適宜修正しながらの手入力じゃ1日かかるわい。予め規定のExcelファイルを配布しておけば互いに楽ではないのか？DXのご時世になんでこんな時代に逆行するような仕組みを搭載してしまうのか？

他の人も絶対同じことを思い悲しんでいるはず。「**せめて入力だけでも...**」と思いpythonを使って自動化しました。
今回の記事はその報告です。

↓　成果物
![特別研究員　提出Webフォームシステム - Google Chrome 2022-03-23 09-59-55_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269013/b7a5c44e-7a12-c0f6-f36d-df159cddeb0f.gif)


**日本学術振興会さん、学術を振興させたいなら来年度はまともなシステムに変えてください...**:sob: 
~~2022年度も変わんなかったから多分これ当分変わんねーな...~~

# 中身の説明
やっていることはpandasによるデータ整形とseleniumによる自動入力なのでn番煎じではあります。
[github](https://github.com/yanami85/JSPS_auto_input)にコードと専用のエクセルファイルを上げるので良かったら使ってあげてください。
~~python触れない人のためにもexe化も検討しています（やれるか怪しい）。~~
**windowsユーザーのみですがGUI化及びexe化を行いました！**

# 事前準備
まずgoogle chromeをインストールし、chromeのバージョンに合ったchromedriverを[ここ](https://chromedriver.chromium.org)からダウンロード。
私の環境では[ChromeDriver 99.0.4844.51](https://chromedriver.storage.googleapis.com/index.html?path=99.0.4844.51/)をダウンロードしました。
OSによってダウンロードするものが違うので注意。windowsはwin32, macはmac64 or mac64_m1を入れる。
解凍し、pythonを記述するファイルと同じディレクトリに中身を入れておく。これで準備はok

# スクリプトの記述

そして設定ファイルの作成。setting_sample.pyを開き、IDとpasswordを入力します。
その後、setting.pyにリネーム

```setting.py
# Jシステムログイン情報
J_SYSTEM_ID = "XXXXXX"
J_SYSTEM_PASS = "XXXXXX"

# Chromedriverのパス
DRIVER_PATH = "./chromedriver.exe"

# 報告書シートへのパス
EXCEL_SHEET_PATH = 'item_list.xlsx'

```

必要なもろもろをimport。足りないものはpip or condaでインストールしてください。
```import.py
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import settings
```

そしてコツコツ入力しておいたExcel fileの読み込み
```read_excel.py
df = pd.read_excel(settings.EXCEL_SHEET_PATH, usecols=[0,1,2,3,4,5,6]).dropna()
```

Excel fileの中身はこんな感じになっています。
|    | 納入日              | 品名                                                  |   単価 | 税抜・税込   |   個数 | 経費項目                 | 領収書の有無   |
|---:|:--------------------|:------------------------------------------------------|-------:|:-------------|-------:|:-------------------------|:---------------|
|  0 | 2021-04-21 00:00:00 | foo                              |  10000| 税込         |      1 | 学術調査にかかる経費     | あり           |
|  1 | 2021-05-22 00:00:00 | bar                             |  30000 | 税込         |      1 | 学術調査にかかる経費     | あり           |

先のことを考え、列名を変更。ついでに各品目でかかった総額も出しておく。
```rename_column.py
df.columns = ["date", "item", "value", "is_tax","number", "expenditure", "is_receipt"]

def add_tax(x):
    if x.is_tax =="税抜":
        return x.value*1.1
    else:
        return x.value

df['value'] = df.apply(lambda x: add_tax(x), axis = 1)*df["number"]
```
ここからは、Web側の入力に合わせてデータを前処理する。
まず、プルダウンメニューについて。
[こちらの記事](https://yuki.world/selenium-select/)によると、seleniumの```select.select_by_value()```で指定ができるらしい。
引数はintであり、要はプルダウンメニュー内の順番。これに合わせてデータ側を改変していく。

「領収書の有無」はラジオボタンなので、それぞれのボタンのidを探してそのidを割り当てればok。


```forum_import.py
df['date'] = df['date'].dt.strftime("%Y-%m")

def convert_date_to_flag(x):
    year = 2022
    date_to_flag_map = {
        f"{year}-04": 1, f"{year}-05": 2, f"{year}-06": 3,
        f"{year}-07": 4, f"{year}-08": 5, f"{year}-09": 6,
        f"{year}-10": 7, f"{year}-11": 8, f"{year}-12": 9,
        f"{year + 1}-01": 10, f"{year + 1}-02": 11, f"{year + 1}-03": 12
    }
    return date_to_flag_map[x.date]
    
def determine_receipt(row):
    if row.determine_receipt == "あり":
        return "etr_needOrNotReceipt_01"
    else:
        return "etr_needOrNotReceipt_02"

def assign_expenditure(row):
    expenditure_map = {
        "学会関係経費": 1,
        "各種研究集会等への参加費": 2,
        "学術調査にかかる経費": 3,
        "自宅での研究に必要な経費": 4,
        "所属・関連機関への交通費": 5
    }
    return expenditure_map[row.expenditure]

df["date"] = df.apply(lambda x: convert_date_to_flag(x), axis = 1)
df["is_receipt"] = df.apply(lambda x: is_receipt(x), axis = 1)
df["expenditure"] = df.apply(lambda x: assign_expenditure(x), axis = 1)

df_l = [list(row) for row in df.itertuples()]
df = df[["date", "item", "expenditure", "is_receipt", "value"]]
df["value"] = pd.Series(df["value"], dtype = 'int64')
```

変更後はこんな感じ。
|    |   date | item                                                  |   expenditure | is_receipt              |   value |
|---:|-------:|:------------------------------------------------------|--------------:|:------------------------|--------:|
|  0 |      1 | foo                                                            |             3 | etr_needOrNotReceipt_01 |   10000|
|  1 |      2 | bar                                  |             3 | etr_needOrNotReceipt_01 |   30000 |

これで入力用のデータは作れた。あとはseleniumで自動入力させていけばいい。



```selenium.py
driver = webdriver.Chrome(executable_path = settings.DRIVER_PATH)

J_SYSTEM_URL = r"https://tyousa.jsps.go.jp/stu22/"
J_SYSTEM_ID = settings.J_SYSTEM_ID
J_SYSTEM_PASS = settings.J_SYSTEM_PASS

driver.get(J_SYSTEM_URL)
wait = WebDriverWait(driver, 5)
    
wait.until(EC.presence_of_element_located((By.NAME, 'login_id')))
driver.find_element(By.NAME, 'login_id').send_keys(J_SYSTEM_ID)
driver.find_element(By.NAME,"login_psw").send_keys(J_SYSTEM_PASS)
driver.find_element(By.NAME,"lg").click()
    
wait.until(EC.presence_of_element_located((By.ID, "li_of_F003")))
driver.find_element(By.ID, "li_of_F003").click()

for row in df.itertuples():
    enter_forum(row)

driver.find_element(By.ID,"additional_function_on_submitting").click()
alert = driver.switch_to.alert
alert.accept()
```

これで出来上がり。上手く行けば自動でchromeが立ち上がりガンガン入力されていくはず。
起動中にchromeを最小化しても問題ありませんが、スクロールするとエラーが出る可能性があります。
今回試したところ、30品目ぐらいで5分もかからなかったので、それなりにはかどると思います。
もし品目を追加したい場合は、追加した品目**だけ**をExcelで入力して、再実行すればいいはずです。
注意点として、品名は**30文字以内**でないといけないので、もし超えている場合はExcel sheet上で修正して下さい。

# GUIで使いたい人へ
windowsユーザにのみですが、GUI化、exe化に対応しました。内容としてはPysimpleGUIとpyinstallerの組み合わせで達成しています。
もしmacユーザの方がいらして、「pyinstaller使えるよ」っていう人がいたら、githubから```auto_input_GUI.py```をpyinstallerでコンパイルしていただければ
macユーザーにも対応できると思います。

使い方は以下の通り。
1. [github](https://github.com/yanami85/JSPS_auto_input)からレポジトリをクローン (わからなければcode　→　download zipとクリックすればzipファイルがダウンロードされるのでそれを解凍する)
2. [ChromeDriver](https://chromedriver.chromium.org/)を[事前準備](#事前準備)のとおりにダウンロードして解凍
2. 解凍したChromeDriverをレポジトリ（またはgithubからダウンロードしたディレクトリ）に配置
3. `item_list_sample.xlsx`に物品名等を入力
4. `auto_input_GUI.exe`を実行
5. 以下のwindowが出るので各項目を入力し「次へ」をクリック
 ![image](https://user-images.githubusercontent.com/41857834/161011843-304f2651-a7bd-4fe5-87c1-0b69bb7667f1.png)

これで少しでも事務に割く時間が削減できればと思います。

# 最後に
コードの全文を載せます。ご自由にお使いください。

```全文.py
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import settings

def add_tax(x):
    if x.is_tax =="税抜":
        return x.value*1.1
    else:
        return x.value

def convert_date_to_flag(x):
    year = 2022
    date_to_flag_map = {
        f"{year}-04": 1, f"{year}-05": 2, f"{year}-06": 3,
        f"{year}-07": 4, f"{year}-08": 5, f"{year}-09": 6,
        f"{year}-10": 7, f"{year}-11": 8, f"{year}-12": 9,
        f"{year + 1}-01": 10, f"{year + 1}-02": 11, f"{year + 1}-03": 12
    }
    return date_to_flag_map[x.date]

def determine_receipt(row):
    if row.determine_receipt == "あり":
        return "etr_needOrNotReceipt_01"
    else:
        return "etr_needOrNotReceipt_02"

def assign_expenditure(row):
    expenditure_map = {
        "学会関係経費": 1,
        "各種研究集会等への参加費": 2,
        "学術調査にかかる経費": 3,
        "自宅での研究に必要な経費": 4,
        "所属・関連機関への交通費": 5
    }
    return expenditure_map[row.expenditure]


def enter_forum(row):
    wait.until(EC.presence_of_element_located((By.ID,'etr_group')))
    dropdown_expense = driver.find_element(By.ID,'etr_group')
    select_expense = Select(dropdown_expense)
    select_expense.select_by_index(row.expenditure)
    
    wait.until(EC.presence_of_element_located((By.ID,'etr_dateOfComp')))
    dropdown_date = driver.find_element(By.ID,'etr_dateOfComp')
    select_date = Select(dropdown_date)
    select_date.select_by_index(row.date)
    
    wait.until(EC.element_to_be_clickable((By.ID, str(row.determine_receipt))))
    driver.find_element(By.ID,str(row.determine_receipt)).click()
    
    wait.until(EC.presence_of_element_located((By.NAME,"etr_itemName")))
    driver.find_element(By.NAME,"etr_itemName").send_keys(row.item)
    
    wait.until(EC.presence_of_element_located((By.NAME,"etr_billAmount")))
    driver.find_element(By.NAME,"etr_billAmount").send_keys(row.value)
    
    wait.until(EC.element_to_be_clickable((By.ID,"add_to_basetable")))
    driver.find_element(By.ID,"add_to_basetable").click()

if __name__ == '__main__':
    df = pd.read_excel(settings.EXCEL_SHEET_PATH, usecols=[0,1,2,3,4,5,6]).dropna()
    
    # seleniumで入力するためのidやキーを纏めたデータを作成する
    df.columns = ["date", "item", "value", "is_tax","number", "expenditure", "determine_receipt"]
    df['date'] = df['date'].dt.strftime("%Y-%m")
    df['value'] = df.apply(lambda x: add_tax(x), axis = 1)*df["number"]
    df["date"] = df.apply(lambda x: convert_date_to_flag(x), axis = 1)
    df["determine_receipt"] = df.apply(lambda x: determine_receipt(x), axis = 1)
    df["expenditure"] = df.apply(lambda x: assign_expenditure(x), axis = 1)
    df_l = [list(row) for row in df.itertuples()]
    df = df[["date", "item", "expenditure", "determine_receipt", "value"]]
    df["value"] = pd.Series(df["value"], dtype = 'int64')

    driver = webdriver.Chrome(executable_path = settings.DRIVER_PATH)

    J_SYSTEM_URL = r"https://tyousa.jsps.go.jp/stu22/"
    J_SYSTEM_ID = settings.J_SYSTEM_ID
    J_SYSTEM_PASS = settings.J_SYSTEM_PASS

    driver.get(J_SYSTEM_URL)
    wait = WebDriverWait(driver, 5)
    
    wait.until(EC.presence_of_element_located((By.NAME, 'login_id')))
    driver.find_element(By.NAME, 'login_id').send_keys(J_SYSTEM_ID)
    driver.find_element(By.NAME,"login_psw").send_keys(J_SYSTEM_PASS)
    driver.find_element(By.NAME,"lg").click()
    
    wait.until(EC.presence_of_element_located((By.ID, "li_of_F003")))
    driver.find_element(By.ID, "li_of_F003").click()

    for row in df.itertuples():
        enter_forum(row)

    driver.find_element(By.ID,"additional_function_on_submitting").click()
    alert = driver.switch_to.alert
    alert.accept()

```