自動化をもっと手軽に！Pythonスクリプトをexe化するステップ解説

![近未来灯台.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/553185f3-1d4a-008f-bc39-f97918b81bef.jpeg)

# はじめに

以前、娘のためにWebサイトへの予約作業を自動化する仕組みを作成し、その内容を記事にまとめました。

https://qiita.com/ogi_kimura/items/4ec8c7c29b0669426276

この自動化スクリプトを使うことで、私のPCでは予約処理が問題なく実行できています。しかし、この仕組みをそのまま娘のPCで動かそうとすると、動作しません。その理由は、娘のPCにはPythonやGoogle Chromeのドライバなど必要な環境が一切整っていないからです。

この予約処理は毎月1回、月初の深夜0:00に実行する必要があります。現在の状態では、私のPCでしか動作しないため、毎月深夜に立ち会って処理を実行する必要があります。しかし、私自身は仕事の都合で月初の営業日は早朝出勤が必要です。このままだと、深夜に娘の予約対応を行い、そのまま早朝から仕事に向かうという、非常にハードなスケジュールをこなさなければなりません。

そこで、娘のPC（Windows）でも予約処理が自動的に動作するように「exe化」を行うことにしました。さらに、exe化により頻繁なソースコードの変更が難しくなることを考慮し、現在はソースコード内に直接記載している「月日」の設定を「csvファイル」に外部出力して管理することにしました。


# ソースコード

前回からの変更点として、以下の改善を行いました：

1. **CSVファイルによるインプット情報の外部管理**  
   月日を事前にCSVファイルへ入力し、ソースコードから外部で管理できるようにしました。これにより、設定を変更する際に直接コードを修正する必要がなくなりました。

2. **ログ出力機能の充実**  
   予約処理の成功・失敗を確実に把握できるよう、ログ出力機能を強化しました。これにより、実行結果や問題点を簡単に追跡できるようになりました。

3. **Chromedriverの配置場所を明確化**  
   chromedriverのフォルダを`C:`直下に配置する仕様に統一しました。この変更により、環境設定が簡単になり、トラブルを防ぐことができます。

これらの変更により、コードの柔軟性と実用性が向上し、よりスムーズに運用できるようになりました。

