【業務効率化】Claude × GoogleDocs × Colabで月次レポートを自動生成|全スクリプト公開

さらにステップ半減!(2025/6/12更新)
生成AI API 導入バージョンもリリースしました。
Gemini版・OpenAI版・Claude版 のレポートサンプルもつけましたので、ぜひご覧ください♪
▶︎ Gemini APIでレポート生成ステップをさらに半減|OpenAI/Claudeにも対応

先月の記事
▶︎【後編・全コード公開】ChatGPTでGoogleスライド月次レポート自動化システムを構築する方法
で、いったん完結したと思っていた「月次レポート自動化システム構築」シリーズ。

しかし、実はまだ一歩先がありました。

先月の時点でのフローは、下記の4ステップで [1] [2] [4] を人間(=私)がやっていたのですが、
Step[2] の作業が非常にめんどうくさい。

【従来のレポート生成フロー】
  • Step[1]
    Colab(Python)

    人間(=私)が、下記Pythonスクリプトの測定期間を設定して実行すると、データ・表の画像とCSVファイルをGoogleドライブ上に自動生成される

  • Step[2]
    CSVとデータ・表画像のURLを取得

    人間(=私)が、Pythonが生成したCSVと、データ・表の画像URL一覧をChatGPTに渡して、レポート生成用のGAS作成を指示する

  • Step[3]
    ChatGPT

    Googleスライドによるレポート生成用のGASを生成

  • Step[4]
    Googleスライド

    人間(=私)がGoogleスライド・テンプレファイルのAppsScript欄に生成されたGASをコピペ→実行したら、レポート生成完了

[2-1] Googleドライブに生成された、すべての画像ファイルのURLを取得して
[2-2] CSVを全部ダウンロードして
[2-3] ChatGPTに添付してファイル作成指示のプロンプトを打つ

たった画像×4ファイル、CSV×2ファイルしかない現時点で既にめんどうくさい。

これは何とかしなくては、と考えていたときに入ってきたのが、
Claude は デフォルトで Google Docs の中身が読める
という情報でした。

1. そこで改造案を考えました

Google Docs を読めるということは、

  1. CSVファイルに書き出していた「カンマ区切りの表データ」2ファイル分をGoogle Docsのファイル1 に書き出して、
  2. 生成した画像URL一覧をGoogle Docsのファイル2 に書き出して
  3. あとはClaudeに読み込んでもらったら終わりじゃない?

と、現状のPythonスクリプトを見せながら、Claudeの「参謀くん」に聞いてみたところ・・・

2. なんと1分後に実現してしまいました(Claudeに丸投げでw)

「はい、できますよ」とあっさり改造してくれました。

・・・。( ゚д゚)ファ?

しかもなんと、実行ログの最後に、生成したGoogle DocsのURLを表示(下図赤枠)するようにしてくれていました。天才かこいつ。

つまり、今後私がやることは、

【今後のレポート生成フロー】
  • Step[1]
    Colab(Python)

    人間(=私)が、下記Pythonスクリプトの測定期間を設定して実行すると、データ・表の画像に加えて、データ一覧と画像ファイルURL一覧を記載したGoogleDocsが自動生成され実行ログ末尾にDocsファイルのURLも表示される

  • Step[2]
    CSVとデータ・表画像のURLを取得

    人間(=私)は、実行ログ末尾のURLのコピペをClaudeに渡して、レポート生成用のGAS作成を指示するだけ

  • Step[3]
    Claude

    GoogleDocsのデータを自動的に読み込んでレポート生成用のGASを生成

  • Step[4]
    Googleスライド(ここは従来と同じ)

    人間(=私)がGoogleスライド・テンプレファイルのAppsScript欄に生成されたGASをコピペ→実行したら、レポート生成完了

要するに、

  1. CalabのPythonスクリプトの日付だけ変える
  2. 実行ボタンを押す
  3. あとは、実行ログの最後に出てくるURL2つと、前月分のGASをClaudeにコピペする
  4. Claudeに「Google Docsの2ファイルのデータを使って、添付のGASを生成して」とお願いする
  5. Claudeが生成した分析コメントつきのGASを、GoogleスライドのApps Script欄にコピペして実行ボタンを押す

