pythonで複数のExcelファイルからの文字列検索

テキストエディタのGrep機能感覚で、複数Excelファイルから手軽に文字列検索するツールを作成したので、その実装方法をご紹介します。

# 背景
 * 複数Excel検索の一括検索をするシーンが多い
   * 仕様の調査
   * 設計書のレビュー
   * 過去議事録の読み返し
 * 多くのサンプルソースが公開されているが、自分好みのものを特定、選別するのが大変
 * AIを使って自分で好みのツールを作った方が速い

スケジュールがタイトで短時間で多くのレビューをこなさなくてはならず困っていました:joy:
ウォーターフォール開発はレビュー品質を高めることで防げる手戻りや障害が沢山あるのです:point_up:


# ツールの要件
 * 検索対象Excelファイル数が多くても、レスポンスが速い（重要要件）
 * 検索対象指定フォルダを複数指定できる
 * 検索対象指定したフォルダのサブフォルダも検索対象に含める
 * 検索結果をTxtファイルに保存できる
 （同じキーワードで検索しなくて済むよう、TxtファイルをRenameして保存する意図）
 
:writing_hand_tone1:動作環境はWindows11、Python 3.12、Excel2021

# ツールの実装
## 機能検討
 * 検索対象パスは外部ファイル「Search.txt」から読ませる
```Search.txt（記入例）
# 検索対象パス
C:\Users\USER\hogePj\10.管理
C:\Users\USER\hogePj\20.要件定義
C:\Users\USER\hogePj\30.基本設計

# 検索文字列
顧客ID
```

 * 処理結果は「Result.txt」に出力
SakuraエディタなどのGrep機能に近いイメージでOK。

 * 「Search.txt」、「Result.txt」ともに、pythonプログラムのある同フォルダに配置


## 実装準備

:::note info
ライブラリOpenPyXl[^openpyxl]をインストールしよう。
:::
下記コマンドを実行すればOK。
`pip install pandas openpyxl`


## OpenPyXL公式リファレンス

https://openpyxl.readthedocs.io/en/stable/


## 実装方法
:::note info
コーディングはChatGPTに任せて、要件の検討と意思決定に注力。
:::

 1. ChatGPTに要件を伝えて、「pythonでソース書いて下さい」と入力
 1. ChatGPTが出力したソースを、VSCodeに貼り付けて実行してみる
 1. 改良要件を「〇〇の部分を✕✕するように修正してください」と入力
 1. 自分の意図通りの動作になるまで、2~3を繰り返す 
 
:point_up:実行エラーも「xx行目の変数hogehogeでint型変換エラーが発生しています。明示的にxxするよう修正して下さい」のように入力すればOK。

## 完成したソース
```ExecelGrep.py
import os
import pandas as pd

def search_in_excel(folder_paths, search_string):
    results = []
    # 指定された各フォルダを処理
    for folder_path in folder_paths:
        # フォルダ内の全Excelファイルを取得（サブフォルダ含む）
        for dirpath, _, filenames in os.walk(folder_path):
            for filename in filenames:
                # "~$"で始まるファイルを除外
                if filename.startswith("~$"):
                    continue
                if filename.endswith(('.xlsx', '.xls', '.xlsm')):
                    file_path = os.path.join(dirpath, filename)
                    try:
                        # Excelファイルを全てのシートを読み込む
                        df = pd.read_excel(file_path, sheet_name=None, header=None)
                        for sheet_name, sheet_data in df.items():
                            # 各シート内のセルを検索
                            for row_index in range(sheet_data.shape[0]):  # 行をループ
                                for col_index in range(sheet_data.shape[1]):  # 列をループ
                                    cell = sheet_data.iloc[row_index, col_index]  # 行と列のインデックスでセルを取得
                                    # 検索対象の文字列が含まれているか確認
                                    if isinstance(cell, str) and search_string in cell:
                                        results.append((file_path, sheet_name, (row_index, col_index), cell))
                    except Exception as e:
                        print(f"Error reading {file_path}: {e}")
    return results

def read_search_config(file_path):
    folder_paths = []
    search_string = ""
    with open(file_path, 'r', encoding='utf-8') as f:
        current_section = None
        for line in f:
            line = line.strip()
            if not line:
                continue
            if line == "# 検索対象パス":
                current_section = "folder_paths"
            elif line == "# 検索文字列":
                current_section = "search_string"
            elif current_section == "folder_paths":
                folder_paths.append(line)
            elif current_section == "search_string":
                search_string = line
    return folder_paths, search_string

# Pythonスクリプトが存在するディレクトリを取得
script_dir = os.path.dirname(os.path.abspath(__file__))

# Search.txtから設定を読み込む
search_config_path = os.path.join(script_dir, 'Search.txt')
folder_paths, search_string = read_search_config(search_config_path)

# 検索を実行
results = search_in_excel(folder_paths, search_string)

# 結果をファイルに出力
output_file_path = os.path.join(script_dir, 'result.txt')
with open(output_file_path, 'w', encoding='utf-8') as file:
    file.write(f"search_keyword: {search_string}\n")
    for result in results:
        file_path, sheet_name, (row_index, col_index), cell_value = result
        file.write(f"Found in {file_path} -> {sheet_name} (Row: {row_index+1}, Col: {col_index+1}) - Value: {cell_value}\n")

print(f"Results have been written to {output_file_path}")
```
ChatGPT様、ありがとうございます。コメントも親切で、解読性が高くデバッグしやすい。


## 実行の様子
1.Search.txtを用意
2.pythonを実行
3.result.txtが生成される
![gqp9mKPBj315yuczstsC1728721753-1728722051.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3315076/0b36f912-9bfa-2410-2abc-898c84fa8053.gif)
4.実際のExcelの中身と、検索Hitしたセル位置の出力内夜が一致していることも確認済み。

以上で、簡単な動作確認が完了。

## ファイル数を増やして、処理速度性能を試した

 * Excelファイルが40~100個程度あるフォルダを4つ指定
 * 検索実行の結果、100か所近くHit

実行時間は2分程度:v:

# さいごに
 * pythonのExcel操作はシンプル、VBAで同ツールを作るより処理速度は高い
 * 普段はコーディングしておらず、pythonの細かい文法を忘れてる私でも爆速でツールが完成した
 * 実行エラーやこちらの意図と違う動作はあるため、ソースが読めて原因の行や理由を特定する勘所は必要(そこさえできれば、ChatGPTに修正を依頼するだけ）
 * 皆さんもChatGPTで自分好みのツールを作って、生産性アップにチャレンジしてみよう


# 番外編（おまけの豆知識）
Win+Shift+Sだと右クリックメニューを表示したままのスクショが取れない💦
そんな悩みに遭遇したことがある方へ
私も本記事のアニメーションGifを作成するときに一瞬はまって、下記で解決しました:joy:

:::note info
右クリックメニューを表示した状態でスクリーンショットを取る方法
　Ctrl + PrintScreen
:::

[^OpenPyXl]: OpenPyXl：PythonでExcelファイルを読み書きするためのライブラリ