```python:app.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import time
import logger
import util
import subprocess


# --- 先ず、以下を実行し、デバッグモードのブラウザを立ちあげる。 ---
command = r'"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\selenium\chrome-profile'
subprocess.Popen(command, shell=True)
time.sleep(10)

chrome_service = Service(executable_path='c:\chromedriver-win64\chromedriver.exe')

# Chromeオプションを設定
chrome_options = Options()
chrome_options.debugger_address = "localhost:9222"  # デバッグモードのポート番号に合わせる

# 既に開いているChromeブラウザに接続
driver = webdriver.Chrome(service=chrome_service, options=chrome_options)

# データ登録の操作を実行
# ここにRPAを実施したいURLを登録します。
driver.get('https://***************************************')

# 現在開かれているタブのハンドルを取得
original_window = driver.current_window_handle

# ===================================================================
# ========================= メイン関数 ===============================
# ===================================================================
def main(): 

    login_flag = 0

    # ==================== 施設予約システム ======================
    logger.debug('施設予約システム START')
    # フレームに切り替え
    driver.switch_to.frame("MainFrame")  # フレーム名 'MainFrame' を使用
    
    # "バスケットボール" のリンクをクリック
    util.link_click(driver, "バスケットボール", 0)
    
    
    # ==================== 所在地選択 ======================
    # "京都市南区" のリンクをクリック
    util.link_click(driver, "京都市**", 0)
    
    # "検索" をクリック
    xpath = "//input[@class='clsImage']"
    util.xpath_click(driver, xpath, 7)
    

    # ==================== 会館選択 ======================
    # "抽選" をクリック
    xpath = "//input[@alt='予約画面へ']"
    util.xpath_click(driver, xpath, 1)
    
    
    # ==================== 日程選択 ======================
    util.read_csvfile()
    # "該当月" をクリック
    util.link_click(driver, f"{util.SELECT_MONTH}月", 0)
    
    logger.info(f'★★★★★  {util.SELECT_MONTH}月の登録を実施★★★★★')
    
    for index in util.SELECT_DAY:
        index = index.strip()

        # 念のため、クリアしておく
        xpath = "//input[@name='btn_clear']"
        util.xpath_click(driver, xpath, 0)
        
        # ポップアップ画面
        util.popup_click(driver, util.POPUP_ACCEPT)
        
        # "該当日" のリンクを取得
        logger.info(f'★★★★★       {str(index)}日の登録を実施★★★★★')
        
        # "該当日" のリンクをクリック
        util.link_click(driver, str(index), 0)
        
        # ************ 条件設定 ************
        # "予約可能" のリンクを取得
        #    image000000000：9:30開始   image000001000：13:00開始    image000002000：16:00開始   image000003000：19:00開始
        xpath = "//img[@name='image000001000' and @alt='予約可能']"
        select_element = util.get_elements_xpath(driver, xpath)
        
        if select_element:

            # クリック
            select_element[0].click()
            time.sleep(util.SLEEP_TIME)
        
            logger.info(f'＝＝＝＝＝  {util.SELECT_MONTH}月{str(index)}日の予約をします ＝＝＝＝＝')    
            # ==================== 面施設選択処理画面 ======================
            # 現在のウィンドウハンドルを取得
            original_window = driver.current_window_handle
            # すべてのウィンドウハンドルを取得
            all_windows = driver.window_handles

            # 新しいウィンドウに切り替える
            util.switch_window(driver, all_windows, original_window)

            # ************ 条件設定 ************
            # "予約可能" のリンクを取得
            # 　　　image0：全面  image1：A面  image2：B面
            # xpath = "//img[( @name='image1' or @name='image2' ) and @alt='予約可能 ']"
            xpath = "//img[@name='image0' and @alt='予約可能 ']"
            select_element2 = util.get_elements_xpath(driver, xpath)
            if select_element2:
                
                # クリック
                select_element2[0].click()
                time.sleep(util.SLEEP_TIME)
                
                # "閉じる" のリンクを取得
                xpath = "//input[@name='btn_close']"
                util.xpath_click(driver, xpath, 0)
                
                # ==================== 日程選択 ======================
                # フレームに切り替え
                driver.switch_to.window(original_window)
                driver.switch_to.frame("MainFrame")  # フレーム名 'MainFrame' を使用
                
                # "次へ" のリンクを取得
                xpath = "//input[@alt='次へ']"
                util.xpath_click(driver, xpath, 0)
                
                # ==================== ログイン画面 ======================
                if login_flag == 0:
                    xpath = "//input[@name='txt_usr_cd']"
                    util.set_textbox(driver, xpath, util.KYOTO_FACILITY_USERNAME, 0)
                    
                    # "パスワード" のテキストボックス
                    xpath = "//input[@name='txt_pass']"
                    util.set_textbox(driver, xpath, util.KYOTO_FACILITY_PASSWORD, 0)
                    
                    # "OK" のリンクを取得
                    xpath = "//input[@alt='ＯＫ']"
                    util.xpath_click(driver, xpath, 0)
                    
                    login_flag = 1
                
                # ==================== 紹介画面 ======================
                # "次へ" のリンクを取得
                xpath = "//input[@alt='次へ']"
                util.xpath_click(driver, xpath, 0)

                # ==================== 利用人数画面 ======================
                # "利用人数" のテキストボックス
                xpath = "//input[@name='NUM_1']"
                util.set_textbox(driver, xpath, '10', 0)
                
                # "次へ" のリンクを取得
                xpath = "//input[@alt='次へ']"
                util.xpath_click(driver, xpath, 0)

                # ==================== 予約確認画面 ======================
                # "予約" のリンクを取得
                xpath = "//input[@alt='予約']"
                util.xpath_click(driver, xpath, 0)
            
                # ==================== 内容確認ダイアログ ====================== 
                util.popup_click(driver, util.POPUP_DISMISS)
                logger.info(f'＝＝＝＝＝  {util.SELECT_MONTH}月{str(index)}日の予約は完了しました ＝＝＝＝＝')
                
                # ==================== 画面戻し作業(1) ======================
                # "戻る" のリンクを取得
                xpath = "//input[@name='btn_back']"
                util.xpath_click(driver, xpath, 0)
                
                # ==================== 画面戻し作業(2) ======================
                # "戻る" のリンクを取得
                xpath = "//input[@name='btn_back']"
                util.xpath_click(driver, xpath, 0)

                # ==================== 画面戻し作業(3) ======================
                # "戻る" のリンクを取得
                xpath = "//input[@name='btn_back']"
                util.xpath_click(driver, xpath, 0)
            
            else:
                # "閉じる" のリンクを取得
                xpath = "//input[@name='btn_close']"
                util.xpath_click(driver, xpath, 0)
                logger.info(f'＝＝＝＝＝  A面・B面が空いていない為、{util.SELECT_MONTH}月{str(index)}日の予約はできませんでした ＝＝＝＝＝') 
            
        else:
            logger.info(f'＝＝＝＝＝  19:00が空いていない為、{util.SELECT_MONTH}月{str(index)}日の予約はできませんでした ＝＝＝＝＝')    
    
    
    # ==================== 終了報告 ====================== 
    print("\n★終了★")



if __name__ == '__main__': 
    logger.debug('============== START ==============')
    main()
    logger.debug('============== END ==============')
```