以上終わり。たった15分ほどの改造作業で、大幅な省力化が実現してしまいました。
これなら本格的な分厚めレポートでも対応できそうです。

3. ついでにもう一つ改善してもらいました

サーチコンソールのAPIで「/tag/chatgptのセッション切れ/」のような日本語URLを取得すると「/tag/chatgpt%E3%81%AE%E3%82%BB%E3%83・・・」のように変換(=URLエンコードといいます)されてしまい、
何のページかわからなくなるので、Pythonでデコード(=変換前の状態に戻すこと)してもらうようにしました

4. 成果物はこちら

結果、以下のようなスライドが完成しました。
構成は前回と全く同じですが、レポート生成にかかる工数が大幅に減り、URLエンコードされて読みにくかったGSCのレポートも読みやすくなり、ようやく実用化可能なものになったように思います。

▶︎ 自動生成されたレポート(Googleスライド)

  • 表紙
  • サマリ+所感(GPTが自動生成)
  • GA4グラフ+表(Drive画像)
  • GSCグラフ+表(Drive画像)←文字化けしていた日本語URLが読めるようになった!
  • 今後の打ち手(GPT提案ベース)

5. 改造の結果

さて、先月の記事にも書いた↓この言葉ですが、本当の意味で現実になってきました。

もうレポートは「分析コメントをリライトするだけ」でいい、という状態が現実のものになってきました。

初回のテンプレファイル作成や改造の作業だけは相変わらず必要ですが、
レポート運用については、分析コメントと改善提案のリライト以外、ほぼ工数をかけずにできるようになりました。

6. 最後に

というわけで、ついに実用化レベルになった「レポート自動生成システム
ご興味のある方は、ぜひお問い合わせフォームまでご一報ください!
無料相談も大歓迎です!

補足:画像出力時の注意点(前回と同じ)

Pythonから自動生成される画像には、画像の周囲に不要な白い余白ができてしまうことがあります。
そこで、Pythonの “Pillow” ライブラリの “crop()” メソッドという機能を使って、白背景との差分を検出し、画像の余白を自動でトリミングする処理も組み込みました。
ImageChops.difference() + getbbox()crop() の流れ)。

※今回のサンプルでは、表画像に対して余白削除の処理をしています。

調整ポイント(前回と同じ):

  • グラフの縦横比はColab側で一度固定する(例:figsize=(8, 4.5) など)
  • japanize_matplotlib を使っても、環境によっては日本語が崩れることがある(→フォント明示推奨)
  • PNGに変換する際は、余白の自動除去(bbox_inches=”tight”)を指定しておくと、貼り付け時のトリミングが不要になる

参考リンク集(前回と同じ)

【付録】今回使用したPythonの全スクリプトとClaudeが生成したGAS

Pythonスクリプトの使い方

Pythonスクリプトは、Google Colabでの使用を前提として作成したものです。他の環境での動作は保証できませんのでご了承ください。

また、下記のブロックで「# ←差し替え必須」のコメントがついている行は、各自の環境にあわせて書き換えてください。

※なお下記のコードを使用する場合は、GASでのレポート生成時に画像格納フォルダを公開状態にする必要がありますが、製品版では、レポート生成時のみ自動的に公開状態に切り替え、生成後非公開に戻す機能を実装しています。

# ========== 設定 ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report"
KEY_PATH = "/content/drive/MyDrive/content/your-service-account.json"  # ←差し替え必須
PROPERTY_ID = "your-GA4-property-ID"  # ←差し替え必須
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SITE_URL = "https://your-site.com/"  # ←差し替え必須

また、測定期間および比較期間は下記のブロックで設定してください。

START_DATE = "2025-04-01"
END_DATE = "2025-04-30"
START_DATE_YOY = "2024-04-01"
END_DATE_YOY = "2024-04-30"

Python:GA4・GSCグラフ/表画像・CSV自動生成スクリプト(Colab用)

# Colab用:GA4/GSCの前年同月データを含むグラフ・CSV・表画像出力スクリプト(ファイル名にYYMM付き)

!pip install --upgrade google-analytics-data google-auth google-api-python-client matplotlib pandas seaborn japanize-matplotlib
!pip install pillow
from PIL import Image, ImageChops

