このシェルスクリプトは、指定したディレクトリ内のすべてのExcelファイル(.xlsx形式)をCSVファイルに変換し、別のディレクトリに保存します。変換にはPythonの xlsx2csv ライブラリを使用します。
事前準備
1. Pythonのインストール
Pythonがシステムにインストールされていることを確認してください。
2. xlsx2csv ライブラリのインストール
xlsx2csv は、ExcelファイルをCSVに変換するためのPythonライブラリです。以下のコマンドでインストールできます。
pip install xlsx2csv
シェルスクリプト
以下の内容を .sh ファイル(例: excel_to_csv.sh)として保存します。
#!/bin/bash
# 変換対象のExcelファイルが存在するディレクトリ
# 例: デスクトップにあるExcelファイルを対象にする場合
input_dir="${HOME}/Desktop"
# 変換後のCSVファイルを保存するディレクトリ
# 例: デスクトップにcsv_outputディレクトリを作成する場合
output_dir="${HOME}/Desktop/csv_output"
# 出力ディレクトリが存在しない場合は作成する
mkdir -p "${output_dir}"
# ExcelファイルをCSVに変換する関数
# 引数: $1 - 入力Excelファイルのフルパス
convert_excel_to_csv() {
local input_file="$1"
# 出力ファイル名を生成 (元のファイル名から拡張子を除き、.csvを付与)
local output_file="${output_dir}/$(basename "${input_file%.*}").csv"
echo "Converting '${input_file}' to '${output_file}'..."
# python3 -m xlsx2csv を使って変換を実行
# -m オプションは、モジュールをスクリプトとして実行することを意味する
python3 -m xlsx2csv "${input_file}" "${output_file}"
if [ $? -eq 0 ]; then # 直前のコマンドの終了ステータスが0なら成功
echo "Conversion successful: '${output_file}'"
else
echo "Conversion failed for: '${input_file}'" >&2 # エラーメッセージは標準エラー出力へ
fi
}
# 指定ディレクトリ内のすべての.xlsxファイルをループ処理
# for ... in ...; do ... done 構文
for excel_file in "${input_dir}"/*.xlsx; do
# ファイルが存在するかチェック (ファイルがない場合にループが1回実行されるのを防ぐ)
if [ -f "${excel_file}" ]; then
convert_excel_to_csv "${excel_file}"
fi
done
echo "All Excel files processed."
スクリプトの実行方法
- 上記のスクリプトを
excel_to_csv.shなどの名前で保存します。 - スクリプトに実行権限を付与します。
chmod +x excel_to_csv.sh - スクリプトを実行します。
./excel_to_csv.sh
スクリプトの解説
#!/bin/bash: このスクリプトをBashシェルで実行することを指定します。input_dir,output_dir: 変換元と変換先のディレクトリパスを定義します。${HOME}はユーザーのホームディレクトリを表します。mkdir -p "${output_dir}": 変換後のCSVファイルを保存するディレクトリが存在しない場合に作成します。-pオプションは、親ディレクトリも同時に作成し、既に存在してもエラーにしないようにします。convert_excel_to_csv()関数:local: 関数内で定義された変数がローカル変数であることを示します。basename "${input_file%.*}": ファイル名からパスと拡張子を除去します。python3 -m xlsx2csv "${input_file}" "${output_file}":xlsx2csvモジュールを実行し、ExcelファイルをCSVに変換します。if [ $? -eq 0 ]: 直前のコマンドの終了ステータス$?が0(成功)かどうかをチェックします。
for excel_file in "${input_dir}"/*.xlsx; do ... done:input_dir内のすべての.xlsx拡張子のファイルをループ処理します。if [ -f "${excel_file}" ]; then ... fi: ループ内で見つかったものが実際にファイルであるかを確認します。これは、*.xlsxにマッチするファイルがない場合にループが1回実行されてしまうのを防ぐためです。
このスクリプトは、Excelファイルを一括でCSVに変換する際に役立ちます。
ベンチマーク:外部コマンド呼び出しはどれくらい重いのか
上記のスクリプトでは、ファイルごとに basename "${input_file%.*}" を呼び出し、さらに python3 -m xlsx2csv という外部プロセスを起動しています。「for ループの中で外部コマンドを1回ずつ呼ぶ」処理は書きやすい反面、ファイル数が増えると無視できないコストになることがあります。実際にどれくらい差が出るのか、3つのパターンで計測しました。
計測環境: macOS (Apple Silicon) / GNU bash 5.3.15。各パターンを50回試行し、平均と標準偏差(stddev)を $EPOCHREALTIME(bashのマイクロ秒精度時刻)で測定しています。
1. ファイル名・拡張子の抽出(200ファイル分をループ処理)
このスクリプトの basename "${input_file%.*}" は、拡張子を除いた部分は ${input_file%.*} というbash組み込みのパラメータ展開で処理していますが、パスからファイル名部分を取り出すのに外部コマンド basename を1ファイルにつき1回forkしています。同じ処理を ${var##*/} という組み込み展開だけで書き換えた場合と比較しました。
# 方式A: 外部コマンド basename を毎回fork(元のスクリプトの書き方)
for f in "${FILES[@]}"; do
out="$(basename "${f%.*}").csv"
done
# 方式B: bash組み込みのパラメータ展開のみ(forkなし)
for f in "${FILES[@]}"; do
name="${f##*/}"
out="${name%.*}.csv"
done
| 方式 | 平均 | 標準偏差 |
|---|---|---|
A: basename を200回fork | 452.4 ms | 10.0 ms |
| B: bash組み込み展開のみ | 2.1 ms | 0.06 ms |
200回のループで 約215倍 の差が出ました。1回あたりに換算すると basename のfork単体は約2.3ms(452.4ms ÷ 200)で、後述の単発実行の計測値(1.9ms)ともほぼ一致しており、コストの大部分が「プロセスを起動するオーバーヘッド」であることが裏付けられます。
2. プロセス起動コストそのものを比較する
外部コマンド1回あたりのfork+exec自体にどれくらいのコストがかかるのか、python3 と basename を単発で1回ずつ実行して比較しました。
| コマンド | 平均 | 標準偏差 |
|---|---|---|
python3 -c 'pass'(単発) | 21.7 ms | 0.32 ms |
basename foo.xlsx(単発) | 1.9 ms | 0.06 ms |
このスクリプトが実際に .xlsx ファイル1つにつき起動する python3 -m xlsx2csv は、Pythonインタプリタの起動だけで約22msかかります。つまり、このスクリプトの実行時間のボトルネックは basename の呼び出し方ではなく、ファイルごとに避けられないPython起動コストにあります。ファイル数が数百・数千規模になる場合は、シェル側の細かい最適化よりも、変換処理自体をPythonスクリプト側でループさせる(Pythonプロセスを1回だけ起動し、その中で全ファイルを処理する)方が効果は大きいでしょう。
3. ループ内の外部コマンド呼び出し vs バッチ実行
一般化した例として、複数ファイルに対して同じ外部コマンドを「1ファイルずつループで呼ぶ」場合と「1回の呼び出しにまとめる」場合も比較しました。200個のログファイルから特定の文字列を含むファイルを探す処理です。
# 方式E: forループの中でgrepを1ファイルずつ呼ぶ(200回fork)
for f in "${LOGFILES[@]}"; do
if grep -q "failed" "$f"; then
count=$((count + 1))
fi
done
# 方式F: 全ファイルをまとめて1回のgrep呼び出しで処理
count=$(grep -l "failed" "${LOGFILES[@]}" | wc -l)
| 方式 | 平均 | 標準偏差 |
|---|---|---|
E: grep をファイルごとに200回fork | 473.5 ms | 17.6 ms |
F: 全ファイルをまとめて1回の grep | 26.8 ms | 4.4 ms |
こちらも 約18倍 の差になりました。grep や sed、awk などはそもそも複数ファイルを引数に取れるように作られているため、「forループで1件ずつ渡す」よりも「配列やワイルドカードでまとめて渡す」方が速く、かつスクリプトの行数も短くなります。