```python:util.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import os
import logger
import csv

SLEEP_TIME = 3

KYOTO_FACILITY_USERNAME = os.environ.get('KYOTO_FACILITY_USERNAME')
KYOTO_FACILITY_PASSWORD = os.environ.get('KYOTO_FACILITY_PASSWORD')

SELECT_MONTH = 0
SELECT_DAY = []

POPUP_ACCEPT = 1
POPUP_DISMISS = 2

file_path = 'input.csv'

# CSVファイルを読み込む
def read_csvfile():
    global SELECT_MONTH, SELECT_DAY
    file = open(file_path, mode='r', encoding='utf-8')
    index = 0
    reader = csv.reader(file)
    for row in reader:
        if index == 0:
            SELECT_MONTH = row[0]
        else:
            SELECT_DAY = row
        index = index + 1


def link_click(driver, link_text, index):
    logger.debug(f'link_text START {link_text}')
    # エレメント取得
    basketball_element = driver.find_elements(By.LINK_TEXT, link_text)
    # クリック
    basketball_element[index].click()
    logger.debug(f'link_text END {link_text}')
    time.sleep(SLEEP_TIME)


def xpath_click(driver, xpath_text, index):
    logger.debug(f'xpath_click START {xpath_text}')
    # エレメント取得
    element = driver.find_elements(By.XPATH, xpath_text)
    # クリック
    element[index].click()
    logger.debug(f'xpath_click END {xpath_text}')
    time.sleep(SLEEP_TIME)


def get_elements_xpath(driver, xpath_text):
    logger.debug(f'get_element_xpath START {xpath_text}')
    # エレメント取得
    elements = driver.find_elements(By.XPATH, xpath_text)
    logger.debug(f'get_element_xpath END {xpath_text}')
    return elements


def popup_click(driver, f):
    logger.debug(f'popup_click START {f}')
    alert = driver.switch_to.alert
    if f == POPUP_ACCEPT:
        alert.accept()
    else:
        alert.dismiss()
    logger.debug(f'popup_click END {f}')
    time.sleep(SLEEP_TIME)


def switch_window(driver, all_windows, original_window):
    logger.debug(f'switch_window START')
    for window in all_windows:
        if window != original_window:
            driver.switch_to.window(window)
            break
    time.sleep(SLEEP_TIME)
    logger.debug(f'switch_window END')


def set_textbox(driver, xpath_text, value, index):
    logger.debug(f'set_textbox START {value}')
    # エレメント取得
    textbox = driver.find_elements(By.XPATH, xpath_text)
    textbox[index].clear()
    textbox[index].send_keys(value)
    logger.debug(f'set_textbox END {value}')
    time.sleep(SLEEP_TIME)
```

```python:logger.py
import logging
import inspect

loglevel = 'info'

if loglevel == 'debug':
    LOG_LEVEL = logging.DEBUG
elif loglevel == 'info':
    LOG_LEVEL = logging.INFO
elif loglevel == 'warning':
    LOG_LEVEL = logging.WARNING
elif loglevel == 'error':
    LOG_LEVEL = logging.ERROR
elif loglevel == 'critical':
    LOG_LEVEL = logging.CRITICAL
else:
    LOG_LEVEL = logging.INFO

# ロガーを動的に取得する関数を定義
def get_dynamic_logger():
    # 呼び出し元のモジュール名を取得
    frame = inspect.stack()[2]
    module_name = inspect.getmodule(frame[0]).__name__
    logger = logging.getLogger(module_name)
    logger.setLevel(LOG_LEVEL)
    
    # ハンドラとフォーマッターを設定
    if not logger.handlers:  # 重複追加を防ぐ
        console_handler = logging.StreamHandler()
        console_handler.setLevel(LOG_LEVEL)

        file_handler = logging.FileHandler('app.log', encoding='utf-8')
        file_handler.setLevel(LOG_LEVEL)

        formatter = logging.Formatter('%(asctime)s - %(filename)s - %(lineno)s - %(levelname)s - %(message)s')
        console_handler.setFormatter(formatter)
        file_handler.setFormatter(formatter)

        logger.addHandler(console_handler)
        logger.addHandler(file_handler)
    return logger

# 動的ロガーを使用する関数
def debug(text):
    logger = get_dynamic_logger()
    logger.debug(text, stacklevel=2)

def info(text):
    logger = get_dynamic_logger()
    logger.info(text, stacklevel=2)

def warning(text):
    logger = get_dynamic_logger()
    logger.warning(text, stacklevel=2)

def error(text):
    logger = get_dynamic_logger()
    logger.error(text, stacklevel=2)

def critical(text):
    logger = get_dynamic_logger()
    logger.critical(text, stacklevel=2)
```

