Claude Media
Claude CodeでGoogle広告のアカウント構造を監査する — 重複と命名ゆれを洗う

Claude CodeでGoogle広告のアカウント構造を監査する — 重複と命名ゆれを洗う

Google広告エディタから書き出したCSVを、Claude Codeに書かせた小さなスクリプトで調べ、重複キーワード・広告グループの粒度・命名規則の乱れを一覧にする手順です。

Google広告のアカウントは、運用が長くなるほど構造が崩れます。同じキーワードが複数の広告グループに入り、キャンペーン名は担当者ごとに書式が違い、1つの広告グループに数十個のキーワードが詰め込まれます。管理画面で1行ずつ見て回るのは現実的ではありません。

広告グループとキーワードの一覧はCSVで書き出せます。このCSVをClaude Codeに読ませて、監査用のスクリプトを書かせて走らせると、重複・粒度・命名の問題が数秒で表になります。ここでは書き出しから、スクリプトの中身、結果の読み方、片付けの反映までを手順にします。

構造の監査は、数える作業と判断する作業に分ける

CSVをそのままチャットに貼って「構造の問題を探して」と頼むと、行数が増えるほど見落としが出ます。同じ語が何回出るか、名前が書式に合うかは数えれば決まる話なので、数える部分はスクリプトに任せます。

作業担当
重複・粒度・命名違反の抽出担当スクリプト(毎回同じ結果)
抽出結果の意味づけ、統合案や名前の改善案担当Claude Code
一時停止や削除を実際に行うかの判断担当人

Claude Codeの強みは、スクリプトを書いて実行し、出力を読んで次の手を決める往復にあります。CSVは全文を読ませる対象ではなく、プログラムの入力として扱うのが安定します。

検索語句を読んで除外キーワードを決める作業は、ClaudeでGoogle広告の検索語句を読み、除外キーワードと広告文を見直すが扱っています。この記事は、その手前にあるアカウントの骨格を見ます。

手順1: アカウントをCSVにする

Google広告エディタには、アカウントのデータをCSVとして書き出し、編集して取り込み直す流れがあります。ヘルプにも、少量の変更なら「エディタからCSVをエクスポートして編集し、再度インポートする方法も有効」と書かれています。

書き出したCSVは、エンティティごとに1行になります。キーワードの行にはキャンペーン名・広告グループ名・キーワードが入り、広告の行ではキーワード欄が空になります。ヘルプの例でも、キーワード2行と広告2行が同じ列構成の中に並んでいます。

エクスポート設定で元の列を含める設定をオンにすると、一部の列が複製され、片方の見出しの末尾に #Original が付きます。値の編集は見出しがそのままの列で行い、#Original の列は元の値を残す役目です。監査では読み取りだけなので、#Original の列は無視します。

ファイルには、広告主の事業内容が分かるキャンペーン名やキーワードが入ります。社外に出せない案件のアカウントなら、手元の環境で完結させます。入力してよい情報の線引きは、Claudeに個人情報を入力する前に見るべき一次資料が参考になります。

手順2: 調べる観点を決める

観点は、次の6つに絞ります。

観点

アカウント構造の監査で見る6つの観点

  • 同じ広告グループ内の重複

    キャンペーン・広告グループ・キーワード・マッチタイプがすべて同じ行です。

  • 広告グループをまたぐ同一キーワード

    別の広告グループに同じキーワードがある状態です。語順だけ違うものも含めます。

  • マッチタイプ違いの併存

    同じ語が完全一致とインテントマッチで両方入っている状態です。

  • 粒度の偏り

    キーワードが多すぎる広告グループと、1個だけの広告グループです。

  • 命名ゆれ

    大文字小文字や区切り文字だけ違う名前と、決めた書式に合わない名前です。

  • 動いていないキーワード

    有効に見えても、親のキャンペーンや広告グループが一時停止中のものです。

なぜ語順違いまで重複に数えるのか

キーワードのマッチタイプのヘルプには、「赤 車」と「車 赤」のようによく似たインテントマッチのキーワードを両方追加しても、両方の検索に一致するキーワードは1つだけだと書かれています。重複として認識され、広告ランクの高いほうが使われます。入札は広告主1者につき1件のみです。

