指定した期間の申告書XMLデータを e-tax WEB版からノークリックで自動ダウンロードする Python スクリプト

### ※2024年5月28日Upadte
e-tax WEB版のサイト構造がガラリと変わりました。
下記のスクリプトは、旧e-tax WEB版を基に作成したため、
現在のバージョンには対応しておりません。。

## はじめに
### e-Taxからデータを自動ダウンロードする背景と目的
・毎月その月に提出されたすべての申告書データ（拡張子.xtxのXMLデータ）をダウンロードして解析・情報抽出して業務で活用しているが、平均300～400件/月ほどのダウンロード作業に2～3時間ほどかかるためしんどかった。（おもにクリックしすぎで手首が痛む）

・個人の確定申告時期（2～3月）のダウンロード数が1000件/月を軽く超えそうだったため、大慌てで本スクリプトの作成に着手した。

・目的は、「指定した期間の提出済み申告書データについて、e-tax WEB版からのダウンロードを自動化して手首を守る」こと

### 本記事の想定対象読者
・Seleniumでe-tax WEB版のブラウザ操作を自動化したい会計事務所の人
・e-tax WEB版から申告書データのダウンロード作業を自動化したい会計事務所の人
・上記のようなニーズを持つ会社の経理/税務担当者の人

### 本スクリプトが対応できること
・[会計事務所の場合]受信通知の閲覧に税理士カードによる認証が不要である法人納税者の手続き（法人税、消費税、源泉所得税などの申告その他申請・届出）に係る申告書データのダウンロード
・[通常の会社の場合]自社で提出されたすべての電子申告データのうち、ダウンロードデータが存在するもの

### 本スクリプトが対応できないこと
・受信通知の閲覧に税理士カードによる認証が必要である個人納税者の手続き（所得税、個人消費税、相続税、贈与税、源泉所得税などの申告その他申請・届出）に係る申告書データのダウンロード（開発中）