import os
from google.colab import drive

# ========== Driveマウント(初回のみ必要。再実行時は自動スキップ) ==========
if not os.path.exists("/content/drive/MyDrive"):
    drive.mount("/content/drive")
    assert os.path.exists("/content/drive/MyDrive"), "[!]Driveマウントが失敗しています"
else:
    print("Driveは既にマウント済みです")

# ========== 設定 ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report"
KEY_PATH = "/content/drive/MyDrive/content/your-service-account.json"  # ←差し替え必須
PROPERTY_ID = "your-GA4-property-ID"  # ←差し替え必須
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SITE_URL = "https://your-site.com/"  # ←差し替え必須

import datetime
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
from urllib.parse import urlparse
from google.oauth2 import service_account
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import DateRange, Metric, Dimension, RunReportRequest
from googleapiclient.discovery import build

START_DATE = "2025-04-01"
END_DATE = "2025-04-30"
START_DATE_YOY = "2024-04-01"
END_DATE_YOY = "2024-04-30"

yyyymm = datetime.datetime.strptime(START_DATE, "%Y-%m-%d").strftime("%y%m")
os.makedirs(OUTPUT_DIR, exist_ok=True)

plt.rcParams['font.family'] = 'IPAexGothic'

# ========== 認証 ==========
credentials = service_account.Credentials.from_service_account_file(KEY_PATH)
client = BetaAnalyticsDataClient(credentials=credentials)
webmasters_service = build("searchconsole", "v1", credentials=credentials.with_scopes(SCOPES))

# ========== 画像の余白削除 ==========
def trim_white_margin(img_path):
    img = Image.open(img_path).convert("RGB")
    bg = Image.new("RGB", img.size, (255, 255, 255))
    diff = ImageChops.difference(img, bg)
    bbox = diff.getbbox()
    if bbox:
        img.crop(bbox).save(img_path)  # 上書き保存

# ========== 表用補助関数 ==========
def strip_domain(url):
    if "?fbclid=" in url:
        return url.split("?fbclid=")[0] + "?fbclid="
    parsed = urlparse(url)
    return parsed.path if parsed.scheme else url

# ========== GA4 日次セッション取得・グラフ描画 ==========
def fetch_ga4_sessions(start_date, end_date):
    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[Dimension(name="date")],
        metrics=[Metric(name="sessions")],
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
    )
    response = client.run_report(request)
    return pd.DataFrame([
        {"date": row.dimension_values[0].value, "sessions": int(row.metric_values[0].value)}
        for row in response.rows
    ])

ga4_df = fetch_ga4_sessions(START_DATE, END_DATE).sort_values('date')
ga4_yoy_df = fetch_ga4_sessions(START_DATE_YOY, END_DATE_YOY).sort_values('date')

ga4_df['day'] = pd.to_datetime(ga4_df['date']).dt.strftime('%m-%d')
ga4_yoy_df['day'] = pd.to_datetime(ga4_yoy_df['date']).dt.strftime('%m-%d')
day_order = sorted(set(ga4_df['day']) | set(ga4_yoy_df['day']))
ga4_df = ga4_df.set_index('day').reindex(day_order).fillna(0)
ga4_yoy_df = ga4_yoy_df.set_index('day').reindex(day_order).fillna(0)

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(day_order, ga4_df['sessions'], label="2025", color="blue", linewidth=2)
ax.plot(day_order, ga4_yoy_df['sessions'], label="2024", color="slategray", linestyle="dashed", linewidth=1.5)
ax.set_title("GA4 日次セッション(前年同月比較)")
ax.set_xticks(day_order[::2])
ax.set_xticklabels(day_order[::2], rotation=45)
ax.legend()
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/ga4_sessions_yoy_{yyyymm}.png")
plt.close()

ga4_merged_df = pd.DataFrame({
    'date': [f"2025-{d}" for d in day_order],
    'sessions_2025': ga4_df['sessions'].values,
    'sessions_2024': ga4_yoy_df['sessions'].values
})
ga4_merged_df.to_csv(f"{OUTPUT_DIR}/ga4_daily_sessions_{yyyymm}.csv", index=False)