つまり、語順違いの重複は費用が二重にかかる問題ではありません。管理する行が増えるだけで、どちらが働いているかが分かりにくくなる問題です。監査で拾う目的は、削除ではなく見通しを良くすることにあります。

マッチタイプ違いの併存は、一律に消さない

同じヘルプには、広範囲のマッチタイプで一致する検索は、狭いマッチタイプで同じキーワードを指定した場合に一致する検索をすべて含むとあります。そのためインテントマッチ1つで、同じキーワードを繰り返し指定しなくても複数のマッチタイプ分のメリットが得られる、という説明です。

とはいえ、完全一致を残して入札を分けている運用もあります。併存を見つけたら、意図があって分けたものか、昔の名残かを持ち主に聞いてから動かします。スクリプトが出すのは「併存している」という事実までです。

手順3: Claude Codeにスクリプトを書かせる

プロジェクトのフォルダに書き出したCSVを置き、Claude Codeを起動して、次のように頼みます。

exports/ に Google 広告エディタから書き出した CSV がある。
scripts/ads_audit.py を書いて、次の6つを出力して。
1. 同じ広告グループ内でキャンペーン・広告グループ・キーワード・マッチタイプが同じ行
2. 複数の広告グループに入っている同一キーワード(語順違いも同一とみなす)
3. 同じ広告グループにマッチタイプ違いで入っている同一キーワード
4. キーワードが20個を超える広告グループと、1個だけの広告グループ
5. キャンペーン名・広告グループ名の大文字小文字や区切り文字だけの違い、
   および次の書式に合わない名前
   キャンペーン: 3〜5字の大文字_目的_地域(例 SRCH_Brand_JP)
   広告グループ: テーマ_サブテーマ(例 ring_engage)
6. 親のキャンペーン/広告グループが Paused のキーワード
CSV は読み取りだけ。Type が Negative / Campaign negative の行と
Removed の行は対象外。列見出しは大文字小文字・スペースの有無を無視して読む。

「読み取りだけ」と添えるのは、書き出したファイルを誤って書き換えられないようにするためです。

列見出しの扱いは、エディタ側の仕様に合わせます。ヘルプによると、エディタはCSVの列を自動認識させるために英語の見出しを前提にし、大文字小文字やスペースの有無は区別しません。daily budget、dailybudget、daily_budget はどれも同じ意味になります。スクリプトも同じ規則で見出しを読めば、手元で整形し直す手間が減ります。

キーワードの行の判別には Type 系の列を使います。ヘルプの列一覧では、この列にマッチタイプ(Broad など)を入れ、値が Negative なら除外キーワード、Campaign negative ならキャンペーン単位の除外として扱われます。ステータスは Enabled / Paused / Removed のいずれかです。

scripts/ads_audit.py の全文
import csv, io, re, sys, unicodedata
from collections import defaultdict
 
MAX_KW_PER_GROUP = 20   # 粒度の目安。自社の運用に合わせて変える
CAMPAIGN_RULE = re.compile(r"^[A-Z]{3,5}_[A-Za-z0-9]+_[A-Za-z0-9]+$")  # 例: SRCH_Brand_JP
GROUP_RULE = re.compile(r"^[A-Za-z0-9]+_[A-Za-z0-9]+$")                # 例: ring_engage
 
ALIASES = {
    "campaign": ["campaign", "campaignname"],
    "group": ["adgroup", "adgroupname"],
    "keyword": ["keyword", "keywordtext"],
    "type": ["type", "criteriontype", "keywordtype", "matchtype"],
    "status": ["status"],
    "cstatus": ["campaignstatus"],
    "gstatus": ["adgroupstatus"],
}
 
def read_rows(path):
    raw = open(path, "rb").read()
    for enc in ("utf-8-sig", "utf-16"):
        try:
            text = raw.decode(enc)
            break
        except UnicodeError:
            continue
    first = text.splitlines()[0]
    delim = "\t" if first.count("\t") >= first.count(",") else ","
    rows = list(csv.reader(io.StringIO(text), delimiter=delim))
    heads = [re.sub(r"[\s_]", "", h.lower()) for h in rows[0]]
    cols = {}
    for key, names in ALIASES.items():
        for n in names:
            if n in heads:
                cols[key] = heads.index(n)
                break
    return cols, rows[1:]
 