### 参考記事
[Pythonでe-TAXへのログインを自動化する方法](https://ban-tax.com/python_e-tax_web/)

## 環境構築
各種バージョンは下記のとおり。
・Python 3.12.0
・selenium 4.18.1
・ブラウザ：Chrome（バージョン: 124.0.6367.91）

### Seleniumのインストール方法
```
pip install selenium
```
**※かつて必要だったChromedriverの手動ダウンロードやパス設定は不要になっています。**
これについて下記の記事がすごく参考になりました。

[最新のChromedriverの設定　vol.2](https://qiita.com/Chronos/items/7f56898af25523d04598#%E3%81%BE%E3%81%95%E3%81%8B%E3%81%AE%E4%BA%8B%E5%AE%9F)

### e-Tax-APの自動有効化
e-tax WEB版を正しく動作させるためには、国税庁が提供しているChrome拡張機能「e-Tax AP」を有効化しておく必要があります。そのため、Seleniumのwebdriverで起動するChromeにおいて「e-Tax AP」を自動で有効化させるために、「e-Tax AP」をCRXファイル化して、スクリプト実行中で読み込む必要があります。
（「e-Tax AP」をCRXファイルを読み込まないと、ログインの前ではじかれます）

本スクリプトでは、「e-Tax AP」のCRXファイルを、ファイル名 `e-Tax-AP.crx` として、本スクリプトファイルと同じディレクトリに配置することを想定しています。 

この手順について、下記の記事がすごく参考になりました。

[chromeアドオンのcrxファイルを入手する方法｜ぬまろぐ](https://www.mussyu1204.com/hp/articles/0004_chrome-offline-addon)

### 事前準備セットアップ
e-tax WEB版にログインを試みる際、e-tax WEB版を初めて利用する場合や、前回セットアップから一定期間（2～3か月程度）経過している場合などに、下記のようなポップアップが出現して、事前準備セットアップを促されることがあります。（最新バージョンの事前準備セットアップが完了していればこのポップアップは出現しなくなります）


スクリプト動作時にこのポップアップが出現しないように、[e-tax WEB版のログイン画面](https://clientweb.e-tax.nta.go.jp/UF_WEB/WP000/FCSE00001/SE00S010SCR.do?_gl=1*wmsnko*_ga*MzQyMDgzMjg3LjE3MTA4ODAwNjU.*_ga_3PB99RN219*MTcxNDE5ODU2NC41LjEuMTcxNDIwMDE2NS4wLjAuMA..)に予めアクセスしてみて、ポップアップが出現する場合は事前準備セットアップを完了させておくことをおすすめします。

<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559502/b442a555-6bb7-8e0e-6b38-c8684ae15c88.png>


## スクリプトの説明
### 動作の概要
・実行後、指定期間開始日、指定期間終了日、"過去分"の探索ありorなし、利用者識別番号、パスワードを指定します。

・あとは、ログイン処理、画面遷移、申告書データのダウンロードなど一連の操作すべて自動で実行してくれます。

・概ねダウンロード１件あたり、1.2～1.5秒でこなします。

・本スクリプトでダウンロードの対象になる手続きは、「メッセージボックス一覧」画面に表示されている申告履歴のうち、税理士カードによる認証なしで閲覧可能な受信通知に限られます

・ダウンロードデータが存在しない手続き（納付情報登録依頼、税務署からのお知らせ、など）も判定して自動でスキップします。

・e-tax WEB版の仕様上、「メッセージボックス一覧」画面には最大1000件の手続きが表示され、それ以前の手続きを閲覧したい際は「次へ」ボタンや「過去分」ボタンで表示を切り替える必要がありますが、これらの操作も自動で対応します。

・指定期間終了日まで探索後、指定期間外のデータが５個連続したタイミングでスクリプトは終了します。

・実行中に探索の対象となった各手続きとその処理結果は、ログファイル（ファイル名 `access_histroy.txt`） に出力されます。

### ※下記の点に注意してください。
・前月１か月分の提出済み申告書データのダウンロードを想定しているため、自動化指定期間開始日より前～開始日までは全て探索対象になります。かなり前の日付を開始日に指定するときはその分時間かかります。

・いったん実行すると途中でポーズできません。中断する際は、ターミナルから`[Ctrl]+[c]`などで強制中断してください。

・ボタンクリック、ページ遷移など操作対象となる要素の指定は、idやclassなどを使って要素をハードコードで指定しているため、これらの情報が変更された場合は正常に動作しません。（過去３か月程度の間に１度、idに含まれる番号が変更されたことがあるため、そこそこの頻度で変更がかかると思われます）

### スクリプト本体
```AutoDLer.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
from datetime import datetime as dt
from datetime import timedelta


# 各申告履歴の処理状況をログに出力する関数
def logging (array) :
    access_history.append(array)
    with open("access_histroy.txt", "w", encoding="utf-8") as f:
        for item in access_history:
            f.write(str(item) + "\n")
    

# ダウンロードする期間と過去分の設定を指定する
flag1 = True
while flag1:
    start_date = input('開始日を入力してください（例：2024/01/01）\n指定しない場合はEnter：')
    if start_date:
        try:
            dt.strptime(start_date, '%Y/%m/%d')
            flag1 = False
        except:
            print('\033[31m'+'開始日の日付の形式が正しくありません。\n再度指定してください。'+'\033[0m')
            continue
    else:
        flag1 = False
flag2 = True
while flag2:
    end_date = input('終了日を入力してください（例：2024/12/31）\n指定しない場合はEnter：')
    if end_date:
        try:
            dt.strptime(end_date, '%Y/%m/%d')
        except:
            print('\033[31m'+'終了日の日付の形式が正しくありません。\n再度指定してください。'+'\033[0m')
            continue
        if start_date:
            if dt.strptime(end_date, '%Y/%m/%d') < dt.strptime(start_date, '%Y/%m/%d'):
                print('\033[31m'+'開始日以降の日付を入力してください。\n再度指定してください。'+'\033[0m')
                continue
            else:
                flag2 = False
        else:
            flag2 = False
    else:
        flag2 = False
flag3 = True
while flag3:
    past_flag = input("過去分のダウンロードを行いますか？（はい：1、いいえ：0）\n")
    if past_flag == "1":
        past_flag = 1
        flag3 = False
    elif past_flag == "0":
        past_flag = 0
        flag3 = False
    else:
        print('\033[31m'+'選択肢が正しくありません。\n再度指定してください。'+'\033[0m')
        continue

# URLを指定する
URL = 'https://clientweb.e-tax.nta.go.jp/UF_WEB/WP000/FCSE00001/SE00S010SCR.do'

# 利用者識別番号とパスワードを指定する
userid = input('12桁の利用者識別番号を入力してください（例：0000-1111-2222-3333）：')
ID_1 = userid[0:4]
ID_2 = userid[5:9]
ID_3 = userid[10:14]
ID_4 = userid[15:19]
passward = input('パスワードを入力してください：')


# e-Tax-APの有効化
extension_path = 'e-Tax-AP.crx'
chrome_options = Options()
chrome_options.add_extension(extension_path)

# ChromeDriverを使用してChromeを起動（拡張機能を指定）
driver = webdriver.Chrome(options=chrome_options)
driver.get(URL)

# e-taxログイン画面を開く
button1_xpath = '//img[@alt="ログイン"]' 
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.XPATH, button1_xpath)))
time.sleep(1) 
button1 = driver.find_element(By.XPATH, button1_xpath)
button1.click()

# ログインを実行
id_xpath = '//div[@id="dijit_layout_ContentPane_18"]/input' # 番号が変わる可能性あり
pass_xpath = '//input[@id="maskat_widget_dojo_59"]' # 番号が変わる可能性あり
button2_xpath = '//a[@tabindex="29"]/img[@alt="ログイン"]' # 番号が変わる可能性あり
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.XPATH, id_xpath)))
time.sleep(1)
id1_input = driver.find_element(By.XPATH, f'{id_xpath}[1]')
id2_input = driver.find_element(By.XPATH, f'{id_xpath}[2]')
id3_input = driver.find_element(By.XPATH, f'{id_xpath}[3]')
id4_input = driver.find_element(By.XPATH, f'{id_xpath}[4]')
pass_input = driver.find_element(By.XPATH, pass_xpath)
id1_input.send_keys(ID_1)
id2_input.send_keys(ID_2)
id3_input.send_keys(ID_3)
id4_input.send_keys(ID_4)
pass_input.send_keys(passward)
rogin_button_2=driver.find_element(By.XPATH, button2_xpath)
rogin_button_2.click()
print("ログイン成功！")

# 送信結果・お知らせページを開く
button3_xpath = '//a[@tabindex="33"]/img[@alt="送信結果・お知らせ"]' # 番号が変わる可能性あり
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.XPATH, button3_xpath)))
time.sleep(1)
rogin_button_3 = driver.find_element(By.XPATH, button3_xpath)
rogin_button_3.click()
print("送信結果・お知らせページを開きました。")

# メッセージボックス一覧を開く
button4_xpath = '//a[@tabindex="21"]/img[@alt="操作に進む"]' # 番号が変わる可能性あり
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.XPATH, button4_xpath)))
time.sleep(1)
rogin_button_4 = driver.find_element(By.XPATH, button4_xpath)
rogin_button_4.click()
print("メッセージボックスを開きました。")

# メッセージボックス一覧を開いているウィンドウを切り替え用に保存しておく
messagebox_window = driver.current_window_handle

# 表示されている申告履歴の数を取得する
history_items_count = len(WebDriverWait(driver, 10).until(
    EC.presence_of_all_elements_located((By.XPATH, '//table[starts-with(@id, "ta")]'))
))
#history_items_count = 3 # テスト用

# メッセージ詳細へアクセスした履歴保存用リスト
access_history = []

# 各申告履歴に対してデータダウンロードを実行する
# 指定した終了日以降で期間外データが5件連続続いた場合は処理を終了する
break_flag = True
while break_flag:
    outdate_cnt = 0 # 期間外データ連続数のカウント用
    outdate_flag = 0 # 期間外データがあったかのフラグ
    # 各申告履歴1件ずつに対して…
    for i in range(history_items_count):
        print(i)
        try:
            # 申告履歴の情報（提出日、納税者名、手続き名など）を取得する
            WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, f"sd{i}")))
            submittion_date = driver.find_element(By.ID, f"sd{i}").text
            received_number = driver.find_element(By.ID, f"on{i}").text
            received_date = driver.find_element(By.ID, f"od{i}").text
            result = driver.find_element(By.ID, f"or{i}").text
            tetsuduki_name = driver.find_element(By.ID, f"tn{i}").text
            name = driver.find_element(By.ID, f"nocn{i}").text
            arr = [submittion_date, received_number, received_date, result, tetsuduki_name, name]
            # 最初に指定した期間と、申告履歴の提出日を比較する
            if start_date: s_day = dt.strptime(start_date, '%Y/%m/%d')
            if end_date: e_day = dt.strptime(end_date, '%Y/%m/%d') + timedelta(days=1)
            subedday = dt.strptime(submittion_date, '%Y/%m/%d %H:%M:%S')
            # 期間外のデータはスキップする
            if start_date and end_date:
                if subedday < s_day or e_day < subedday:
                    outdate_cnt += 1
                    print(f'期間外のデータです: {name}  {tetsuduki_name}')
                    arr.append("out of date range")
                    logging(arr)
                    if outdate_cnt >= 5 and outdate_flag == 1:
                        break_flag = False
                        break
                    continue
            elif start_date and not end_date:
                if subedday < s_day:
                    outdate_cnt += 1
                    print(f'期間外のデータです: {name}  {tetsuduki_name}')
                    arr.append("out of date range")
                    logging(arr)
                    if outdate_cnt >= 5 and outdate_flag == 1:
                        break_flag = False
                        break
                    continue
            elif not start_date and end_date:
                if e_day < subedday:
                    outdate_cnt += 1
                    print(f'期間外のデータです: {name}  {tetsuduki_name}')
                    arr.append("out of date range")
                    logging(arr)
                    if outdate_cnt >= 5 and outdate_flag == 1:
                        break_flag = False
                        break
                    continue
            
            # 期間内のデータであった場合は後続の処理を実行する
            item_id = f"ta{i}"
            item = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.ID, item_id))
            )
            
            # 別タブでメッセージ「受信通知」ページへ遷移可能なら遷移する
            # ※個人の納税者の受信通知は、税理士カードの認証を行わなければ、「受信通知」ページに遷移できない
            try:
                item.click()


                # 開いたタブに切り替える
                driver.switch_to.window(driver.window_handles[1])
                        
                try:
                    # 「受信データ」項目内の「ダウンロード」ボタンをクリックする
                    # 「ダウンロード」ボタンが存在しない場合は3秒待ってからエラーをキャッチして例外処理に移る
                    download_button = WebDriverWait(driver, 3).until(
                        EC.presence_of_element_located((By.CSS_SELECTOR, 'input[type="image"][alt="ダウンロード"]'))
                    )
                    download_button.click()
                
                    # 「ダウンロード確認」ポップアップの「はい」ボタンをクリックする
                    yes_button = WebDriverWait(driver, 15).until(
                        EC.presence_of_element_located((By.CSS_SELECTOR, 'a[onclick="closeConfirmationDialog1();return false"] img[src="/content/images/yes.gif"]'))
                    )
                    yes_button.click()
                    
                    arr.append("downloaded")
                    logging(arr)
                    print(f'ダウンロードしました: {name}  {tetsuduki_name}')
                    outdate_flag = 1
                    outdate_cnt = 0
                    
                    # メッセージボックス一覧ページに戻る
                    driver.close()
                    driver.switch_to.window(messagebox_window)
                # ダウンロードボタンがない場合はエラーをキャッチして例外処理に移る    
                except Exception as e:
                    print(f'errer : DLデータなし {name}  {tetsuduki_name}')
                    arr.append("ダウンロード可能なデータが存在しません")
                    logging(arr)
                    outdate_flag = 1
                    outdate_cnt = 0

                    # メッセージボックス一覧ページに戻る
                    driver.close()
                    driver.switch_to.window(messagebox_window)
                    continue
                # アクセス権限がない場合はエラーをキャッチして処理を続行する
            # 「受信通知」ページに遷移できない場合はエラーをキャッチして処理処理に移る
            except:
                print(f'errer :アクセス権限なし {name}  {tetsuduki_name}')
                arr.append("アクセス権限なし")
                logging(arr)
                outdate_flag = 1
                outdate_cnt = 0
                continue
        except:
            print(f'errer :何らかの不具合でスキップされました {name}  {tetsuduki_name}')
            arr.append("何らかの不具合でスキップ")
            logging(arr)
            outdate_flag = 1
            outdate_cnt = 0
            
    # 次のリストがある場合は次へボタンから次のリストに遷移する
    if break_flag == False:
        continue
    nextlist_button_Xpath = '//a[@tabindex="24" and not(contains(@style, "display: none"))]'
    try:
        nextlist_button = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.XPATH, nextlist_button_Xpath))
        )
        print('\033[34m'+"次のリストに遷移します"+'\033[0m')
        nextlist_button.click()
    except:
        print("次のリストがありません")
        if past_flag == 1:
            print('\033[32m'+"過去分のダウンロードに移行します"+'\033[0m')
            past_flag = 0
            pastlist_button_Xpath = '//a[@tabindex="30"]'
            pastlist_button = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.XPATH, pastlist_button_Xpath))
            )
            pastlist_button.click()
            continue
        else:
            print('\033[32m'+"ダウンロードを終了します。"+'\033[0m')
            break
    
with open("access_histroy.txt", "w", encoding="utf-8") as f:
    for item in access_history:
        f.write(str(item) + "\n")
print('\033[34m'+"ログが出力されました"+'\033[0m')
   
print('\033[32m'+"すべての自動ダウンロードが完了しました\nEnterを押すとブラウザを閉じてスクリプトを終了します"+'\033[0m')

input()

```

## 免責
・本スクリプトは、特定の環境下での動作を確認したものであり、全ての環境において正常に動作することを保証するものではありません。e-Taxのシステム変更や、利用者の環境差異によっては、期待通りの結果が得られない可能性があります。
・本スクリプトの使用によって生じた、いかなる不利益や損害についても、筆者は一切の責任を負いません。利用者自身で自己責任でお願いします。