# ========== GSC 日次クリック・表示取得・グラフ描画 ==========
def fetch_gsc_metric(start_date, end_date):
    response = webmasters_service.searchanalytics().query(
        siteUrl=SITE_URL,
        body={
            "startDate": start_date,
            "endDate": end_date,
            "dimensions": ["date"],
            "rowLimit": 1000
        }
    ).execute()
    return pd.DataFrame([
        {"date": row["keys"][0], "clicks": row["clicks"], "impressions": row["impressions"]}
        for row in response.get("rows", [])
    ])

gsc_df = fetch_gsc_metric(START_DATE, END_DATE)
gsc_yoy_df = fetch_gsc_metric(START_DATE_YOY, END_DATE_YOY)
gsc_df['day'] = pd.to_datetime(gsc_df['date']).dt.strftime('%m-%d')
gsc_yoy_df['day'] = pd.to_datetime(gsc_yoy_df['date']).dt.strftime('%m-%d')
gsc_df = gsc_df.groupby('day')[['clicks', 'impressions']].sum().reset_index()
gsc_yoy_df = gsc_yoy_df.groupby('day')[['clicks', 'impressions']].sum().reset_index()
gsc_df = gsc_df.set_index('day').reindex(day_order).fillna(0)
gsc_yoy_df = gsc_yoy_df.set_index('day').reindex(day_order).fillna(0)

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(day_order, gsc_df['clicks'], label="Clicks 2025", color='blue', linewidth=2)
ax.plot(day_order, gsc_yoy_df['clicks'], label="Clicks 2024", color='slategray', linestyle='dashed', linewidth=1.5)
ax.plot(day_order, gsc_df['impressions'], label="Impressions 2025", color='darkorange', linewidth=2)
ax.plot(day_order, gsc_yoy_df['impressions'], label="Impressions 2024", color='saddlebrown', linestyle='dashed', linewidth=1.5)
ax.set_title("GSC 日次クリック・表示回数(前年同月比較)")
ax.set_xticks(day_order[::2])
ax.set_xticklabels(day_order[::2], rotation=45)
ax.legend()
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/gsc_clicks_impressions_yoy_{yyyymm}.png")
plt.close()

gsc_merged_df = pd.DataFrame({
    'date': [f"2025-{d}" for d in day_order],
    'clicks_2025': gsc_df['clicks'].values,
    'impressions_2025': gsc_df['impressions'].values,
    'clicks_2024': gsc_yoy_df['clicks'].values,
    'impressions_2024': gsc_yoy_df['impressions'].values
})
gsc_merged_df.to_csv(f"{OUTPUT_DIR}/gsc_daily_clicks_{yyyymm}.csv", index=False)

# ========== 表画像:GA4 LPテーブル ==========
def fetch_ga4_lp_table(start_date, end_date):
    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[Dimension(name="landingPagePlusQueryString")],
        metrics=[
            Metric(name="sessions"),
            Metric(name="activeUsers"),
            Metric(name="newUsers"),
            Metric(name="engagementRate")
        ],
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
        limit=20
    )
    response = client.run_report(request)
    base_rows = [
        [
            i + 1,
            strip_domain(row.dimension_values[0].value),
            int(row.metric_values[0].value),
            int(row.metric_values[1].value),
            int(row.metric_values[2].value),
            f"{float(row.metric_values[3].value) * 100:.1f}%"
        ] for i, row in enumerate(response.rows)
    ]
    df = pd.DataFrame(base_rows, columns=["#", "LP", "セッション数", "アクティブユーザー", "新規ユーザー", "エンゲージメント率"])
    return df