まとめ
- パスからファイル名を取り出すだけなら、外部コマンド
basenameより bash組み込みのパラメータ展開(${var##*/}や${var%.*})の方が 200倍以上 速く、依存コマンドも減らせる。 - 一方で、今回のスクリプトのように
python3 -m xlsx2csvを1ファイルごとに起動する処理は、Pythonインタプリタの起動コスト(約22ms/回)自体が支配的なので、シェル側を組み込み展開に置き換えても全体の実行時間への影響は小さい。ボトルネックがどこにあるかを見極めてから最適化するのが重要。 grep・sed・awkのように複数ファイルをまとめて扱えるコマンドは、forループで1ファイルずつ呼ぶよりバッチで渡した方が大幅に速い(今回の例で約18倍)。ファイル一覧を配列に入れて一括で渡せないか、まず検討する価値がある。
よくある質問(FAQ)
Q. 古い形式の.xlsファイルも変換できますか?
このスクリプトはfor excel_file in "${input_dir}"/*.xlsx; do ... doneというループ条件で対象ファイルを絞り込んでいるため、拡張子が.xlsxのファイルのみが変換対象になる。.xls(Excel 97-2003形式)のファイルはこのグロブにマッチしないため変換されない。.xlsも対象にしたい場合は、ループ条件やファイルの拡張子判定を変更する必要がある。
Q. 変換に失敗したファイルがあると、スクリプト全体が止まりますか?
止まらない。convert_excel_to_csv()関数の中でpython3 -m xlsx2csvの終了ステータスをif [ $? -eq 0 ]でチェックし、失敗した場合は"Conversion failed for: '${input_file}'"というメッセージを標準エラー出力に表示するだけで、forループ自体は次のファイルの処理を継続する。どのファイルが失敗したかは、実行時に出力されるログで確認できる。
Q. 変換対象のファイル数が増えると、なぜ処理が遅くなるのですか?
記事後半のベンチマークで計測した通り、このスクリプトはExcelファイル1つにつきpython3 -m xlsx2csvという外部プロセスを起動しており、Pythonインタプリタの起動だけで1回あたり約22msかかる。ファイル数が数百・数千規模になると、この起動コストの積み重ねが処理時間の大部分を占めるようになる。basename呼び出しをbash組み込み展開に置き換えるような細かい最適化よりも、Python側で全ファイルをまとめてループさせるなど、外部プロセスの起動回数そのものを減らす方が効果が大きい。
関連ツール
- Chmod計算機(DevToolBox) - ファイルパーミッションの数値・記号表記を変換