def norm(s):
    return unicodedata.normalize("NFKC", s).lower().strip()
 
def split_match(kw, typ):
    kw = kw.strip()
    if kw.startswith("[") and kw.endswith("]"):
        return kw[1:-1], "exact"
    if kw.startswith('"') and kw.endswith('"'):
        return kw[1:-1], "phrase"
    return kw, (typ.strip().lower() or "broad")
 
cols, rows = read_rows(sys.argv[1])
missing = [k for k in ("campaign", "group", "keyword") if k not in cols]
if missing:
    sys.exit(f"列が見つかりません: {missing} / 見つかった列: {cols}")
 
def cell(r, k):
    i = cols.get(k)
    return r[i].strip() if i is not None and i < len(r) else ""
 
kws, camps, groups = [], defaultdict(set), defaultdict(set)
for r in rows:
    c, g = cell(r, "campaign"), cell(r, "group")
    if c: camps[norm(re.sub(r"[\s_\-]", "", c))].add(c)
    if g: groups[(c, norm(re.sub(r"[\s_\-]", "", g)))].add(g)
    k = cell(r, "keyword")
    if not k or not c or not g:
        continue
    text, mt = split_match(k, cell(r, "type"))
    if "negative" in mt:
        continue   # 除外キーワードは別の監査で扱う
    states = [cell(r, "status"), cell(r, "cstatus"), cell(r, "gstatus")]
    if any(s.lower() == "removed" for s in states):
        continue
    kws.append(dict(c=c, g=g, key=" ".join(sorted(norm(text).split())), mt=mt,
                    dead=any(s.lower() == "paused" for s in states)))
 
print("## 1. 同じ広告グループ内の重複")
d = defaultdict(int)
for k in kws: d[(k["c"], k["g"], k["key"], k["mt"])] += 1
for (c, g, key, mt), n in d.items():
    if n > 1: print(f"- {c} / {g} / {key} ({mt}) x{n}")
 
print("## 2. 広告グループをまたぐ同一キーワード(語順違いを含む)")
x = defaultdict(set)
for k in kws:
    if not k["dead"]: x[(k["key"], k["mt"])].add((k["c"], k["g"]))
for (key, mt), where in x.items():
    if len(where) > 1:
        print(f"- {key} ({mt}): " + " | ".join(f"{c}/{g}" for c, g in sorted(where)))
 
print("## 3. 同じ広告グループにマッチタイプ違いで併存")
m = defaultdict(set)
for k in kws: m[(k["c"], k["g"], k["key"])].add(k["mt"])
for (c, g, key), mts in m.items():
    if len(mts) > 1: print(f"- {c} / {g} / {key}: {sorted(mts)}")
 
print(f"## 4. 粒度(キーワード数が {MAX_KW_PER_GROUP} 超、または1個)")
n = defaultdict(int)
for k in kws: n[(k["c"], k["g"])] += 1
for (c, g), cnt in sorted(n.items()):
    if cnt > MAX_KW_PER_GROUP or cnt == 1: print(f"- {c} / {g}: {cnt}")
 
print("## 5. 命名")
for names in camps.values():
    if len(names) > 1: print(f"- キャンペーン名のゆれ: {sorted(names)}")
for (c, _), names in groups.items():
    if len(names) > 1: print(f"- 広告グループ名のゆれ({c}): {sorted(names)}")
for c in sorted({k["c"] for k in kws}):
    if not CAMPAIGN_RULE.match(c): print(f"- キャンペーン規則違反: {c}")
for c, g in sorted({(k["c"], k["g"]) for k in kws}):
    if not GROUP_RULE.match(g): print(f"- 広告グループ規則違反: {c} / {g}")
 
print("## 6. 親が一時停止で動いていないキーワード")
dead = defaultdict(int)
for k in kws:
    if k["dead"]: dead[(k["c"], k["g"])] += 1
for (c, g), cnt in dead.items(): print(f"- {c} / {g}: {cnt}")

文字コードの扱いにも注意が要ります。ヘルプは、Excelで編集したファイルをインポートするときはUnicodeテキスト形式で保存するよう案内していて、Macでは「UTF-16 Unicodeテキスト」を選ぶと拡張子が .csv から .txt に変わります。エディタからの書き出しファイルの文字コードや区切り文字は手元で確かめる必要があるため、スクリプトはUTF-8とUTF-16、カンマとタブの両方を試す作りにしています。