ga4_table = fetch_ga4_lp_table(START_DATE, END_DATE)
fig, ax = plt.subplots(figsize=(10, 0.5 + len(ga4_table) * 0.35))
ax.axis('off')
table = ax.table(cellText=ga4_table.values,
                 colLabels=ga4_table.columns,
                 cellLoc='center',
                 loc='center',
                 colWidths=[0.03, 0.45, 0.09, 0.12, 0.09, 0.12])
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.15)
for col in range(len(ga4_table.columns)):
    table[0, col].set_facecolor('#DDDDDD')
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png")
plt.close()
trim_white_margin(f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png")
ga4_table.to_csv(f"{OUTPUT_DIR}/GA4-seo_lp_table_{yyyymm}.csv", index=False)

# ========== 表画像:GSC クエリテーブル ==========
import urllib.parse  # URLデコード用にインポート

def fetch_gsc_query_table(start_date, end_date):
    response = webmasters_service.searchanalytics().query(
        siteUrl=SITE_URL,
        body={
            "startDate": start_date,
            "endDate": end_date,
            "dimensions": ["page", "query"],
            "rowLimit": 500  # 500件に設定
        }
    ).execute()
    
    # データ取得状況をログ出力(任意)
    print(f"GSCデータ取得: {len(response.get('rows', []))}件")
    
    # URLデコード機能付きの文字数制限関数
    def process_url(url, max_length=20):
        """
        URLをデコードして整形する
        1. ドメイン部分を削除
        2. URLエンコードされた日本語をデコード
        3. 長さを制限
        """
        # ドメイン部分を削除
        url = strip_domain(url)
        
        # URLデコード(日本語などの文字を元に戻す)
        try:
            url = urllib.parse.unquote(url)
        except Exception as e:
            print(f"URLデコードエラー: {e}")
        
        # 長さを制限
        if len(url) > max_length:
            return url[:max_length] + "…"
        
        return url
    
    # 検索クエリ処理関数
    def process_query(query, max_length=25):
        """検索クエリを整形"""
        if not isinstance(query, str):
            return "(no query)"
            
        # 短いクエリは特別処理
        if len(query) <= 3:
            return f"「{query}」"
            
        # 長いクエリは切る
        if len(query) > max_length:
            return query[:max_length] + "…"
            
        return query
    
    # まず生データを一時的なデータフレームに格納
    temp_data = []
    for i, row in enumerate(response.get("rows", [])):
        # 空のクエリやURLを処理
        page = row["keys"][0] if row["keys"][0] else "/"
        query = row["keys"][1] if row["keys"][1] else "(not set)"
        
        temp_data.append({
            "page": page,
            "query": query,
            "clicks": row["clicks"],
            "impressions": row["impressions"],
            "position": float(row["position"]),
            "ctr": (row["clicks"] / row["impressions"] * 100) if row["impressions"] > 0 else 0
        })
    
    # データフレームを作成してソート
    temp_df = pd.DataFrame(temp_data)
    
    # 第1優先:クリック数 降順、第2優先:表示回数 降順でソート
    temp_df = temp_df.sort_values(by=["clicks", "impressions"], ascending=[False, False])
    
    # ソートされたデータから上位20件を取得して表示用に整形
    rows = []
    for i, (_, row) in enumerate(temp_df.head(20).iterrows()):
        rows.append([
            i + 1,  # 連番を振り直し
            process_url(row["page"], 20),  # URLをデコードして処理
            process_query(row["query"], 25),  # 検索クエリを処理
            row["clicks"],
            row["impressions"],
            f"{row['position']:.1f}",
            f"{row['ctr']:.2f}%" if row["impressions"] > 0 else "-"
        ])
    
    df = pd.DataFrame(rows, columns=["#", "LP", "検索クエリ", "クリック数", "表示回数", "平均掲載順位", "CTR"])
    return df

gsc_table = fetch_gsc_query_table(START_DATE, END_DATE)
fig, ax = plt.subplots(figsize=(10, 0.5 + len(gsc_table) * 0.35))
ax.axis('off')
table = ax.table(cellText=gsc_table.values,
                 colLabels=gsc_table.columns,
                 cellLoc='center',
                 loc='center',
                 colWidths=[0.03, 0.21, 0.26, 0.1, 0.1, 0.1, 0.1])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.15)
for col in range(len(gsc_table.columns)):
    table[0, col].set_facecolor('#DDDDDD')