# 入力ファイル(CSVファイル)
以下、入力ファイルです。1行目に「月」2行目に「日」を記入します。
娘の予約設定日は、火曜日・木曜日で且つ祝日の場合は次の日ということですので、2025年2月はこのようにしています。

```text:input.csv
2
4, 6, 13, 18, 22, 25, 27
```

# exeファイル作成

ここからは「exeファイル」の作成手順について説明します。  
参考にさせていただいたのは以下の記事です。  
@nal_dal_dere (磯村 なる)さん、分かりやすい記事をありがとうございました！

[参考記事](https://qiita.com/nal_dal_dere/items/95e173068af399e61981)

### 手順

1. **ライブラリのインストール**  
   まず、`pyinstaller`をインストールします。

   ```bash
   pip install pyinstaller
   ```

   これで必要なライブラリがインストールされました。

2. **Pythonスクリプトをexe化**  
   次に、対象のスクリプト（例: `app.py`）を「exeファイル」に変換します。

   ```bash
   pyinstaller app.py
   ```

   実行後、以下の成果物が生成されます：
   - `exeファイル`  
   - `_internal`という名前のフォルダ

   このフォルダは、「exeファイル」と同じ階層に配置する必要があると考えられます。これを怠ると、正しく実行できない可能性があります。

3. **配布準備**  
   上記で生成された`exeファイル`と`_internal`フォルダをセットにして、娘のPCに持ち込むことにします。

これで、exeファイルの作成と配布準備が完了です！


# 環境変数の設定

前回の記事でもお伝えしましたが、`GitHub`にソースコードを公開する際に、ユーザー情報やパスワード情報をソースコードや設定ファイル（例: イニシャルファイル）に記載するのは非常に危険です。  
これらの情報は**環境変数**として設定し、外部から参照する形を取ることを強くお勧めします。ご自身で取得した情報は環境変数に登録し、セキュリティを確保してください。


# Google Chromeのインストール

今回のスクリプトは`Google Chrome`ブラウザを操作対象としています。そのため、以下の手順を事前に行ってください：

1. **Google Chromeをインストール**  
   PCに`Google Chrome`がインストールされていない場合、先にインストールしてください。

2. **Chromeのバージョン確認**  
   インストール後、**必ず**Chromeのバージョンを確認してください。  
   これが次に説明する`ChromeDriver`のバージョンと一致していない場合、処理エラーが発生します。


# ChromeDriverのダウンロード

次に、`ChromeDriver`をダウンロードします。以前の記事で紹介した方法を参考に、正しい手順で進めてください。

[ChromeDriverのダウンロード方法](https://qiita.com/ogi_kimura/items/db22e2697eb1a4def0c9#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%83%A2%E3%83%BC%E3%83%89)

### 注意点
1. **Driverのバージョン確認**  
   ダウンロードする際、必ず`Google Chrome`のバージョンに対応する`ChromeDriver`を選択してください。

2. **ダウンロード後のファイル配置**  
   - ダウンロードした`zip`ファイルを解凍してください。
   - 解凍したファイルを`Cドライブ`直下に配置します。  
     必要に応じて、ソースコードを修正し、`ChromeDriver`を「exeファイル」と同じフォルダに置くよう設定しても構いません。

これらの手順を事前に実施しておくことで、スクリプトが正しく動作する環境を整えられます。



# 実行
娘のPC（Windows 11）で、作成した`app.exe`をダブルクリックして実行したところ、問題なく処理が進みました。  
Pythonをインストールせずとも、予約確定の直前まで（実際の予約は費用がかかるため行いませんでした）正しく動作することを確認できました。


# おわりに
今回、「exeファイル化」を実現できたことは、私にとって非常に大きな一歩でした。（「そんなことも知らなかったのか」と思われるかもしれませんが…！）  
これにより、PythonがインストールされていないWindows PCでも自動化スクリプトを動かせることが分かり、自信を深めることができました。

今後、ランサーズやクラウドワークスなどのプラットフォームで、「生成AI」関連のサービスを提供しようと考えていますが、今回のように「exeファイル化」することで、利用者がより簡単に自動処理を実行できるようになると感じています。


最後までお読みいただき、ありがとうございました！