手順4: 実行して結果を読む

書き出したCSVを指定して実行します。

python3 scripts/ads_audit.py exports/account.csv

以下は、架空のサンプルCSV(13行)で上のスクリプトを走らせた実際の出力です。サンプルには、わざと重複・語順違い・名前のゆれ・一時停止中の親を仕込んであります。

## 1. 同じ広告グループ内の重複
- SRCH_Brand_JP / brand_main / エンゲージ リング (broad) x2
## 2. 広告グループをまたぐ同一キーワード(語順違いを含む)
- エンゲージ リング (broad): SRCH_Brand_JP/brand_main | SRCH_Gen_JP/ring_engage
- 結婚指輪 (broad): SRCH_Brand_JP/brand_main | SRCH_Gen_JP/ring_wedding
- 婚約指輪 安い (phrase): SRCH_Gen_JP/ring_engage | srch_gen_jp/指輪 汎用
## 3. 同じ広告グループにマッチタイプ違いで併存
- SRCH_Brand_JP / brand_main / エンゲージ リング: ['broad', 'exact']
## 4. 粒度(キーワード数が 20 超、または1個)
- srch_gen_jp / 指輪 汎用: 1
## 5. 命名
- キャンペーン名のゆれ: ['SRCH_Gen_JP', 'srch_gen_jp']
- キャンペーン規則違反: Search 指輪 2022
- キャンペーン規則違反: srch_gen_jp
- 広告グループ規則違反: srch_gen_jp / 指輪 汎用
## 6. 親が一時停止で動いていないキーワード
- Search 指輪 2022 / old_group: 2

出力から読み取れることは次のとおりです。

  • 1つ目の エンゲージ リング は、完全に同じ行が2本あります。片方を消しても配信は変わらないので、最も安全に片付けられる候補です。
  • 2つ目の エンゲージ リング と リング エンゲージ は、別々の広告グループにあります。語順が違うだけなので、同じキーワードとして拾われています。ブランド用の広告グループと一般用の広告グループに分けた意図があるのか、確認の対象です。
  • 婚約指輪 安い は、大文字小文字違いのキャンペーン名(SRCH_Gen_JP と srch_gen_jp)にまたがっています。命名ゆれが、重複の発見を遅らせていた例です。
  • Search 指輪 2022 は、キーワードは有効のままでも親のキャンペーンが一時停止なので、実際には配信されません。

ステータスのヘルプにも、同じ関係が書かれています。キャンペーンや広告グループを一時停止すると、その中のキーワードは「無効」の扱いになり、広告の表示に使えなくなります。有効のまま残るキーワードは、数字の上だけ構造を太らせます。

閾値と書式は、アカウントごとに決める

上のスクリプトにある20個という粒度の目安や、SRCH_Brand_JP 型の書式は、サンプルのために置いた値です。ヘルプに数値の基準はありません。書かれているのは、類似するキーワードを広告グループにまとめ、テーマ別にグループ分けするという方針と、「エンゲージリング」「結婚指輪」のようにテーマごとに広告グループを分け、専用の広告を作る例です。

したがって粒度の検査は、数の上限よりもテーマの混在を見るのが本筋です。スクリプトで多すぎる広告グループを見つけたら、Claude Codeに中身のキーワードを渡し、テーマごとに分けるとしたらどう分かれるかを案として出させます。分け方を決めて実行するのは人です。

命名規則は、CLAUDE.mdに書いておくと再利用できる

命名の検査は、書式が決まって初めて機械的にできます。書式をスクリプトの正規表現にだけ持たせると、新しいキャンペーンを作るたびに別の人が別の書式を使います。プロジェクトの CLAUDE.md に命名規則を書いておけば、Claude Codeに名前の案を出させるときも、同じ規則に沿った案になります。

## Google広告の命名規則
- キャンペーン: {媒体3-5字}_{目的}_{地域}  例 SRCH_Brand_JP
- 広告グループ: {テーマ}_{サブテーマ}   例 ring_engage
- 英数字とアンダースコアのみ。大文字小文字は上の例のとおり
- 年号や日付は名前に入れない(期間は設定側で管理する)
- 監査は scripts/ads_audit.py。書式を変えたら正規表現も同時に直す