plt.tight_layout()
plt.savefig(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png")
plt.close()
trim_white_margin(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png")
gsc_table.to_csv(f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.csv", index=False)



# ===== GoogleドキュメントへのCSVデータ出力(Claudeが読み取れるようにするため)=====
!pip install --upgrade google-api-python-client google-auth-oauthlib

from googleapiclient.discovery import build
from google.colab import auth
from googleapiclient.http import MediaIoBaseUpload
import io

# Google認証
print("Google API認証を行います...")
auth.authenticate_user()
print("認証が完了しました")

# 保存先フォルダIDを取得(画像と同じ場所に保存するため)
def get_folder_id(file_path):
    """ファイルが存在するフォルダのIDを取得する"""
    # Driveサービス初期化
    drive_service = build('drive', 'v3')

    # ファイル名を取得
    file_name = os.path.basename(file_path)

    # ファイルを検索
    search_response = drive_service.files().list(
        q=f"name='{file_name}' and trashed = false",
        fields="files(id, name, parents)"
    ).execute()

    # ファイルが見つかった場合、その親フォルダIDを返す
    if 'files' in search_response and search_response['files']:
        file_info = search_response['files'][0]
        if 'parents' in file_info and file_info['parents']:
            return file_info['parents'][0]

    # 見つからない場合はNoneを返す
    return None

# Google Docsドキュメントを作成する関数
def create_google_doc(title, content, folder_id=None):
    """Google Docsドキュメントを作成し、内容を設定する"""
    # Driveサービス初期化
    drive_service = build('drive', 'v3')

    # 空のGoogle Docsファイルを作成
    file_metadata = {
        'name': title,
        'mimeType': 'application/vnd.google-apps.document'
    }

    # フォルダが指定されている場合は、そのフォルダに保存
    if folder_id:
        file_metadata['parents'] = [folder_id]

    # 一時的なテキストファイルとしてアップロード後、Google Docsに変換
    media = MediaIoBaseUpload(
        io.BytesIO(content.encode('utf-8')),
        mimetype='text/plain',
        resumable=True
    )

    # ファイル作成
    file = drive_service.files().create(
        body=file_metadata,
        media_body=media,
        fields='id,webViewLink'
    ).execute()

    # 共有設定
    drive_service.permissions().create(
        fileId=file['id'],
        body={'type': 'anyone', 'role': 'reader'},
        fields='id'
    ).execute()

    return file['id'], file['webViewLink']

# CSVデータをGoogle Docsに保存する関数
def save_csv_to_doc(title, csv_files, folder_id=None):
    """CSVファイルの内容をGoogle Docsドキュメントに保存する"""
    # メタデータ情報を含む内容の作成
    content = f"{title}\n\n"
    content += f"期間: {START_DATE} ~ {END_DATE}\n"
    content += f"前年同期: {START_DATE_YOY} ~ {END_DATE_YOY}\n\n"

    # 各CSVファイルの内容を追加
    for section_name, csv_path in csv_files.items():
        # セクションタイトル
        content += f"## {section_name}\n\n"

        # CSVの内容を読み込み
        try:
            with open(csv_path, 'r') as file:
                csv_content = file.read()

            # CSVコンテンツを追加
            content += csv_content + "\n\n"
        except Exception as e:
            print(f"[!]{csv_path}の読み込みに失敗しました: {e}")
            content += f"[!]データの読み込みに失敗しました: {e}\n\n"

    # Google Docsドキュメントを作成
    doc_id, doc_url = create_google_doc(title, content, folder_id)
    print(f"データドキュメントを作成しました: {doc_url}")
    return doc_url

# 画像URLのGoogle Docsドキュメントを作成
def save_image_urls_to_doc(title, image_files, folder_id=None):
    """画像ファイルのURLをGoogle Docsドキュメントに保存する"""
    # Driveサービス初期化
    drive_service = build('drive', 'v3')

    # 内容の準備
    content = f"{title}\n\n"

    # 各画像ファイルの情報を取得
    for img_path in image_files:
        # ファイル名からIDを取得
        img_name = os.path.basename(img_path)
        search_response = drive_service.files().list(
            q=f"name='{img_name}' and trashed = false",
            fields="files(id, name, webViewLink)"
        ).execute()

        if 'files' in search_response and search_response['files']:
            file_info = search_response['files'][0]
            file_id = file_info['id']

            # 表示用URL
            view_url = f"https://drive.google.com/uc?export=view&id={file_id}"

            # コンテンツに追加
            content += f"{img_name}: {view_url}\n\n"

    # Google Docsドキュメントを作成
    doc_id, doc_url = create_google_doc(title, content, folder_id)
    print(f"画像URL情報を作成しました: {doc_url}")
    return doc_url

# CSVファイル一覧
csv_files = {
    "GA4 日次セッション": f"{OUTPUT_DIR}/ga4_daily_sessions_{yyyymm}.csv",
    "GA4 ランディングページ": f"{OUTPUT_DIR}/GA4-seo_lp_table_{yyyymm}.csv",
    "GSC 日次クリック・表示": f"{OUTPUT_DIR}/gsc_daily_clicks_{yyyymm}.csv",
    "GSC クエリテーブル": f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.csv"
}

# 画像ファイル一覧
image_files = [
    f"{OUTPUT_DIR}/ga4_sessions_yoy_{yyyymm}.png",
    f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png",
    f"{OUTPUT_DIR}/gsc_clicks_impressions_yoy_{yyyymm}.png",
    f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png"
]

# 画像ファイルの保存先フォルダIDを取得
folder_id = None
if len(image_files) > 0:
    folder_id = get_folder_id(image_files[0])
    if folder_id:
        print(f"保存先フォルダIDを取得しました: {folder_id}")
    else:
        print("[!]保存先フォルダIDの取得に失敗しました。ルートフォルダに保存します。")

# エラーハンドリング付きでGoogle Docsへの出力を実行
try:
    data_doc_url = save_csv_to_doc(f"Analytics Data {yyyymm}", csv_files, folder_id)
    image_url_doc = save_image_urls_to_doc(f"Image URLs {yyyymm}", image_files, folder_id)

    print("Google Docsへの出力が完了しました")
    print(f"・データドキュメント: {data_doc_url}")
    print(f"・画像URL情報: {image_url_doc}")
except Exception as e:
    print(f"[!]Google Docsへの出力で問題が発生しました: {e}")

print("前年同月の比較を含むグラフ+CSV+表画像出力(YYMM付きファイル名)が完了しました。")

Claudeが生成したGASスクリプト(画像・コメント入りスライド自動生成)

GoogleスライドのテンプレートとGASの使い方については、前編の記事をご覧ください。

let latestPresentationId = ""; // グローバル変数でIDを保持

// 実行関数(全部まとめて呼び出し)
function generateFullReport() {
  createMonthlyReport();
  insertImageToSummarySlide();
  insertGSCImageToSlide();
  insertGA4LandingPageTable();
  insertGSCLandingQueryTable();
  Logger.log("月次レポート:完全自動生成 完了!");
}

// 1. テンプレートから複製して変数を差し込み
function createMonthlyReport() {
  const templateId = "1n_TmPVJEGqcNdY2SBnj9CTFfF3ZCFOO_vMF9G70Fwxg";
  const newTitle = "サンプル株式会社様|2025年4月 月間レポート 自動生成テスト";

  const data = {
    "{{yearmonth}}": "2025年4月",
    "{{period}}": "2025/04/01〜2025/04/30",
    "{{summary}}": "4月は前年同月比で顕著な成長が見られ、GA4のセッション数は約5倍、GSCのクリック数は約4倍と大幅に改善しました。特にChatGPT関連コンテンツの強い牽引力が見て取れます。\n全体的に指標の底上げが進み、サイトの認知度とコンテンツの充実による相乗効果が表れ始めています。検索からの流入が安定して増加する傾向が定着し、サイトの持続的な成長基盤が形成されつつあります。",
    "{{highlight}}": "GA4の2025年4月の月間セッション数は296件で、前年同月比では5倍の伸びとなりました(前年59件)。\n1日あたりの平均セッション数も約10件と大幅に向上し、特に4月9日から25日にかけては毎日10件以上のセッションが安定して発生しています。\nランディングページ別では、「/chatgpt_trouble_faq/」が46セッションで最も多く、次いでトップページが40セッションと続いており、ChatGPT関連コンテンツが強い牽引力を持っていることが明らかです。",
    "{{insight}}": "ランディングページのエンゲージメント分析から、「/gas-ga4-api-spread_sheet/」は83.3%、「/gtm-hash/」は100%と非常に高いエンゲージメント率を示しています。これらの技術的なコンテンツは、ユーザーの明確な意図に応えていると推察されます。\nBigQuery関連のページ群(「/bigquery_looker-studio_chatgpt/」や「/bigquery_ga4_lookerstudio/」)も複数上位に入っており、GA4データ分析に関する専門的なニーズに応えるハブとなりつつあります。\n一方で、平均的なエンゲージメント率は40〜60%程度であり、滞在時間や回遊性をさらに高める余地があります。",
    "{{gsc}}": "2025年4月のGSCデータでは、検索表示回数が2,504件(前年同月比約55%増)、クリック数が96件(前年比4倍)を記録し、CTRは約3.8%まで改善しました。\n特に注目すべきは4月下旬の急激な成長で、4月29日には表示回数235件、4月30日には355件と大幅に増加しています。\n検索クエリでは「chatgpt セッション切れ」「chat gpt 画像生成 できない」などChatGPTの問題解決に関する検索が上位を占め、ニッチな課題に特化したコンテンツが価値を発揮しています。また「ga4 api python」などの技術クエリも目立ち始めています。",
    "{{action}}": "1. **ChatGPT関連コンテンツの拡充**: 特に問題解決・トラブルシューティング系の記事が好調なため、さらに深掘りした派生コンテンツの作成を推奨します。\n2. **GA4/BigQuery技術記事の強化**: エンゲージメント率の高い技術記事のシリーズ化を進め、専門性の高いユーザー層の囲い込みを図りましょう。\n3. **検索上位表示の記事のリライト**: GSCデータで表示はされているがクリック率が低いページを特定し、タイトルや説明文の最適化を実施することでCTRの向上が期待できます。\n4. **内部リンク構造の最適化**: 関連性の高いコンテンツ間のリンク強化により、セッション持続時間とページビュー数の向上を目指します。"
  };

  const newPresentation = DriveApp.getFileById(templateId).makeCopy(newTitle);
  latestPresentationId = newPresentation.getId();
  const presentation = SlidesApp.openById(latestPresentationId);
  const slides = presentation.getSlides();

  slides.forEach(slide => {
    for (const key in data) {
      slide.replaceAllText(key, data[key]);
    }
  });

  Logger.log("スライド生成完了: https://docs.google.com/presentation/d/" + latestPresentationId + "/edit");
  return latestPresentationId;
}

// 共通関数:画像を比率維持して挿入
function insertImagePreserveAspect(slide, imageUrl, width, top, left) {
  const image = slide.insertImage(imageUrl);
  const aspectRatio = image.getHeight() / image.getWidth();
  image.setWidth(width);
  image.setHeight(width * aspectRatio);
  image.setTop(top);
  image.setLeft(left);
}

// GA4セッション推移グラフ(スライド4)
function insertImageToSummarySlide() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=1-SXBq1r-cPE4P3vBMe_DPIU5dNKXVP7g";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[3];
  insertImagePreserveAspect(slide, imageUrl, 600, 100, 50);
}

// GSCクリック・表示グラフ(スライド8)
function insertGSCImageToSlide() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=1-H_Obh7OqHrDMirJNygRo1a3pd1I7LXT";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[7];
  insertImagePreserveAspect(slide, imageUrl, 600, 100, 50);
}

// GA4 LP表(スライド5)
function insertGA4LandingPageTable() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=1-B8n_OGGVu3wOpdF9zILANsjEGrZJJXP";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[4];
  insertImagePreserveAspect(slide, imageUrl, 550, 80, 75);
}

// GSC クエリ表(スライド9)
function insertGSCLandingQueryTable() {
  const imageUrl = "https://drive.google.com/uc?export=view&id=1-9I18PwWoVOkbqbEPGa9RIT4BJAwOBV7";
  const slide = SlidesApp.openById(latestPresentationId).getSlides()[8];
  insertImagePreserveAspect(slide, imageUrl, 550, 80, 75);
}

■ 無料相談・お問合せ (弊社からの営業は一切しません)

「うちの場合は自動化・省力化できるのか?」
まずはお気軽に無料相談をご利用ください。

  • オンライン全国対応
  • 相談だけでもOK
  • 営業メールなどは一切しません
  • 技術者が直接ヒアリングして回答します
  • 概算費用・工数をご提示します

弊社の主要サービス ▼