CloudFormation の API は「パラメータ」と「リソース」で取得元が別なので、2 系統で集める形になります。
ポイント
パラメータ: describe-stacks はスタック名を省略すると全スタックを一括で返すので、92 個でも数回の API 呼び出しで済みます(Parameters が含まれる)
リソース: list-stack-resources はスタックごとの呼び出しが必要(92 回)。describe-stack-resources は 100 件超でページングできないので list-stack-resources 推奨
スロットリング: CFn の Describe 系は 1 req/s 程度と厳しめなので、リトライ設定は必須
方法1: AWS CLI + jq(手軽に)
STATUSES="CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE IMPORT_COMPLETE"
# パラメータ一覧
aws cloudformation describe-stacks \
--query 'Stacks[?contains(`['"$(echo $STATUSES | sed 's/ /`,`/g')"']`, StackStatus)]' \
| jq -r '.[] | .StackName as $s | (.Parameters // [])[]
| [$s, .ParameterKey, .ParameterValue] | @csv' > parameters.csv
# リソース一覧
aws cloudformation list-stacks --stack-status-filter $STATUSES \
--query 'StackSummaries[].StackName' --output text | tr '\t' '\n' | sort -u |
while read -r s; do
sleep 0.5
aws cloudformation list-stack-resources --stack-name "$s" \
| jq -r --arg s "$s" '.StackResourceSummaries[]
| [$s, .LogicalResourceId, .PhysicalResourceId, .ResourceType] | @csv'
done > resources.csv
方法2: boto3(Python)
import csv, boto3
from botocore.config import Config
COMPLETE = {"CREATE_COMPLETE", "UPDATE_COMPLETE",
"UPDATE_ROLLBACK_COMPLETE", "IMPORT_COMPLETE",
"IMPORT_ROLLBACK_COMPLETE", "ROLLBACK_COMPLETE"}
cfn = boto3.client("cloudformation",
config=Config(retries={"max_attempts": 10, "mode": "adaptive"}))
stacks = [s for page in cfn.get_paginator("describe_stacks").paginate()
for s in page["Stacks"] if s["StackStatus"] in COMPLETE]
with open("parameters.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["StackName", "ParameterKey", "ParameterValue", "ResolvedValue"])
for s in stacks:
for p in s.get("Parameters", []):
w.writerow([s["StackName"], p["ParameterKey"],
p.get("ParameterValue", ""), p.get("ResolvedValue", "")])
with open("resources.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["StackName", "LogicalResourceId", "PhysicalResourceId",
"ResourceType", "ResourceStatus"])
for s in stacks:
for page in cfn.get_paginator("list_stack_resources").paginate(
StackName=s["StackId"]):
for r in page["StackResourceSummaries"]:
w.writerow([s["StackName"], r["LogicalResourceId"],
r.get("PhysicalResourceId", ""),
r["ResourceType"], r["ResourceStatus"]])
print(f"{len(stacks)} stacks exported")
注意点
NoEcho: true のパラメータは値が **** でマスクされて返ります。実値は取得できません
SSM パラメータ型(AWS::SSM::Parameter::Value<...>)は ParameterValue が SSM のキー名、ResolvedValue が実際の値になります
ネストスタックも describe-stacks に含まれます。除外したい場合は ParentId があるものを弾いてください
リージョンをまたいでいないかもチェックを。マルチリージョンなら --region でループさせる必要があります
権限は cloudformation:DescribeStacks / ListStacks / ListStackResources が必要です
方法3 bat(コマンドプロンプト + AWS CLI)
@echo off
rem ============================================================
rem CloudFormation スタックのリソース/パラメータ一覧を CSV 出力
rem 実行: get-cfn-info.bat
rem ============================================================
setlocal
rem --- 設定 -------------------------------------------------
rem プロファイル・リージョンを使う場合は下記に指定(不要なら空のまま)
set "AWSOPT="
rem 例: set "AWSOPT=--profile myprofile --region ap-northeast-1"
set "STATUS=CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE IMPORT_COMPLETE IMPORT_ROLLBACK_COMPLETE"
set "OUTDIR=%~dp0"
set "RES=%OUTDIR%resources.csv"
set "PRM=%OUTDIR%parameters.csv"
rem ----------------------------------------------------------
rem ヘッダー行を書き出し(既存ファイルは上書き)
> "%RES%" echo StackName,LogicalResourceId,PhysicalResourceId,ResourceType
> "%PRM%" echo StackName,ParameterKey,ParameterValue
set /a COUNT=0
for /f "usebackq delims=" %%s in (`aws cloudformation list-stacks %AWSOPT% --stack-status-filter %STATUS% --query "StackSummaries[].[StackName]" --output text`) do (
set /a COUNT+=1
echo [INFO] processing: %%s
rem --- リソース(論理ID, 物理ID, タイプ)---
for /f "usebackq delims=" %%r in (`aws cloudformation list-stack-resources %AWSOPT% --stack-name %%s --query "StackResourceSummaries[].join(',',[LogicalResourceId,not_null(PhysicalResourceId,'-'),ResourceType])" --output text`) do (
if not "%%r"=="None" >> "%RES%" echo %%s,%%r
)
rem --- パラメータ(Key, Value)---
for /f "usebackq delims=" %%p in (`aws cloudformation describe-stacks %AWSOPT% --stack-name %%s --query "Stacks[0].Parameters[].join(',',[ParameterKey,not_null(ParameterValue,'-')])" --output text`) do (
if not "%%p"=="None" >> "%PRM%" echo %%s,%%p
)
rem --- API スロットリング対策(約1秒スリープ)---
ping -n 2 127.0.0.1 > nul
)
echo.
echo [DONE] stacks: %COUNT%
echo %RES%
echo %PRM%
endlocal
pause
注意点
バッチファイル vs 直接入力
上記は .bat 用です。コマンドプロンプトに直接貼り付ける場合は、for 変数を %%s → %s のように % を1つ に減らしてください。
文字コード
出力は既定の Shift_JIS(CP932)です。Excel で開いて日本語が化ける場合は、chcp 65001 を先頭に足して UTF-8 にするか、Excel の「データ」→「テキストから」でエンコードを指定してください。
CSV としての限界
join(',', ...) で連結しているため、パラメータ値自体にカンマを含む場合(subnet-a,subnet-b のようなリスト型など)に列がずれます。その場合は区切り文字を | などに変えて、Excel 側でその文字を区切りとして読み込む方が安全です。
join('|',[ParameterKey,not_null(ParameterValue,'-')])
その他
NoEcho: true のパラメータは **** にマスクされ、実値は API から取得できません
SSM パラメータ型は ParameterValue が SSM のキー名になります(実値は ResolvedValue。必要なら join に追加してください)
list-stacks はネストスタックも含みます。除外したい場合は describe-stacks の ParentId で判定が必要です
92 スタック × 2 API + 1 秒スリープで、おおよそ 2〜3 分かかります
必要な権限は cloudformation:ListStacks / ListStackResources / DescribeStacks です
batで取得したファイルを分割するPython
# -*- coding: utf-8 -*-
"""
parameters.csv / resources.csv を スタック名ごとの CSV に分割する。
【入力フォーマット】
parameters.csv
1行目 : ヘッダー(読み飛ばし)
2行目~: StackName,ParameterKey,ParameterValue<TAB>ParameterKey,ParameterValue<TAB>...
resources.csv
1行目 : ヘッダー(読み飛ばし)
2行目~: StackName,LogicalResourceId,PhysicalResourceId,ResourceType<TAB>論理ID,物理ID,タイプ<TAB>...
【出力】
out/parameters/<スタック名>.csv ヘッダー: NO,設定項目,設定値
out/resources/<スタック名>.csv ヘッダー: NO,論理ID,物理ID,タイプ
【使い方】
python split_cfn_csv.py
python split_cfn_csv.py --params parameters.csv --resources resources.csv --outdir out
"""
import argparse
import csv
import os
import re
import sys
# Excel でそのまま開けるように BOM 付き UTF-8 で出力
OUT_ENCODING = "utf-8-sig"
# 入力ファイルの想定エンコーディング(先頭から順に試行)
IN_ENCODINGS = ("utf-8-sig", "cp932", "utf-8")
INVALID_FILENAME_CHARS = re.compile(r'[\\/:*?"<>|]')
def read_lines(path):
"""エンコーディングを自動判別して行リストを返す。"""
last_err = None
for enc in IN_ENCODINGS:
try:
with open(path, encoding=enc) as f:
return f.read().splitlines()
except UnicodeDecodeError as e:
last_err = e
raise last_err
def safe_filename(name):
"""Windows で使えない文字を除去したファイル名を返す。"""
cleaned = INVALID_FILENAME_CHARS.sub("_", name).strip().rstrip(".")
return cleaned or "_unnamed"
def split_param(field):
"""'Key,Value' を分解する。Value 側にカンマがあっても壊れないよう最大1回で分割。"""
parts = field.split(",", 1)
key = parts[0].strip()
value = parts[1].strip() if len(parts) > 1 else ""
return [key, value]
def split_resource(field):
"""'論理ID,物理ID,タイプ' を分解する。物理IDにカンマがあっても壊れないよう両端から確定させる。"""
parts = field.split(",")
if len(parts) < 3:
# 想定より列が少ない場合は空文字で埋める
parts = parts + [""] * (3 - len(parts))
return [p.strip() for p in parts]
logical = parts[0].strip()
rtype = parts[-1].strip()
physical = ",".join(parts[1:-1]).strip()
return [logical, physical, rtype]
def parse_line(line, first_field_extra, splitter):
"""
1行を (スタック名, [レコード, ...]) に分解する。
first_field_extra: 先頭フィールドに含まれる余分な列数(スタック名の分=1)
"""
fields = line.split("\t")
head = fields[0]
# 先頭フィールドだけ「スタック名,」が前に付いているので切り離す
head_parts = head.split(",", first_field_extra)
if len(head_parts) <= first_field_extra:
return None, []
stack_name = head_parts[0].strip()
rest_head = ",".join(head_parts[first_field_extra:])
records = [splitter(rest_head)]
for field in fields[1:]:
if field.strip():
records.append(splitter(field))
return stack_name, records
def process(in_path, out_dir, header, splitter, label):
if not os.path.isfile(in_path):
print("[SKIP] ファイルが見つかりません: {}".format(in_path))
return 0, 0
os.makedirs(out_dir, exist_ok=True)
lines = read_lines(in_path)
stack_count = 0
row_count = 0
for line in lines[1:]: # 1行目のヘッダーは読み飛ばし
if not line.strip():
continue
stack_name, records = parse_line(line, 1, splitter)
if not stack_name or not records:
print("[WARN] 解析できない行をスキップしました: {}".format(line[:80]))
continue
out_path = os.path.join(out_dir, safe_filename(stack_name) + ".csv")
with open(out_path, "w", encoding=OUT_ENCODING, newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
for no, rec in enumerate(records, start=1): # NO は 1 から採番
writer.writerow([no] + rec)
row_count += 1
stack_count += 1
print(" {:<60} {:>4} 件".format(stack_name, len(records)))
print("[DONE] {}: {} スタック / {} 行 -> {}".format(
label, stack_count, row_count, out_dir))
return stack_count, row_count
def main():
parser = argparse.ArgumentParser(
description="CloudFormation の parameters.csv / resources.csv をスタックごとに分割します。")
parser.add_argument("--params", default="parameters.csv", help="parameters.csv のパス")
parser.add_argument("--resources", default="resources.csv", help="resources.csv のパス")
parser.add_argument("--outdir", default="out", help="出力先フォルダ(既定: out)")
args = parser.parse_args()
print("=== parameters ===")
process(
args.params,
os.path.join(args.outdir, "parameters"),
["NO", "設定項目", "設定値"],
split_param,
"parameters",
)
print()
print("=== resources ===")
process(
args.resources,
os.path.join(args.outdir, "resources"),
["NO", "論理ID", "物理ID", "タイプ"],
split_resource,
"resources",
)
if __name__ == "__main__":
sys.exit(main())
作成したCSVをマージする
# -*- coding: utf-8 -*-
"""
out/resources と out/parameters の同名 CSV をスタック単位でマージする。
【基準】
out/resources のファイル名(=スタック名)を基準にループする。
同名の parameters ファイルが無い場合は「なし」と出力する。
【出力レイアウト】 out/merged/<スタック名>.csv
1行目 : スタック名
2行目 : (空行)
3行目 : パラメータ
4行目~: parameters ファイルの中身(無ければ「なし」)
(空行)
構築リソース
resources ファイルの中身
【使い方】
python merge_cfn_csv.py
python merge_cfn_csv.py --resources out/resources --params out/parameters --outdir out/merged
"""
import argparse
import csv
import os
import sys
OUT_ENCODING = "utf-8-sig"
IN_ENCODINGS = ("utf-8-sig", "cp932", "utf-8")
def read_csv_rows(path):
"""CSV を行のリスト(各行は文字列のリスト)として読み込む。"""
last_err = None
for enc in IN_ENCODINGS:
try:
with open(path, encoding=enc, newline="") as f:
return [row for row in csv.reader(f) if any(c.strip() for c in row)]
except UnicodeDecodeError as e:
last_err = e
raise last_err
def main():
parser = argparse.ArgumentParser(
description="resources を基準に parameters をマージして1スタック1CSVにまとめます。")
parser.add_argument("--resources", default=os.path.join("out", "resources"),
help="resources CSV のフォルダ")
parser.add_argument("--params", default=os.path.join("out", "parameters"),
help="parameters CSV のフォルダ")
parser.add_argument("--outdir", default=os.path.join("out", "merged"),
help="出力先フォルダ")
args = parser.parse_args()
if not os.path.isdir(args.resources):
print("[ERROR] フォルダが見つかりません: {}".format(args.resources))
return 1
os.makedirs(args.outdir, exist_ok=True)
res_files = sorted(
f for f in os.listdir(args.resources)
if f.lower().endswith(".csv")
and os.path.isfile(os.path.join(args.resources, f))
)
if not res_files:
print("[ERROR] CSV が見つかりません: {}".format(args.resources))
return 1
merged = 0
no_param = 0
for fname in res_files:
stack_name = os.path.splitext(fname)[0]
res_rows = read_csv_rows(os.path.join(args.resources, fname))
param_path = os.path.join(args.params, fname)
if os.path.isfile(param_path):
param_rows = read_csv_rows(param_path)
else:
param_rows = None
rows = []
rows.append([stack_name]) # 1行目: スタック名
rows.append([]) # 2行目: 空行
rows.append(["パラメータ"]) # 3行目: 見出し
if param_rows: # 4行目以降: パラメータ本体
rows.extend(param_rows)
else:
rows.append(["なし"])
no_param += 1
rows.append([]) # 空行
rows.append(["構築リソース"]) # 見出し
rows.extend(res_rows) # リソース本体
out_path = os.path.join(args.outdir, fname)
with open(out_path, "w", encoding=OUT_ENCODING, newline="") as f:
csv.writer(f).writerows(rows)
merged += 1
mark = "" if param_rows else " (パラメータなし)"
print(" {}{}".format(stack_name, mark))
print()
print("[DONE] {} ファイルを出力しました(うちパラメータなし: {})".format(merged, no_param))
print(" -> {}".format(os.path.abspath(args.outdir)))
return 0
if __name__ == "__main__":
sys.exit(main())
CSVをExcel化
# -*- coding: utf-8 -*-
"""
フォルダ内の CSV をまとめて 1 つの Excel ブックに変換する。
【動作】
- 指定フォルダ内の *.csv をすべて読み込み、CSV 1 つ = シート 1 つとして出力
- ブックの先頭に「目次」シートを作成し、CSV ファイル名の一覧と各シートへのリンクを出力
- 全セルを文字列形式(表示形式 "@")で書き込むため、ゼロ始まりの値もそのまま残る
【使い方】
# フォルダ 1 つを変換 -> group01.xlsx が出来る
python csv_to_excel.py group01
# 親フォルダ配下のサブフォルダを一括変換(サブフォルダごとに 1 ブック)
python csv_to_excel.py --batch books
# 出力ファイルを指定
python csv_to_excel.py group01 --out C:\\work\\group01.xlsx
【必要なライブラリ】
pip install openpyxl
"""
import argparse
import csv
import os
import re
import sys
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.hyperlink import Hyperlink
IN_ENCODINGS = ("utf-8-sig", "cp932", "utf-8")
TOC_SHEET_NAME = "目次"
BASE_FONT = "Arial"
MAX_SHEET_NAME = 31 # Excel のシート名上限
INVALID_SHEET_CHARS = re.compile(r"[\[\]:*?/\\]")
MAX_COL_WIDTH = 60
# ---------------------------------------------------------------- 読み込み
def read_csv_rows(path):
"""CSV を行のリストとして読み込む(エンコーディング自動判別)。"""
last_err = None
for enc in IN_ENCODINGS:
try:
with open(path, encoding=enc, newline="") as f:
return list(csv.reader(f))
except UnicodeDecodeError as e:
last_err = e
raise last_err
# ---------------------------------------------------------------- シート名
def make_sheet_name(base, used):
"""Excel の制約に合わせたユニークなシート名を作る。"""
name = INVALID_SHEET_CHARS.sub("_", base).strip().strip("'")
if not name:
name = "sheet"
name = name[:MAX_SHEET_NAME]
if name.lower() not in used:
used.add(name.lower())
return name
# 重複時は末尾に ~1, ~2 ... を付与
for i in range(1, 1000):
suffix = "~{}".format(i)
cand = name[:MAX_SHEET_NAME - len(suffix)] + suffix
if cand.lower() not in used:
used.add(cand.lower())
return cand
raise ValueError("シート名を決定できません: {}".format(base))
# ---------------------------------------------------------------- 書き込み
def write_text_cell(ws, row, col, value):
"""必ず文字列としてセルに書き込む(ゼロ始まり・"=" 始まり対策込み)。"""
cell = ws.cell(row=row, column=col)
cell.value = "" if value is None else str(value)
cell.data_type = "s" # "=" で始まる値を数式と解釈させない
cell.number_format = "@" # 表示形式を「文字列」に固定
cell.font = Font(name=BASE_FONT, size=10)
cell.alignment = Alignment(vertical="center")
return cell
def autofit(ws, rows):
"""列幅を内容におおまかに合わせる(全角は 2 文字分として計算)。"""
widths = {}
for row in rows:
for idx, value in enumerate(row, start=1):
text = "" if value is None else str(value)
width = sum(2 if ord(ch) > 0x7F else 1 for ch in text)
if width > widths.get(idx, 0):
widths[idx] = width
for idx, width in widths.items():
ws.column_dimensions[get_column_letter(idx)].width = min(width + 3, MAX_COL_WIDTH)
def add_data_sheets(wb, csv_paths):
"""CSV 群をシートとして書き込み、目次用の情報を返す。"""
used_names = {TOC_SHEET_NAME.lower()}
entries = []
for path in csv_paths:
file_name = os.path.basename(path)
base = os.path.splitext(file_name)[0]
rows = read_csv_rows(path)
sheet_name = make_sheet_name(base, used_names)
ws = wb.create_sheet(sheet_name)
for r, row in enumerate(rows, start=1):
for c, value in enumerate(row, start=1):
write_text_cell(ws, r, c, value)
autofit(ws, rows)
entries.append((file_name, sheet_name, len(rows)))
return entries
def write_toc(toc, title, entries):
"""目次シートを組み立てる。"""
header_fill = PatternFill("solid", fgColor="DDEBF7")
toc["A1"].value = title
toc["A1"].font = Font(name=BASE_FONT, size=12, bold=True)
headers = ["NO", "CSVファイル名", "シート名", "行数"]
for c, text in enumerate(headers, start=1):
cell = toc.cell(row=3, column=c)
cell.value = text
cell.font = Font(name=BASE_FONT, size=10, bold=True)
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center", vertical="center")
for i, (file_name, sheet_name, row_count) in enumerate(entries, start=1):
r = 3 + i
write_text_cell(toc, r, 1, i)
link_cell = write_text_cell(toc, r, 2, file_name)
write_text_cell(toc, r, 3, sheet_name)
write_text_cell(toc, r, 4, row_count)
# 数式ではなく location 属性のブック内リンクでシートへジャンプさせる
link_cell.hyperlink = Hyperlink(
ref=link_cell.coordinate,
location="'{}'!A1".format(sheet_name.replace("'", "''")),
tooltip=file_name,
)
link_cell.font = Font(name=BASE_FONT, size=10, color="0563C1", underline="single")
toc_rows = [headers] + [[i, e[0], e[1], e[2]] for i, e in enumerate(entries, start=1)]
autofit(toc, toc_rows)
toc.freeze_panes = "A4"
toc.sheet_view.showGridLines = False
def convert_folder(folder, out_path=None):
"""1 フォルダ分を 1 ブックに変換する。"""
if not os.path.isdir(folder):
print("[SKIP] フォルダがありません: {}".format(folder))
return None
csv_paths = sorted(
os.path.join(folder, f)
for f in os.listdir(folder)
if f.lower().endswith(".csv") and os.path.isfile(os.path.join(folder, f))
)
if not csv_paths:
print("[SKIP] CSV がありません: {}".format(folder))
return None
folder_name = os.path.basename(os.path.abspath(folder))
if out_path is None:
out_path = os.path.join(os.path.dirname(os.path.abspath(folder)),
folder_name + ".xlsx")
wb = Workbook()
wb.remove(wb.active) # 既定の "Sheet" を除去
toc = wb.create_sheet(TOC_SHEET_NAME) # 先に作るのでこれが先頭シートになる
entries = add_data_sheets(wb, csv_paths)
write_toc(toc, "{} 目次(全 {} ファイル)".format(folder_name, len(entries)), entries)
wb.active = 0
wb.save(out_path)
print("[OK] {:<24} {:>3} シート -> {}".format(folder_name, len(entries), out_path))
return out_path
def main():
parser = argparse.ArgumentParser(
description="フォルダ内の CSV を目次付きの Excel ブックに変換します。")
parser.add_argument("path", nargs="?", default=".",
help="変換するフォルダ(--batch 指定時は親フォルダ)")
parser.add_argument("--batch", action="store_true",
help="配下のサブフォルダをそれぞれ 1 ブックに変換する")
parser.add_argument("--out", default=None,
help="出力ファイルパス(--batch 指定時は無視)")
args = parser.parse_args()
if args.batch:
subdirs = sorted(
os.path.join(args.path, d) for d in os.listdir(args.path)
if os.path.isdir(os.path.join(args.path, d))
)
if not subdirs:
print("[ERROR] サブフォルダが見つかりません: {}".format(args.path))
return 1
made = sum(1 for d in subdirs if convert_folder(d))
print()
print("[DONE] {} 個のブックを作成しました。".format(made))
else:
if not convert_folder(args.path, args.out):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
VBAの色付け:HighlightKeywordRows
Attribute VB_Name = "HighlightKeywordRows"
Option Explicit
'==========================================================
' 全シートのA列を検索し、指定キーワードと完全一致する行の
' A~D列の背景色を黄緑色(#E2EFDA)に設定する
'==========================================================
Sub HighlightKeywordRows()
' 背景色 #E2EFDA を指定(VBAのInterior.ColorはBGR順のため &HDAEFE2 となる)
Const TARGET_COLOR As Long = &HDAEFE2
Dim ws As Worksheet
Dim keywords As Variant
Dim lastRow As Long
Dim i As Long
Dim k As Long
Dim cellText As String
Dim hitCount As Long
Dim totalHit As Long
' 検索する単語(完全一致)
keywords = Array("スタック名", "パラメータ", "構築リソース")
Application.ScreenUpdating = False
' --- 1シートずつ処理 ---
For Each ws In ThisWorkbook.Worksheets
hitCount = 0
' A列の最終行を取得
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow >= 1 Then
For i = 1 To lastRow
' 前後の空白(半角・全角)を除去して比較
cellText = Trim$(Replace(CStr(ws.Cells(i, "A").Value), ChrW(&H3000), " "))
For k = LBound(keywords) To UBound(keywords)
If cellText = keywords(k) Then
' 完全一致 → A~D列を塗りつぶし
With ws.Range(ws.Cells(i, "A"), ws.Cells(i, "D")).Interior
.Pattern = xlSolid
.Color = TARGET_COLOR
End With
hitCount = hitCount + 1
Exit For ' 同じ行で他のキーワードは判定不要
End If
Next k
Next i
End If
' 一致が0件のシートは何もせず次のシートへ
totalHit = totalHit + hitCount
Next ws
Application.ScreenUpdating = True
MsgBox "処理が完了しました。" & vbCrLf & _
"塗りつぶした行数: " & totalHit & " 行", vbInformation
End Sub
表の処理:FormatStackSheets
Attribute VB_Name = "FormatStackSheets"
Option Explicit
'==========================================================
' 1シート目(目次)を除く全シートについて
' ・「パラメータ」「構築リソース」テーブルのヘッダー行を黄緑色(#E2EFDA)にする
' ・上記2つのテーブルにのみ罫線を引く
' テーブルの行数・列数は自動判定(不定行数に対応)
'==========================================================
' 背景色 #E2EFDA(VBAのInterior.ColorはBGR順のため &HDAEFE2)
Private Const HEADER_COLOR As Long = &HDAEFE2
' セクションの見出しとして扱う単語(テーブルの終端判定にも使用)
Private Const SECTION_WORDS As String = "スタック名|パラメータ|構築リソース"
Sub FormatStackSheets()
Dim ws As Worksheet
Dim i As Long
Dim rowParam As Long
Dim rowResource As Long
Dim tableCount As Long
Dim skipSheets As String
If ThisWorkbook.Worksheets.Count < 2 Then
MsgBox "対象シート(2シート目以降)がありません。", vbExclamation
Exit Sub
End If
Application.ScreenUpdating = False
' --- 2シート目以降を1シートずつ処理(1シート目=目次はスキップ) ---
For i = 2 To ThisWorkbook.Worksheets.Count
Set ws = ThisWorkbook.Worksheets(i)
rowParam = FindRowInColumnA(ws, "パラメータ")
rowResource = FindRowInColumnA(ws, "構築リソース")
' 見出しの1行下がヘッダー行
If rowParam > 0 Then
If FormatTable(ws, rowParam + 1) Then tableCount = tableCount + 1
End If
If rowResource > 0 Then
If FormatTable(ws, rowResource + 1) Then tableCount = tableCount + 1
End If
If rowParam = 0 And rowResource = 0 Then
skipSheets = skipSheets & vbCrLf & " ・" & ws.Name
End If
Next i
Application.ScreenUpdating = True
MsgBox "処理が完了しました。" & vbCrLf & _
"整形したテーブル数: " & tableCount & _
IIf(Len(skipSheets) > 0, vbCrLf & vbCrLf & "対象テーブルが見つからなかったシート:" & skipSheets, ""), _
vbInformation
End Sub
'----------------------------------------------------------
' A列から指定文字列(完全一致)の行番号を取得。見つからない場合は 0
'----------------------------------------------------------
Private Function FindRowInColumnA(ByVal ws As Worksheet, ByVal keyword As String) As Long
Dim lastRow As Long
Dim r As Long
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
For r = 1 To lastRow
If NormalizeText(ws.Cells(r, "A").Value) = keyword Then
FindRowInColumnA = r
Exit Function
End If
Next r
FindRowInColumnA = 0
End Function
'----------------------------------------------------------
' headerRow を先頭行とするテーブルを整形する
' 戻り値: 整形した場合 True
'----------------------------------------------------------
Private Function FormatTable(ByVal ws As Worksheet, ByVal headerRow As Long) As Boolean
Dim lastCol As Long
Dim endRow As Long
Dim r As Long
Dim txt As String
Dim rng As Range
' ヘッダー行が空なら処理しない
If NormalizeText(ws.Cells(headerRow, "A").Value) = "" Then Exit Function
' 列数はヘッダー行から自動判定(パラメータ=4列/構築リソース=5列)
lastCol = ws.Cells(headerRow, ws.Columns.Count).End(xlToLeft).Column
If lastCol < 1 Then Exit Function
' 最終行を判定(A列が空、または次のセクション見出しが現れる直前まで)
endRow = headerRow
r = headerRow + 1
Do While r <= ws.Rows.Count
txt = NormalizeText(ws.Cells(r, "A").Value)
If txt = "" Then Exit Do
If InStr(1, "|" & SECTION_WORDS & "|", "|" & txt & "|", vbTextCompare) > 0 Then Exit Do
endRow = r
r = r + 1
Loop
Set rng = ws.Range(ws.Cells(headerRow, 1), ws.Cells(endRow, lastCol))
' --- 罫線(テーブル範囲のみ) ---
With rng.Borders
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
' --- ヘッダー行の背景色 ---
With ws.Range(ws.Cells(headerRow, 1), ws.Cells(headerRow, lastCol)).Interior
.Pattern = xlSolid
.Color = HEADER_COLOR
End With
FormatTable = True
End Function
'----------------------------------------------------------
' 前後の空白(半角・全角)を除去した文字列を返す
'----------------------------------------------------------
Private Function NormalizeText(ByVal v As Variant) As String
NormalizeText = Trim$(Replace(CStr(v), ChrW(&H3000), " "))
End Function