最後の行が要点です。規則とスクリプトの正規表現を別々に直すと、片方だけが古くなります。

手順5: 片付けをエディタに戻す

監査の結果から実際にキーワードを整理するときは、Google広告エディタのインポートに戻します。

  1. 一時停止や削除の対象を、Claude Codeに変更用のCSVとして作らせる
  2. エディタで[アカウント] > [インポート]を選び、ファイルから取り込む
  3. 列見出しを確認し、[インポート]を選ぶ
  4. 取り込んだ変更を[提案された変更]として確認し、承認するか拒否する

ヘルプによると、CSVからインポートした変更は提案された変更としてアカウントに表示されます。すべて承認することも、すべて拒否することも、個別に確認して承認や拒否をすることもできます。インポート自体も、必要に応じて後から取り消せます。いきなりアカウントが書き換わるわけではないので、監査の結果をそのまま流し込む前の安全弁になります。

変更用のCSVを作るときは、次の2点を守ります。

  • 削除(Removed)の前に、まず一時停止(Paused)にする。ステータス列は Enabled / Paused / Removed のいずれかで指定します
  • 触らない列は空白にする。ヘルプによると、フィールドを空白のままにするとその項目は維持され、角かっこ [] を入れると値が消去されます

後者は取り違えると危険です。[] を入れるのは値を消したいときだけで、変更しない列に入れてはいけません。依頼文に「触らない列は空白、[] は使わない」と明記しておきます。

また、エディタのインポートでプレビューに出るのは100行目までですが、それより多い行もすべてインポートされます。大きな変更用CSVでは、プレビューに見えない行も変わる点に注意してください。

定期的に回すなら、パイプで渡す

監査を毎月回すなら、Claude Codeの非対話実行が使えます。概要ページには、ログをパイプで渡したり、スクリプトの出力を渡して指示を付けたりする例が載っています。同じ形で、監査スクリプトの出力を渡します。

python3 scripts/ads_audit.py exports/account.csv \
  | claude -p "この監査結果を、優先度の高い順に整理して。重複は消す前に一時停止する案で。"

数える部分をスクリプトが固定しているので、毎月の結果を前月と並べて比較できます。Claude Codeに渡すのは数字の意味づけと優先順位づけで、抽出そのものではありません。

つまずきやすい点

落とし穴

一時停止中のキャンペーンまで重複に数えてしまう

一時停止中のキャンペーンのキーワードは配信に使われないため、有効なキーワードとの「重複」は実害のない重複です。上のスクリプトは、親が一時停止のキーワードを重複の検査から外しています。古いキャンペーンを先に整理したいときは、観点6の結果を起点にします。

落とし穴

除外キーワードを混ぜて数える

Type が Negative や Campaign negative の行は、通常のキーワードとは動きが違います。同じ語が通常キーワードと除外キーワードの両方にあるのは重複ではありません。除外キーワードの見直しは、検索語句と合わせて行います。

落とし穴

Excelで開き直して保存する

ヘルプのとおり、エディタにXLSファイルは取り込めません。書き出したCSVをExcelで開いて整形し、そのまま保存し直すと、文字コードや区切り文字が変わることがあります。監査用のコピーは別名で置き、元のファイルは触らないでおきます。

広告の不承認が原因でキーワードが動いていないケースは、ClaudeでGoogle広告の不承認を修正し再審査に出す手順で扱っています。構造の問題だけでなく、配信されない原因を探すときは、あわせて見る価値があります。

まとめ

アカウント構造の監査は、数える部分をスクリプトに固定し、意味づけをClaude Codeに任せ、反映はエディタの提案された変更で確かめる、という3段にすると続けやすくなります。重複は語順違いまで拾い、消す前に意図を確かめます。粒度は数より先にテーマの混在を見ます。命名は規則を CLAUDE.md に置き、検査の正規表現と同時に直します。

同じ棚卸しの考え方は、タグ管理にも使えます。GTMコンテナの棚卸しをClaude Codeで進めるでは、書き出したJSONを同じ型で調べています。

この記事を共有:XはてブLinkedIn