Excel階層から一括でフォルダ構造を作るスクリプト
プロジェクト立ち上げのたびにエクスプローラで「新規フォルダー」を何十回もクリックする手間を、1回のExcel入力に集約。列=階層の構造で記入したExcelを読み込み、`os.makedirs` で全フォルダを一括作成します。「ひな形を出力」モードで凡例とサンプル付きの空Excelも生成でき、Excelに不慣れなメンバーへ依頼する場合にも使えます。
「新規プロジェクトのフォルダ構成、ひな形だけ作っておいて」と頼まれて、エクスプローラで右クリック→新規フォルダーを延々と繰り返した経験はありませんか?このスクリプトはExcelの列を階層として読み取り、ネスト構造のフォルダを `os.makedirs` で一括作成します。最初に「ひな形を出力」モードで凡例付きの空Excelを生成できるので、Pythonを書かないメンバーにも依頼しやすい運用です。
このスクリプトでできること
- Excel列を階層として読むネスト構造の一括作成
- 凡例とサンプル付きのひな形Excelを自動生成
- Windowsで禁止のフォルダ名文字を実行前にバリデーション
- 既存フォルダはスキップ、新規分のみ作成
- 列を増やせば何階層でも対応(5階層・6階層も自由)
.pybes ファイルをPybesにインポートすると、スクリプトと設定フィールドが自動で読み込まれます。
設定フィールド
このスクリプトで使用する設定フィールドです。Pybes上でGUIから値を入力できます。
mode ドロップダウン 必須 モード
「ひな形を出力」で記入用Excelを生成、「フォルダを作成」で記入済みExcelからフォルダ構造を一括作成します。
excel_file ファイル 必須 Excelファイル
「フォルダを作成」モードでのみ使用。先に出力したひな形Excelに階層を記入したxlsxファイルを選びます。ひな形出力モードでは空のままでOK。
folder フォルダ 必須 フォルダ
ひな形Excelの保存先(モードA)または、フォルダを作成するルート(モードB)。
コード解説
import sys
import json
import os
import re
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
with open(sys.argv[1], encoding="utf-8") as f:
inputs = json.load(f)
mode = inputs["モード"]
folder = inputs["フォルダ"]
excel_path = inputs["Excelファイル"]
INVALID_CHARS = r'\/:*?"<>|'
INVALID_PATTERN = re.compile(r'[\\/:*?"<>|]')
def validate_name(name):
found = set(INVALID_PATTERN.findall(name))
return sorted(found)
try:
# ── ひな形を出力 ──────────────────────────────
if mode == "ひな形を出力":
wb = Workbook()
ws_data = wb.active
ws_data.title = "データ"
# ヘッダー行(デフォルト4列、列を追加すれば何階層でも対応)
DEFAULT_LEVELS = 4
header_fill = PatternFill(fill_type="solid", fgColor="4472C4")
header_font = Font(bold=True, color="FFFFFF")
for col_i in range(1, DEFAULT_LEVELS + 1):
cell = ws_data.cell(row=1, column=col_i, value=f"第{col_i}階層")
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center")
ws_data.column_dimensions[get_column_letter(col_i)].width = 20
# 凡例シート
ws_legend = wb.create_sheet(title="凡例")
section_font = Font(bold=True, color="FFFFFF")
section_fill = PatternFill(fill_type="solid", fgColor="4472C4")
sample_fill = PatternFill(fill_type="solid", fgColor="E9F0FB")
indent_fill = PatternFill(fill_type="solid", fgColor="F5F5F5")
ws_legend["A1"] = "📋 フォルダ作成ツール ── 入力方法"
ws_legend["A1"].font = Font(bold=True, size=13)
ws_legend.merge_cells("A1:D1")
ws_legend["A3"] = "ルール"
ws_legend["A3"].font = section_font
ws_legend["A3"].fill = section_fill
ws_legend.merge_cells("A3:D3")
rules = [
"・A列(第1階層)が一番上のフォルダ、B列(第2階層)がその中のフォルダ、C列以降も同様です",
"・値を入力した列が、その行のフォルダの深さになります",
"・列を右に追加すれば何階層でも対応できます(第5階層・第6階層…と自由に増やせます)",
"・空白行はスキップされます",
f"・フォルダ名に使えない文字: {INVALID_CHARS}",
]
for i, rule in enumerate(rules, start=4):
ws_legend[f"A{i}"] = rule
ws_legend.merge_cells(f"A{i}:D{i}")
ws_legend["A10"] = "サンプル"
ws_legend["A10"].font = section_font
ws_legend["A10"].fill = section_fill
ws_legend.merge_cells("A10:D10")
for col_i, h in enumerate(["第1階層", "第2階層", "第3階層", "第4階層"], start=1):
cell = ws_legend.cell(row=11, column=col_i, value=h)
cell.font = Font(bold=True)
cell.fill = PatternFill(fill_type="solid", fgColor="BDD7EE")
cell.alignment = Alignment(horizontal="center")
sample_rows = [
["プロジェクトA", "", "", ""],
["", "資料", "", ""],
["", "", "2024年度", ""],
["", "", "2025年度", ""],
["", "議事録", "", ""],
["", "", "定例会議", ""],
["プロジェクトB", "", "", ""],
["", "資料", "", ""],
["", "報告書", "", ""],
]
for row_i, row_data in enumerate(sample_rows, start=12):
for col_i, val in enumerate(row_data, start=1):
cell = ws_legend.cell(row=row_i, column=col_i, value=val)
cell.fill = sample_fill if val else indent_fill
ws_legend["A22"] = "↑ この例だと「プロジェクトA/資料/2024年度」「プロジェクトA/資料/2025年度」「プロジェクトA/議事録/定例会議」「プロジェクトB/資料」「プロジェクトB/報告書」が作成されます"
ws_legend["A22"].font = Font(color="808080", italic=True)
ws_legend.merge_cells("A22:D22")
for col in ["A", "B", "C", "D"]:
ws_legend.column_dimensions[col].width = 22
save_path = os.path.join(folder, "フォルダ作成ひな形.xlsx")
wb.save(save_path)
print(f"ひな形を出力しました: {save_path}")
# ── フォルダを作成 ────────────────────────────
elif mode == "フォルダを作成":
if not excel_path:
print("エラー: Excelファイルを指定してください", file=sys.stderr)
sys.exit(1)
wb = load_workbook(excel_path, data_only=True)
ws = wb["データ"]
max_col = ws.max_column
print(f"列数を検出: {max_col} 列(最大 {max_col} 階層)\n")
# 2行目以降を読み込み(1行目はヘッダー)
all_rows = []
for row in ws.iter_rows(min_row=2, values_only=True):
all_rows.append([str(c).strip() if c is not None else "" for c in row])
# バリデーション
errors = []
for row_i, row in enumerate(all_rows, start=2):
for col_i, val in enumerate(row):
if val:
bad = validate_name(val)
if bad:
errors.append(f"{row_i}行目 {col_i+1}列目「{val}」に使えない文字が含まれています: {' '.join(bad)}")
if errors:
print("⚠️ 以下の問題が見つかりました。Excelを修正してから再実行してください。\n", file=sys.stderr)
for e in errors:
print(f" ・{e}", file=sys.stderr)
sys.exit(1)
print("✅ バリデーション OK\n")
# 階層を追いながらパスを構築
current_path = {}
paths_to_create = []
for row in all_rows:
level = None
value = None
for col_i, val in enumerate(row):
if val:
level = col_i
value = val
break
if level is None:
continue
current_path[level] = value
for deeper in list(current_path.keys()):
if deeper > level:
del current_path[deeper]
parts = [current_path[l] for l in sorted(current_path.keys())]
full_path = os.path.join(folder, *parts)
paths_to_create.append((full_path, "/".join(parts)))
print(f"{len(paths_to_create)} 件のフォルダを検出しました\n")
created = 0
skipped = 0
for full_path, display in paths_to_create:
if not os.path.exists(full_path):
os.makedirs(full_path)
print(f"作成: {display}/")
created += 1
else:
print(f"スキップ(既存): {display}/")
skipped += 1
print(f"\n完了 ── 作成: {created} 件 / スキップ: {skipped} 件")
print(f"保存先: {folder}")
except Exception as e:
print(f"エラー: {e}", file=sys.stderr) 標準ライブラリ(sys / json / os / re)と openpyxl 関連モジュール(Workbook / load_workbook と styles / utils 群)を読み込みます。Workbook は新規作成、load_workbook は既存Excel読み込み用です。sys.argv[1] で渡されたJSON設定ファイルを inputs 辞書に展開し、mode / folder / excel_path の3変数に取り出します。Excelファイル は allow_empty: true なので、ひな形出力モードでは空文字で届きます。
Windowsでフォルダ名に使えない文字 \\/:*?"<>| を re.compile で1度だけパターン化し、validate_name で違反文字の集合を返します。set で重複を除いてから sorted で並べ替えるので、1セルに同じ違反文字が複数あってもエラーメッセージはシンプルになります。INVALID_CHARS の方は表示用の文字列で、凡例シートとエラーメッセージの両方で再利用しています。
ひな形出力モード。Workbook() で新規ブックを作り、デフォルトシートを データ にリネームしたあと、第1階層 〜 第4階層 の見出しを濃紺背景+白文字で配置します。続いて wb.create_sheet で 凡例 シートを追加し、ルール文・サンプル表(プロジェクトA/プロジェクトBの構造)・最後の説明文を順に配置していきます。merge_cells でセル結合、PatternFill で塗り分け、Alignment で中央寄せまで設定し、受け取った非エンジニアが迷わず記入できる体裁に仕上げます。最後に フォルダ作成ひな形.xlsx として保存します。
フォルダ作成モード。load_workbook を data_only=True で開き(数式の結果値だけが必要)、データ シートの全セルをstr化+strip。次のバリデーションで全セルを validate_name に通し、違反があれば1件もフォルダを作らずに sys.exit(1) で終わります。バリデーション通過後は current_path 辞書で「現在開いている各階層のフォルダ名」を保持しながら、行ごとに最浅の入力列を探し、それより深い階層を del で落としてから新しいパスを os.path.join(folder, *parts) で組み立てます。最後に os.makedirs で一括作成し、既存パスは os.path.exists で除外してスキップ件数としてカウント。
仕組みの詳細
2モード構成(ひな形出力 / フォルダ作成)
1つのスクリプトに「ひな形Excelを出力」と「Excelからフォルダを作る」の2モードを乗せています。select フィールドの分岐で if mode == "ひな形を出力": / elif mode == "フォルダを作成": と振り分け。先にひな形を渡して非エンジニアに記入してもらい、戻ってきたExcelで作成する2ステップ運用が想定です。
Excel列=階層の対応関係
A列が第1階層(プロジェクト名)、B列が第2階層、C列以降も同様で、入力値のある一番浅い列がそのフォルダの位置を決めます。current_path 辞書に「現在開いている各階層のフォルダ名」を保持し、行を進むたびに浅い階層が変わったら下位階層を del で落として新しいパスを構築します。
ひな形Excelの作り込み
openpyxl で「データ」シート(4列ヘッダー)と「凡例」シート(ルール・サンプル付き)の2枚を持つExcelを Workbook() から組み立てます。ヘッダーの色付け、merge_cells でのセル結合、サンプル行の濃淡塗り分けまで含めて、受け取った人が迷わず記入できる状態で配布できます。
事前バリデーションで実行前に止める
フォルダ名に \\/:*?"<>| を含めないというWindowsの制約があるため、re.compile で作った INVALID_PATTERN で全セルを事前チェックし、違反があればフォルダを1つも作らずに終わります。途中まで作って失敗する「中途半端な状態」を避ける設計です。
応用・カスタマイズ
デフォルト4階層のヘッダー数を変える
DEFAULT_LEVELS = 4 を変更すれば、ひな形のヘッダー数を増減できます(例: DEFAULT_LEVELS = 6)。読み込み側は ws.max_column で列数を自動検出するので、ひな形側だけ変えても整合性は崩れません。
ひな形のサンプル内容を差し替える
sample_rows リストを書き換えると凡例シートのサンプル行を自社の標準フォルダ構成に合わせられます。「議事録」「資料」「報告書」を「設計」「実装」「テスト」に変えるだけで開発プロジェクト用のひな形になります。
作成と同時に各フォルダへ空のREADMEを置く
os.makedirs(full_path) の直後に open(os.path.join(full_path, "README.md"), "w").close() を追加すれば、フォルダごとに空のREADMEを生成できます。GitHubに空フォルダをpushしたい場合の .gitkeep 配置にも応用できます。
禁止文字を緩める/追加する
INVALID_CHARS と INVALID_PATTERN の正規表現を編集します。Mac/Linuxで運用するなら : をパターンから外して許可、社内ルールで全角スペースも禁止にしたい場合は r'[\\\\/:*?"<>| ]' のように追加と、一箇所で完結します。
よくあるエラーと対処
FileNotFoundError: Excelファイルが見つからない
「フォルダを作成」モードで Excelファイル 欄が空のまま、またはパスが間違っている場合に出ます。Pybesのファイル選択ダイアログから xlsx を選び直してください。ダイアログには .xlsx フィルタがかかっているので、それ以外の拡張子は選べないようになっています。
PermissionError: WinError 5 アクセスが拒否されました
書き込み先フォルダが管理者権限フォルダ(C:\\Program Files 配下など)の場合、通常権限では作成できません。Documents や Desktop のように自分のユーザー領域配下を選んでください。OneDrive同期中のフォルダもごく稀にロックがかかることがあります。
KeyError: データ シートが見つからない
ひな形Excelの「データ」シート名を自分で書き換えてしまうと wb["データ"] で KeyError になります。シート名は変えず、データを行追加していく運用にしてください。シート名を変更したい場合は ws = wb["データ"] の文字列も合わせて修正します。
「以下の問題が見つかりました」で止まる
\\/:*?"<>| のいずれかがフォルダ名に含まれているとここで止まります。Windowsでは作れない文字なので、/ を使った階層表現(2024/Q1 のような)を1セルに書いていないかチェックしてください。階層を分けたい場合は別の列に分割します。
FAQ
なぜ列を階層として扱うのですか?
Excelでネスト構造を表現する一番自然な方法だからです。同じ事を縦持ちで書くと「親フォルダ列/子フォルダ列/孫フォルダ列…」と冗長になり、行ごとに同じ親を何度も書く必要があります。列=階層なら親の入力は1セルで済み、目視でも構造が把握しやすくなります。
途中の階層を飛ばしたら(A列空、B列空、C列だけ書いたら)どうなる?
入力された一番浅い列をその行の階層と判断するので、C列だけ書くと「現在開いている第2階層の中の第3階層」として扱われます。直前にどの階層が開いているかは current_path 辞書で保持されているので、A列・B列を毎回繰り返し書く必要はありません。
既存フォルダの中身は触りますか?
触りません。os.path.exists で存在チェックし、既にあるパスは「スキップ(既存)」表示するだけです。中のファイルや既存サブフォルダはそのまま保持されるので、運用中のプロジェクトに対してこのスクリプトを再実行しても安全です。
1万行のExcelでも動きますか?
動きますが、os.makedirs 自体が呼ばれる回数だけ時間がかかります。事前バリデーションは全セル走査するもののメモリ上の処理なので一瞬。重いのはディスクI/Oの方で、SSDなら数千件/分程度のオーダーです。あまりに巨大な階層なら、まず一部の行だけで試してから本番Excelに進むのを推奨します。
.pybes ファイルをPybesにインポートすると、スクリプトと設定フィールドが自動で読み込まれます。