1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWSの完了ステータスのスタックのリソースとパラメータを取得

1
Last updated at Posted at 2026-08-20

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
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?