
GoogleWorkspaceのビジネスプランを使用しているので、自動的にGeminiの有料版を使うことができるのですが、この数ヶ月で Geminiの性能が格段に上がってきているような気がします。
以前使ったことがある無料版(1.5Flashだったかな?)は日本語も分析も貧弱でしたが、
現在の有料版である、2.5Proは分析もライティングも十分使えるレベル。Flash版も以前に比べたら性能が格段に良くなりました。
また、Gemini が Google製品であることも私にとってはありがたい。
GA4・サーチコンソール(GSC)・GTM・BigQuery・GASなどのGoogle製品を多用している私にとって、Googleの裏仕様まで教えてくれる「Gemini先生」という存在は非常に助かります。
さらに、APIが他のAIに比べて激安ということも非常にありがたいですね。
1. レポート自動生成システムにGemini APIを導入しました
さて、これまで改良し続けてきた「レポート自動生成システム」は、あくまでもレポート生成GASの作成をAIアプリに依頼するための資料を作るためのものでした(詳細は2章参照)。
今回は、Gemini API を導入することで自動化をさらに進め「いきなりレポート生成GASまで自動作成」してしまおう!という改造になります。
今回のプロトタイプには、前述のとおり「Google製品との相性がよくて激安!」な、Gemini API を使ってみることにしました。
2. Gemini APIで「レポート自動生成システム」はどう変わるのか?
前回の「レポート自動生成システム」は下記のようなシステムでした。
- 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をコピペ→実行したら、レポート生成完了
Gemini API導入によってフローはどう変わるか?
前回までは、データと図表のみをPythonで作成し、Claudeなどの生成AIで、分析コメントの作成とGASの生成をしていましたが、
今回は、Gemini APIの導入によって、GAS生成までを1ステップで行えるようになりました。
なお、今回の検証では Gemini APIを使用しましたが、OpenAI API や Claude APIでも実装可能です。(※プロンプト部分の書式もAIによって変わるのでご注意ください)
レポートサンプル(他の生成AI APIによるレポートとの比較)
実際の各AIの分析・コメント作成能力を比較していただくため、リライトは一切せず、改行・改ページなど体裁の調整しかしていません。生成AIによって、コメントのクセもAPIの費用も異なりますので、ご予算とお好みに応じてお選びください。
4ステップ▶︎2ステップに半減!
- Step[1]Colab(Python)
人間が、下記Pythonコードの測定期間を設定して実行すると、データ・表の画像に加えて、Gemini APIによるデータ分析も行い、分析コメント付きのレポート生成用GASが1ステップで生成される
- Step[2]Googleスライド(ここは従来と同じ)
人間が、Googleスライド・テンプレファイルのAppsScript欄に生成されたGASをコピペ→実行したら、レポート生成完了
3. 使用上の注意:カスタマイズは結構大変です(笑)
この「レポート自動生成システム」一度稼働してしまえば、非常に効率的にレポートを作ることができるのですが、本格的に運用する場合、カスタマイズは結構大変です。
具体的には:
- Googleスライドでのテンプレートファイル作成
- 雛形レポートの作成(図表画像やコメント文はダミーでOK)
- 図表画像の挿入位置とサイズを調整
- 文字サイズ決定後、各コメントの文字数を確認
- 最後にダミー画像を削除し、ダミーコメントを変数に置き換える
- Pythonコードの調整
- 1. で作成したテンプレートファイルにあわせて、PythonのGASコード生成部分を修正
- 生成AI APIに投げるプロンプトを作成・調整(※調整は必須になると思います)
- 設定が完了したら、実際にGASコードを生成してみて、Googleスライドで実行。必要があれば、画像のサイズ・位置や、プロンプトなどを調整
といった感じになります。
もし「そんな調整やってる時間ない」「設定が難しい」「プロンプトを一緒に考えてほしい」という方は、導入・設定サービスのご利用をご検討ください。
ご自身で設定されたい場合は、オンラインレクチャー方式でのサポートもしています。
4. 今回使用したPythonコード(Google Colab用です)
最後に、今回作成したサンプルコード全文をご紹介します。
Googleスライドでのテンプレート作成方法ついては、下記のリンクをご参照ください。
▶︎ 【前編】GPT×GASで実現!Googleスライド月次レポート自動生成の仕組みと導入手順
テンプレートができたら、画面上部メニューから「拡張機能 > Apps Script」画面を開いて、生成したGASコードをコピペし、実行するとレポートが自動生成されます。
分析・提案コメントはどうしても、不自然な表現や誤った解釈が出てくることがありますので、必ずチェックしてリライトすることを強く推奨します。
下記Pythonコードの使い方
下記のPythonコードに設定項目を入力した上で、Corab上で実行すると、Googleドライブ内にGoogleスライド テンプレートファイル用のレポート生成GASコードが
Generated_MonthlyReport_Script_202504.txt(※2025年4月のレポートの場合)
という名前のテキストファイルとしてGoogleドライブの指定フォルダ内に生成されます。
Python初期設定/測定期間設定(月跨ぎも可能です)
# ========== 設定 (重要:必ずご自身の環境に合わせてください) ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report_Output_Automated"
KEY_PATH = "/content/drive/MyDrive/Report_Input/your-service-account.json"
PROPERTY_ID = "your-GA4-property-ID"
SITE_URL = "https://your-site.com/"
GEMINI_API_KEY = 'YOUR_GEMINI_API_KEY'
SCOPES_GSC = ["https://www.googleapis.com/auth/webmasters.readonly"]
# 新規追加:GAS用設定
TEMPLATE_ID = "your-google-slides-template-id" # GoogleスライドテンプレートのID
IMAGE_FOLDER_ID = "your-image-folder-id" # 画像保存用DriveフォルダーのID
# レポート期間設定
START_DATE = "2025-04-01"
END_DATE = "2025-05-31"
Gemini API に分析を書かせるためのプロンプト
# ========== プロンプト1: 総合サマリー ==========
summary_prompt_text = f"""
あなたは経験豊富なデジタルマーケティングアナリストです。以下のWebサイトアクセス解析データに基づき、{report_year_month_str}のパフォーマンスについて、**経営陣および実務担当者の両方が状況を正確に把握できる総合サマリー**を250字程度で作成してください。
(中略)
### 求める分析内容
1. **パフォーマンス要約**: 前年同期比の数値変化を明確に示す
2. **成果の背景要因**: 数値変化の主要な要因を推測・分析
3. **ビジネスへの影響**: 数値変化がビジネスに与える意味を評価
4. **総合判断**: 現状を一言で表現する評価コメント
### 出力形式
- 経営陣向けの要点を最初に記載
- 具体的な数値を交えた客観的な表現
- ネガティブな要素も含めたバランスの取れた評価
- 「認知拡大」「顧客獲得」「エンゲージメント」の観点から分析
"""
# ========== プロンプト2: GA4ハイライト ==========
highlight_prompt_text = f"""
あなたは上級Webアナリストです。以下のGA4データに基づき、{report_year_month_str}の**特に注目すべきハイライトポイント**を、実務担当者が次のアクションを検討できるレベルで200字程度で記述してください。
### 分析データ
- GA4セッション概要: {ga4_session_summary_for_ai}
- 主要LPトップ5: {ga4_lp_summary_for_ai}
- サイト全体平均エンゲージメント率: {avg_engagement_rate_current_str}
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得状況を中心に分析
- (not set)セッションとトップページは除外して評価
- 新規ユーザー獲得とエンゲージメント品質の両面から評価
### 求める内容
1. **最も注目すべき成果**: 数値で裏付けられた具体的な成果
2. **成功要因の推測**: なぜその成果が生まれたかの仮説
3. **ユーザー行動の特徴**: データから読み取れるユーザーの行動パターン
4. **改善機会の示唆**: データが示す次の施策のヒント
### 出力要件
- 具体的な数値を必ず含める
- 「なぜそうなったか」の推測を含める
- 実務者が具体的なアクションをイメージできる表現
"""
# ========== プロンプト3: ユーザー行動考察 ==========
insight_prompt_text = f"""
あなたはUXアナリティクスの専門家です。以下のGA4ランディングページデータとサイト全体指標を参考に、**ユーザー行動とコンテンツパフォーマンスに関する深い考察**を250字程度で記述してください。
### 分析データ
- 主要LPトップ5詳細: {ga4_lp_summary_for_ai}
- サイト全体の平均エンゲージメント率: {avg_engagement_rate_current_str}
- 対象期間: {report_period_str}
### 分析観点
- 見込み度の高いユーザーとの接点・認知獲得状況を中心
- エンゲージメント率の差から見るコンテンツ品質の違い
- ユーザーニーズとコンテンツのマッチング度合い
### 求める考察内容
1. **高パフォーマンスコンテンツの成功要因**: エンゲージメント率が高いページの特徴と理由
2. **コンテンツ格差の要因分析**: ページ間のパフォーマンス差が生まれる構造的要因
3. **ユーザーニーズの洞察**: データから推測できるユーザーの求めているもの
4. **改善の方向性**: データが示すコンテンツ戦略の改善ポイント
### 出力要件
- 表面的な数値比較にとどまらない構造的な分析
- ユーザー視点とビジネス視点の両方を含む
- 具体的な改善施策の方向性を示唆
- 推測部分は「~と考えられる」等で明示
"""
# ========== プロンプト4: GSC検索状況分析 ==========
gsc_prompt_text = f"""
あなたはSEO戦略アナリストです。以下のGoogle Search Consoleデータに基づき、{report_year_month_str}の**検索エンジンからの集客状況と検索ニーズの変化**について250字程度で分析してください。
### 分析データ
- GSC概要: {gsc_summary_for_ai}
- 主要検索クエリトップ5: {gsc_query_summary_for_ai}
- 対象期間: {report_period_str}
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得を中心に評価
- CTR・表示回数・クリック数の相関関係から市場動向を推測
- 検索クエリの特徴から ユーザーニーズの変化を分析
### 求める分析内容
1. **検索流入の量的変化**: クリック・表示回数の前年比分析と要因推測
2. **検索品質の評価**: CTRから見る検索結果の魅力度とユーザーマッチング
3. **検索ニーズの特徴**: 主要クエリから読み取れるユーザーの課題・関心
4. **競合環境の推測**: 表示回数とCTRから推測される競合状況
5. **SEO機会の発見**: データが示す次の SEO施策の方向性
### 出力要件
- 単なる数値報告ではなく、背景要因の推測を含む
- 検索市場での自社ポジションの評価
- 具体的な検索キーワードの特徴を分析
- 今後のSEO戦略への示唆を含む
"""
(中略)
# 全ての分析コメント生成完了後にアクション提案を実行
action_prompt_data = f"""
あなたは経験豊富なデジタルマーケティング戦略コンサルタントです。以下の各分析結果を総合的に評価し、**来月以降に取り組むべき具体的で実行可能なネクストアクション**を3~4点、約250字で提案してください。
(中略)
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得状況の改善
- 短期的成果(1-3ヶ月)と中期的価値(3-6ヶ月)の両面を考慮
- リソース効率と成果インパクトのバランス
### 求める提案内容
各アクションについて以下を明記:
1. **具体的な施策内容**: 何をするかを明確に
2. **期待される効果**: 定量的な目標値(可能な範囲で)
3. **優先順位の理由**: なぜその施策が重要か
4. **実行時期の目安**: いつまでに実施すべきか
5. **必要リソース**: 実行に必要な工数・担当者
### アクション分類
- **最優先(1ヶ月以内)**: 即効性が期待できる施策
- **重要(2-3ヶ月以内)**: 中期的な成果を狙う施策
- **継続実施**: 長期的な基盤強化施策
### 出力要件
- 抽象的な提案ではなく、実際に実行可能な具体的施策
- 期待効果は可能な限り数値目標を含める
- 実務担当者が明日から動けるレベルの具体性
- 「やること」だけでなく「やらないこと」も明記
"""
Pythonコード(全文)
# Colab用:GA4/GSCの前年同月データを含むグラフ・CSV・表画像出力 + Gemini連携GAS生成スクリプト
# 目的:
# 1. Google Analytics 4 (GA4) と Google Search Console (GSC) から指定期間および前年同期のデータを取得します。
# 2. 取得したデータから、日次の傾向グラフ(GA4セッション、GSCクリック・表示回数)を生成します。
# 3. 主要なランディングページ(GA4)と検索クエリ(GSC)のトップ20を表形式の画像として生成します。
# 4. 上記のグラフと表の元データをCSVファイルとして保存します。
# 5. 生成した画像ファイルへのGoogleドライブ上のURLと、CSVデータをまとめたサマリーをGoogleドキュメントに出力します(ログ・確認用)。
# 6. 取得・集計したデータと画像URLを元に、Gemini APIを利用して分析コメント(サマリー、ハイライト、考察、GSC状況、アクション提案)を生成します。
# 7. AIが生成したコメントと画像URLを、あらかじめ定義されたGoogle Apps Script (GAS) の雛形に埋め込み、
# 最終的な月次レポート用Googleスライドを自動生成するためのGASコードを完成させ、.txtファイルとして保存します。
# ========== ライブラリインストール ==========
!pip install --upgrade google-analytics-data google-auth google-api-python-client matplotlib pandas seaborn japanize-matplotlib pillow google-generativeai
from PIL import Image, ImageChops
import os
from google.colab import drive
from google.colab import auth
import datetime
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
from urllib.parse import urlparse, unquote
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
from googleapiclient.http import MediaIoBaseUpload
import google.generativeai as genai
import io
import json
import calendar
import time
# ========== Driveマウント(初回のみ必要。再実行時は自動スキップ) ==========
if not os.path.exists("/content/drive/MyDrive"):
print("Google Driveをマウントします...")
drive.mount("/content/drive")
assert os.path.exists("/content/drive/MyDrive"), "[!]Driveマウントが失敗しています"
else:
print("Driveは既にマウント済みです")
# ========== 設定 (重要:必ずご自身の環境に合わせてください) ==========
OUTPUT_DIR = "/content/drive/MyDrive/Report_Output_Automated"
KEY_PATH = "/content/drive/MyDrive/Report_Input/your-service-account.json"
PROPERTY_ID = "your-GA4-property-ID"
SITE_URL = "https://your-site.com/"
GEMINI_API_KEY = 'YOUR_GEMINI_API_KEY'
SCOPES_GSC = ["https://www.googleapis.com/auth/webmasters.readonly"]
# 新規追加:GAS用設定
TEMPLATE_ID = "your-google-slides-template-id" # GoogleスライドテンプレートのID
IMAGE_FOLDER_ID = "your-image-folder-id" # 画像保存用DriveフォルダーのID
# レポート期間設定
START_DATE = "2025-04-01"
END_DATE = "2025-05-31"
try:
start_date_obj = datetime.datetime.strptime(START_DATE, "%Y-%m-%d")
end_date_obj = datetime.datetime.strptime(END_DATE, "%Y-%m-%d")
# 前年同期の基本設定
start_date_yoy_obj = start_date_obj.replace(year=start_date_obj.year - 1)
end_date_yoy_obj = end_date_obj.replace(year=end_date_obj.year - 1)
# 前年同期の日数調整(期間の日数を一致させる)
target_period_days = (end_date_obj - start_date_obj).days + 1
# 前年期間の終了日を調整(日数を一致させる)
adjusted_end_date_yoy = start_date_yoy_obj + datetime.timedelta(days=target_period_days - 1)
# ただし、2月29日問題(うるう年対応)を考慮
try:
# 調整後の日付が有効かテスト
_ = adjusted_end_date_yoy.strftime("%Y-%m-%d")
end_date_yoy_obj = adjusted_end_date_yoy
except:
# 無効な場合は月末調整を適用
last_day_of_yoy_month = calendar.monthrange(end_date_yoy_obj.year, end_date_yoy_obj.month)[1]
if end_date_yoy_obj.day > last_day_of_yoy_month:
end_date_yoy_obj = end_date_yoy_obj.replace(day=last_day_of_yoy_month)
START_DATE_YOY = start_date_yoy_obj.strftime("%Y-%m-%d")
END_DATE_YOY = end_date_yoy_obj.strftime("%Y-%m-%d")
# ファイル名・表示名の生成(月跨ぎ・年跨ぎ対応)
if start_date_obj.month == end_date_obj.month and start_date_obj.year == end_date_obj.year:
# 同一月内の場合
yyyymm = start_date_obj.strftime("%y%m")
report_year_month_str = start_date_obj.strftime("%Y年%m月")
report_year_month_yoy_str = start_date_yoy_obj.strftime("%Y年%m月")
else:
# 月跨ぎ・年跨ぎの場合
if start_date_obj.year == end_date_obj.year:
# 同一年内の月跨ぎ
yyyymm = f"{start_date_obj.strftime('%y%m')}-{end_date_obj.strftime('%m')}"
report_year_month_str = f"{start_date_obj.strftime('%Y年%m月')}〜{end_date_obj.strftime('%m月')}"
else:
# 年跨ぎ
yyyymm = f"{start_date_obj.strftime('%y%m')}-{end_date_obj.strftime('%y%m')}"
report_year_month_str = f"{start_date_obj.strftime('%Y年%m月')}〜{end_date_obj.strftime('%Y年%m月')}"
# 前年期間の表示名も同様に調整
if start_date_yoy_obj.year == end_date_yoy_obj.year:
report_year_month_yoy_str = f"{start_date_yoy_obj.strftime('%Y年%m月')}〜{end_date_yoy_obj.strftime('%m月')}"
else:
report_year_month_yoy_str = f"{start_date_yoy_obj.strftime('%Y年%m月')}〜{end_date_yoy_obj.strftime('%Y年%m月')}"
report_period_str = f"{start_date_obj.strftime('%Y/%m/%d')}〜{end_date_obj.strftime('%Y/%m/%d')}"
# 期間タイプの判定(後のグラフ処理で使用)
is_single_month = (start_date_obj.month == end_date_obj.month and start_date_obj.year == end_date_obj.year)
period_days = target_period_days
print(f"期間設定完了:")
print(f" 対象期間: {report_period_str} ({period_days}日)")
print(f" 前年期間: {START_DATE_YOY} 〜 {END_DATE_YOY}")
print(f" ファイル名: {yyyymm}")
print(f" 期間タイプ: {'単一月' if is_single_month else '月跨ぎ/年跨ぎ'}")
except ValueError:
print("[エラー] START_DATE または END_DATE の形式が正しくありません。YYYY-MM-DD形式で入力してください。")
raise
os.makedirs(OUTPUT_DIR, exist_ok=True)
plt.rcParams['font.family'] = 'IPAexGothic'
# ========== 認証 ==========
print("各種API認証を開始します...")
try:
credentials_gcp = service_account.Credentials.from_service_account_file(KEY_PATH)
client_ga4 = BetaAnalyticsDataClient(credentials=credentials_gcp)
webmasters_service = build("searchconsole", "v1", credentials=credentials_gcp.with_scopes(SCOPES_GSC))
print("GA4/GSC APIのサービスアカウント認証情報読み込み完了。")
except Exception as e:
print(f"[エラー] GA4/GSC API(サービスアカウントキー)認証失敗: {e}\nパス: {KEY_PATH}")
raise
try:
genai.configure(api_key=GEMINI_API_KEY)
model_gemini = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
print("Gemini APIの認証完了。")
except Exception as e:
print(f"[エラー] Gemini APIキー設定またはモデル初期化失敗: {e}")
raise
try:
auth.authenticate_user()
drive_service = build('drive', 'v3')
print("Google Drive APIのユーザー認証完了。")
except Exception as e:
print(f"[エラー] Google Drive APIユーザー認証失敗: {e}")
raise
# ========== Gemini API呼び出し関数 ==========
def generate_text_with_gemini(prompt_text, task_name="分析コメント"):
print(f"\nGeminiに「{task_name}」のプロンプトで問い合わせ中 (先頭500文字): \n---\n{prompt_text[:500]}...\n---")
try:
response = model_gemini.generate_content(prompt_text)
generated_text = response.text
print(f"Geminiからの「{task_name}」の応答あり。")
return generated_text
except Exception as e:
print(f"[エラー] Gemini API「{task_name}」呼び出し失敗: {e}")
return f"Error: AI comment generation for {task_name} failed. {e}"
# ========== 画像の余白削除 ==========
def trim_white_margin(img_path):
if not os.path.exists(img_path):
print(f"[警告] 余白削除対象画像なし: {img_path}")
return
try:
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_cropped = img.crop(bbox)
if img_cropped.size[0] < img.size[0] or img_cropped.size[1] < img.size[1]:
img_cropped.save(img_path)
print(f"画像余白削除: {img_path}")
else:
print(f"余白削除不要(bboxが全体): {img_path}")
else:
print(f"余白発見不可(画像が単色等): {img_path}")
except Exception as e:
print(f"[エラー] 画像余白削除中 ({img_path}): {e}")
# ========== 表用補助関数 (URLからパス部分のみを抽出) ==========
def strip_domain_from_url(url_text):
if not isinstance(url_text, str) or not url_text.startswith(('http://', 'https://')):
return url_text
url_text = url_text.split("?fbclid=")[0].split("&fbclid=")[0]
parsed = urlparse(url_text)
path = parsed.path
if parsed.query:
path += "?" + parsed.query
return path if path else "/"
# ========== GA4 日次セッション取得・グラフ描画 ==========
print(f"\nGA4 日次セッション ({report_year_month_str} vs {report_year_month_yoy_str}) 取得・グラフ作成...")
import matplotlib.dates as mdates
def fetch_ga4_sessions(start_dt, end_dt, prop_id):
request = RunReportRequest(
property=f"properties/{prop_id}",
dimensions=[Dimension(name="date")],
metrics=[Metric(name="sessions")],
date_ranges=[DateRange(start_date=start_dt, end_date=end_dt)],
)
response = client_ga4.run_report(request)
return pd.DataFrame([
{"date_orig": r.dimension_values[0].value,
"date": datetime.datetime.strptime(r.dimension_values[0].value, "%Y%m%d").strftime("%Y-%m-%d"),
"sessions": int(r.metric_values[0].value)}
for r in response.rows
])
ga4_sessions_yoy_png_path = None
ga4_daily_sessions_csv_path = None
ga4_session_summary_for_ai = "GA4セッションデータ取得/処理失敗"
try:
# データを取得
ga4_df_curr = fetch_ga4_sessions(START_DATE, END_DATE, PROPERTY_ID)
ga4_df_py = fetch_ga4_sessions(START_DATE_YOY, END_DATE_YOY, PROPERTY_ID)
if ga4_df_curr.empty and ga4_df_py.empty:
print("[警告] GA4: 対象・前年両期間セッションデータなし")
else:
# 'date'列をdatetimeオブジェクトに変換
ga4_df_curr['date'] = pd.to_datetime(ga4_df_curr['date'])
ga4_df_py['date'] = pd.to_datetime(ga4_df_py['date'])
# 1. 現在期間のデータ準備 (日付の抜けを0で埋める)
full_date_range_curr = pd.date_range(start=start_date_obj, end=end_date_obj)
ga4_df_curr = ga4_df_curr.set_index('date').reindex(full_date_range_curr, fill_value=0).reset_index().rename(columns={'index': 'date'})
ga4_df_curr['sessions'] = ga4_df_curr['sessions'].astype(int)
# 2. 前年期間のデータ準備 (日付を1年ずらして比較可能にし、抜けを0で埋める)
ga4_df_py['comparable_date'] = ga4_df_py['date'] + pd.DateOffset(years=1)
full_date_range_py_comp = pd.date_range(start=start_date_yoy_obj + pd.DateOffset(years=1), end=end_date_yoy_obj + pd.DateOffset(years=1))
ga4_df_py_reindexed = ga4_df_py.set_index('comparable_date').reindex(full_date_range_py_comp, fill_value=0).reset_index().rename(columns={'index': 'comparable_date'})
ga4_df_py_reindexed['sessions'] = ga4_df_py_reindexed['sessions'].astype(int)
# 3. グラフ描画
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(ga4_df_curr['date'], ga4_df_curr['sessions'], label=f"{start_date_obj.year}年", color="blue", linewidth=2, marker='o', markersize=4)
ax.plot(ga4_df_py_reindexed['comparable_date'], ga4_df_py_reindexed['sessions'], label=f"{start_date_yoy_obj.year}年", color="grey", linestyle="dashed", linewidth=1.5, marker='x', markersize=4)
ax.set_title(f"GA4 日次セッション比較 ({report_period_str} vs 前年)", fontsize=14)
ax.set_xlabel("日付", fontsize=12)
ax.set_ylabel("セッション数", fontsize=12)
ax.legend(fontsize=10)
ax.grid(True, linestyle='--', alpha=0.6)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
fig.autofmt_xdate(rotation=45)
plt.tight_layout()
ga4_sessions_yoy_png_path = f"{OUTPUT_DIR}/ga4_sessions_yoy_{yyyymm}.png"
plt.savefig(ga4_sessions_yoy_png_path)
plt.close()
print(f"GA4日次グラフ保存: {ga4_sessions_yoy_png_path}")
# CSV保存ロジック
ga4_df_py_csv = ga4_df_py_reindexed[['comparable_date', 'sessions']].rename(columns={'comparable_date': 'date', 'sessions': f'sessions_{start_date_yoy_obj.year}'})
ga4_df_curr_csv = ga4_df_curr.rename(columns={'sessions': f'sessions_{start_date_obj.year}'})
# 日付をキーにして外部結合
ga4_csv_df = pd.merge(ga4_df_curr_csv, ga4_df_py_csv, on='date', how='outer').sort_values('date')
ga4_csv_df['date'] = ga4_csv_df['date'].dt.strftime('%Y-%m-%d')
ga4_csv_df = ga4_csv_df.fillna(0)
ga4_csv_df.iloc[:, 1:] = ga4_csv_df.iloc[:, 1:].astype(int)
ga4_daily_sessions_csv_path = f"{OUTPUT_DIR}/ga4_daily_sessions_{yyyymm}.csv"
ga4_csv_df.to_csv(ga4_daily_sessions_csv_path, index=False)
print(f"GA4日次CSV保存: {ga4_daily_sessions_csv_path}")
total_curr = ga4_df_curr['sessions'].sum()
total_py = ga4_df_py_reindexed['sessions'].sum()
change_pct = f"{((total_curr - total_py) / total_py * 100):.1f}%" if total_py > 0 else "N/A"
ga4_session_summary_for_ai = (
f"GA4セッション ({report_period_str}): 合計{total_curr} (前年同期は{total_py})。前年同期比 {change_pct}。"
f"関連ファイル: {os.path.basename(ga4_daily_sessions_csv_path or '')}, {os.path.basename(ga4_sessions_yoy_png_path or '')}."
)
print(f"GA4セッションサマリー(AI用): {ga4_session_summary_for_ai}")
except Exception as e:
print(f"[エラー] GA4日次セッション処理: {e}")
# ========== GSC 日次クリック・表示取得・グラフ描画(月跨ぎ対応版) ==========
print(f"\nGSC 日次データ ({report_year_month_str} vs {report_year_month_yoy_str}) 取得・グラフ作成...")
def fetch_gsc_daily_metrics(start_dt, end_dt, site_url_param):
response = webmasters_service.searchanalytics().query(
siteUrl=site_url_param,
body={"startDate": start_dt, "endDate": end_dt, "dimensions": ["date"], "rowLimit": 1000}
).execute()
return pd.DataFrame([
{"date": r["keys"][0], "clicks": r["clicks"], "impressions": r["impressions"]}
for r in response.get("rows", [])
])
gsc_clicks_impressions_yoy_png_path = None
gsc_daily_metrics_csv_path = None
gsc_summary_for_ai = "GSCデータ取得/処理失敗"
try:
gsc_df_curr = fetch_gsc_daily_metrics(START_DATE, END_DATE, SITE_URL)
gsc_df_py = fetch_gsc_daily_metrics(START_DATE_YOY, END_DATE_YOY, SITE_URL)
if gsc_df_curr.empty and gsc_df_py.empty:
print("[警告] GSC: 対象・前年両期間日次データなし")
else:
# 月跨ぎ対応:日付ベースでの処理に変更
if is_single_month:
# 単一月の場合:従来通りの「日」ベース処理
for df_gsc in [gsc_df_curr, gsc_df_py]:
if not df_gsc.empty:
df_gsc['day'] = pd.to_datetime(df_gsc['date']).dt.strftime('%d')
gsc_curr_grouped = gsc_df_curr.groupby('day')[['clicks', 'impressions']].sum().reset_index() if not gsc_df_curr.empty else pd.DataFrame(columns=['day','clicks','impressions'])
gsc_py_grouped = gsc_df_py.groupby('day')[['clicks', 'impressions']].sum().reset_index() if not gsc_df_py.empty else pd.DataFrame(columns=['day','clicks','impressions'])
num_days = calendar.monthrange(start_date_obj.year, start_date_obj.month)[1]
all_days_labels_gsc = [f"{d:02d}" for d in range(1, num_days + 1)]
x_axis_label = "日"
gsc_curr_reidx = gsc_curr_grouped.set_index('day').reindex(all_days_labels_gsc).fillna(0).reset_index().astype({'clicks':int, 'impressions':int})
gsc_py_reidx = gsc_py_grouped.set_index('day').reindex(all_days_labels_gsc).fillna(0).reset_index().astype({'clicks':int, 'impressions':int})
else:
# 月跨ぎ・年跨ぎの場合:日付範囲ベース処理
# 全期間の日付リストを生成
date_range_curr = pd.date_range(start=start_date_obj, end=end_date_obj)
date_range_yoy = pd.date_range(start=start_date_yoy_obj, end=end_date_yoy_obj)
# データフレームに日付列を追加
if not gsc_df_curr.empty:
gsc_df_curr['date_obj'] = pd.to_datetime(gsc_df_curr['date'])
gsc_curr_pivot = gsc_df_curr.set_index('date_obj')[['clicks', 'impressions']].reindex(date_range_curr).fillna(0)
else:
gsc_curr_pivot = pd.DataFrame({'clicks': [0]*len(date_range_curr), 'impressions': [0]*len(date_range_curr)}, index=date_range_curr)
if not gsc_df_py.empty:
gsc_df_py['date_obj'] = pd.to_datetime(gsc_df_py['date'])
gsc_py_pivot = gsc_df_py.set_index('date_obj')[['clicks', 'impressions']].reindex(date_range_yoy).fillna(0)
else:
gsc_py_pivot = pd.DataFrame({'clicks': [0]*len(date_range_yoy), 'impressions': [0]*len(date_range_yoy)}, index=date_range_yoy)
# X軸ラベル生成(期間が長い場合は間引き)
if period_days <= 31:
# 31日以下:全日表示
all_days_labels_gsc = [d.strftime('%m/%d') for d in date_range_curr]
x_tick_indices = range(0, len(all_days_labels_gsc))
elif period_days <= 62:
# 62日以下:2日おき
all_days_labels_gsc = [d.strftime('%m/%d') for d in date_range_curr]
x_tick_indices = range(0, len(all_days_labels_gsc), 2)
else:
# 62日超:週単位
all_days_labels_gsc = [d.strftime('%m/%d') for d in date_range_curr]
x_tick_indices = range(0, len(all_days_labels_gsc), 7)
x_axis_label = "日付"
# グラフ用データの準備
gsc_curr_reidx = pd.DataFrame({
'day': all_days_labels_gsc,
'clicks': gsc_curr_pivot['clicks'].astype(int).values,
'impressions': gsc_curr_pivot['impressions'].astype(int).values
})
gsc_py_labels = [d.strftime('%m/%d') for d in date_range_yoy]
gsc_py_reidx = pd.DataFrame({
'day': gsc_py_labels,
'clicks': gsc_py_pivot['clicks'].astype(int).values,
'impressions': gsc_py_pivot['impressions'].astype(int).values
})
# グラフ描画(共通処理)
fig, ax1 = plt.subplots(figsize=(12, 5))
cl_c, cl_p = 'blue', 'skyblue'
im_c, im_p = 'red', 'salmon'
ax1.set_xlabel(x_axis_label, fontsize=12)
ax1.set_ylabel("クリック数", color=cl_c, fontsize=12)
ax1.plot(range(len(gsc_curr_reidx)), gsc_curr_reidx['clicks'], color=cl_c, label=f'Clicks {start_date_obj.year}', marker='o', ms=4)
ax1.plot(range(len(gsc_py_reidx)), gsc_py_reidx['clicks'], color=cl_p, linestyle='dashed', label=f'Clicks {start_date_yoy_obj.year}', marker='x', ms=4)
ax1.tick_params(axis='y', labelcolor=cl_c)
ax1.grid(True, linestyle='--', alpha=0.6, axis='y')
ax2 = ax1.twinx()
ax2.set_ylabel("表示回数", color=im_c, fontsize=12)
ax2.plot(range(len(gsc_curr_reidx)), gsc_curr_reidx['impressions'], color=im_c, label=f'Impressions {start_date_obj.year}', marker='s', ms=4)
ax2.plot(range(len(gsc_py_reidx)), gsc_py_reidx['impressions'], color=im_p, linestyle='dashed', label=f'Impressions {start_date_yoy_obj.year}', marker='^', ms=4)
ax2.tick_params(axis='y', labelcolor=im_c)
lns1, labs1 = ax1.get_legend_handles_labels()
lns2, labs2 = ax2.get_legend_handles_labels()
ax2.legend(lns1 + lns2, labs1 + labs2, loc='upper left', fontsize=10)
fig.suptitle(f"GSC 日次クリック・表示回数比較 ({report_year_month_str} vs {report_year_month_yoy_str})", fontsize=14)
# X軸ラベル設定(期間に応じて調整)
if is_single_month:
plt.xticks(range(0, len(all_days_labels_gsc), 2), [all_days_labels_gsc[i] for i in range(0, len(all_days_labels_gsc), 2)], rotation=45, ha="right")
else:
plt.xticks(x_tick_indices, [all_days_labels_gsc[i] for i in x_tick_indices], rotation=45, ha="right")
fig.tight_layout(rect=[0, 0.05, 1, 0.95])
gsc_clicks_impressions_yoy_png_path = f"{OUTPUT_DIR}/gsc_clicks_impressions_yoy_{yyyymm}.png"
plt.savefig(gsc_clicks_impressions_yoy_png_path)
plt.close()
print(f"GSC日次グラフ保存: {gsc_clicks_impressions_yoy_png_path}")
# CSV保存(月跨ぎ対応)
if is_single_month:
# 単一月の場合:従来形式
gsc_csv_df = pd.DataFrame({
'day_of_month': all_days_labels_gsc,
f'clicks_{start_date_obj.year}': gsc_curr_reidx.set_index('day')['clicks'].values,
f'impressions_{start_date_obj.year}': gsc_curr_reidx.set_index('day')['impressions'].values,
f'clicks_{start_date_yoy_obj.year}': gsc_py_reidx.set_index('day')['clicks'].values,
f'impressions_{start_date_yoy_obj.year}': gsc_py_reidx.set_index('day')['impressions'].values
})
else:
# 月跨ぎの場合:日付形式
gsc_csv_df = pd.DataFrame({
'date': all_days_labels_gsc,
f'clicks_{start_date_obj.year}': gsc_curr_reidx['clicks'].values,
f'impressions_{start_date_obj.year}': gsc_curr_reidx['impressions'].values,
f'clicks_{start_date_yoy_obj.year}': gsc_py_reidx['clicks'].values,
f'impressions_{start_date_yoy_obj.year}': gsc_py_reidx['impressions'].values
})
gsc_daily_metrics_csv_path = f"{OUTPUT_DIR}/gsc_daily_metrics_{yyyymm}.csv"
gsc_csv_df.to_csv(gsc_daily_metrics_csv_path, index=False)
print(f"GSC日次CSV保存: {gsc_daily_metrics_csv_path}")
tc_curr = gsc_curr_reidx['clicks'].sum()
ti_curr = gsc_curr_reidx['impressions'].sum()
tc_py = gsc_py_reidx['clicks'].sum()
ti_py = gsc_py_reidx['impressions'].sum()
ch_cl = f"{((tc_curr - tc_py) / tc_py * 100):.1f}%" if tc_py > 0 else "N/A"
ch_im = f"{((ti_curr - ti_py) / ti_py * 100):.1f}%" if ti_py > 0 else "N/A"
ctr_curr = f"{(tc_curr / ti_curr * 100):.2f}%" if ti_curr > 0 else "0.00%"
gsc_summary_for_ai = (
f"GSCデータ ({report_year_month_str}): 総クリック数 {tc_curr} (前年{report_year_month_yoy_str}は{tc_py}、前年比 {ch_cl})。"
f"総表示回数 {ti_curr} (前年{report_year_month_yoy_str}は{ti_py}、前年比 {ch_im})。平均CTR {ctr_curr}。"
f"関連ファイル: {os.path.basename(gsc_daily_metrics_csv_path or '')}, {os.path.basename(gsc_clicks_impressions_yoy_png_path or '')}."
)
print(f"GSCサマリー(AI用): {gsc_summary_for_ai}")
except Exception as e:
print(f"[エラー] GSC日次処理: {e}")
# ========== 表画像:GA4 LPテーブル ==========
print(f"\nGA4ランディングページ ({report_year_month_str}) 取得・表画像作成...")
def fetch_ga4_lp_table(start_dt, end_dt, prop_id):
request = RunReportRequest(
property=f"properties/{prop_id}",
dimensions=[Dimension(name="landingPagePlusQueryString")],
metrics=[Metric(name="sessions"), Metric(name="activeUsers"), Metric(name="newUsers"),
Metric(name="engagementRate"), Metric(name="conversions"), Metric(name="averageSessionDuration")],
date_ranges=[DateRange(start_date=start_dt, end_date=end_dt)],
order_bys=[{"metric": {"metric_name": "sessions"}, "desc": True}],
limit=20
)
response = client_ga4.run_report(request)
rows = [[
i + 1, strip_domain_from_url(r.dimension_values[0].value), int(r.metric_values[0].value),
int(r.metric_values[1].value), int(r.metric_values[2].value), f"{float(r.metric_values[3].value) * 100:.1f}%",
int(r.metric_values[4].value), f"{float(r.metric_values[5].value):.1f}s"
] for i, r in enumerate(response.rows)]
return pd.DataFrame(rows, columns=["#", "LP", "セッション", "アクティブユーザー", "新規ユーザー", "エンゲージメント率", "CV", "平均滞在"])
ga4_lp_table_png_path = None
ga4_lp_table_csv_path = None
ga4_lp_summary_for_ai = "GA4 LPデータ取得/処理失敗"
avg_engagement_rate_current_str = "N/A"
try:
ga4_lp_df = fetch_ga4_lp_table(START_DATE, END_DATE, PROPERTY_ID)
if ga4_lp_df.empty:
print("[警告] GA4 LPデータなし")
else:
h = 0.5 + len(ga4_lp_df) * 0.4
fig, ax = plt.subplots(figsize=(12, h if h > 2 else 2))
ax.axis('off')
tbl = ax.table(cellText=ga4_lp_df.values, colLabels=ga4_lp_df.columns, cellLoc='left', loc='center',
colWidths=[0.03, 0.35, 0.08, 0.1, 0.08, 0.12, 0.05, 0.09])
tbl.auto_set_font_size(False)
tbl.set_fontsize(8.5)
tbl.scale(1, 1.2)
for (r, c), cell in tbl.get_celld().items():
if r == 0:
cell.set_facecolor('#E0E0E0')
cell.set_text_props(weight='bold', ha='center')
elif c == 1:
cell.set_text_props(ha='left')
else:
cell.set_text_props(ha='right')
plt.tight_layout(pad=0.1)
ga4_lp_table_png_path = f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.png"
plt.savefig(ga4_lp_table_png_path, bbox_inches='tight', pad_inches=0.05)
plt.close()
trim_white_margin(ga4_lp_table_png_path)
print(f"GA4 LP表画像保存: {ga4_lp_table_png_path}")
ga4_lp_table_csv_path = f"{OUTPUT_DIR}/ga4_lp_table_{yyyymm}.csv"
ga4_lp_df.to_csv(ga4_lp_table_csv_path, index=False)
print(f"GA4 LP表CSV保存: {ga4_lp_table_csv_path}")
top_lps_ai_df = ga4_lp_df.head(5)[['LP', 'セッション', 'エンゲージメント率', 'CV', '平均滞在']]
ga4_lp_summary_for_ai = (
f"GA4主要LP ({report_year_month_str}) トップ5:\n{top_lps_ai_df.to_string(index=False)}\n"
f"全LPデータ(上位20件)は {os.path.basename(ga4_lp_table_csv_path or '')}, {os.path.basename(ga4_lp_table_png_path or '')}参照。"
)
# engagement rate の文字列から '%' と 's' を取り除いて数値に変換し、平均を計算
avg_engagement_rate_current_str = f"{ga4_lp_df['エンゲージメント率'].str.replace('%', '').astype(float).mean():.1f}%"
print(f"GA4 LPサマリー(AI用): {ga4_lp_summary_for_ai[:300]}...")
except Exception as e:
print(f"[エラー] GA4 LPテーブル処理: {e}")
# ========== 表画像:GSC クエリテーブル ==========
print(f"\nGSCクエリデータ ({report_year_month_str}) 取得・表画像作成...")
def fetch_gsc_query_table(start_dt, end_dt, site_url_param):
response = webmasters_service.searchanalytics().query(
siteUrl=site_url_param,
body={"startDate": start_dt, "endDate": end_dt, "dimensions": ["page", "query"], "rowLimit": 1000}
).execute()
print(f"GSC生データ取得(page, query): {len(response.get('rows', []))}件")
data = []
if "rows" in response:
for r_dict in response["rows"]:
keys = r_dict.get("keys", [])
if len(keys) < 2:
continue
data.append({
"page_raw": keys[0] or "/",
"query": keys[1] or "(not set)",
"clicks": int(r_dict["clicks"]),
"impressions": int(r_dict["impressions"]),
"ctr": float(r_dict["ctr"]) * 100,
"position": float(r_dict["position"])
})
if not data:
print("[警告] GSCクエリデータ(page, query)なし")
return pd.DataFrame(columns=["#", "LP", "検索クエリ", "クリック", "表示", "順位", "CTR"])
df_tmp = pd.DataFrame(data)
df_grouped = df_tmp.groupby(['page_raw', 'query']).agg(
clicks=('clicks', 'sum'),
impressions=('impressions', 'sum'),
position=('position', 'mean')
).reset_index()
df_grouped['ctr'] = (df_grouped['clicks'] / df_grouped['impressions'] * 100).where(df_grouped['impressions'] > 0, 0)
df_sorted = df_grouped.sort_values(by=["clicks", "impressions"], ascending=[False, False]).head(20).reset_index(drop=True)
rows_proc = []
for i, r_data in df_sorted.iterrows():
lp_proc = strip_domain_from_url(unquote(r_data["page_raw"]))
lp_disp = lp_proc[:30] + "..." if len(lp_proc) > 30 else lp_proc
q_disp = r_data["query"]
q_disp = q_disp[:25] + "..." if len(q_disp) > 25 else q_disp
rows_proc.append([
i + 1, lp_disp, q_disp,
r_data["clicks"], r_data["impressions"], f"{r_data['position']:.1f}", f"{r_data['ctr']:.2f}%"
])
return pd.DataFrame(rows_proc, columns=["#", "LP", "検索クエリ", "クリック", "表示", "順位", "CTR"])
gsc_query_table_png_path = None
gsc_query_table_csv_path = None
gsc_query_summary_for_ai = "GSCクエリデータ取得/処理失敗"
try:
gsc_q_df = fetch_gsc_query_table(START_DATE, END_DATE, SITE_URL)
if gsc_q_df.empty:
print("[警告] GSCクエリデータなし")
else:
h_gsc = 0.5 + len(gsc_q_df) * 0.4
fig, ax = plt.subplots(figsize=(12, h_gsc if h_gsc > 2 else 2))
ax.axis('off')
tbl_gsc = ax.table(cellText=gsc_q_df.values, colLabels=gsc_q_df.columns, cellLoc='left', loc='center',
colWidths=[0.03, 0.25, 0.25, 0.07, 0.07, 0.07, 0.07])
tbl_gsc.auto_set_font_size(False)
tbl_gsc.set_fontsize(8)
tbl_gsc.scale(1, 1.2)
for (r, c), cell in tbl_gsc.get_celld().items():
if r == 0:
cell.set_facecolor('#E0E0E0')
cell.set_text_props(weight='bold', ha='center')
elif c in [1, 2]:
cell.set_text_props(ha='left')
else:
cell.set_text_props(ha='right')
plt.tight_layout(pad=0.1)
gsc_query_table_png_path = f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.png"
plt.savefig(gsc_query_table_png_path, bbox_inches='tight', pad_inches=0.05)
plt.close()
trim_white_margin(gsc_query_table_png_path)
print(f"GSCクエリ表画像保存: {gsc_query_table_png_path}")
gsc_query_table_csv_path = f"{OUTPUT_DIR}/gsc_query_table_{yyyymm}.csv"
gsc_q_df.to_csv(gsc_query_table_csv_path, index=False)
print(f"GSCクエリ表CSV保存: {gsc_query_table_csv_path}")
top_q_ai_df = gsc_q_df.head(5)[['検索クエリ', 'クリック', '表示', 'CTR', 'LP']]
gsc_query_summary_for_ai = (
f"GSC主要検索クエリ ({report_year_month_str}) トップ5:\n{top_q_ai_df.to_string(index=False)}\n"
f"全クエリデータ(上位20件)は {os.path.basename(gsc_query_table_csv_path or '')}, {os.path.basename(gsc_query_table_png_path or '')}参照。"
)
print(f"GSCクエリサマリー(AI用): {gsc_query_summary_for_ai[:300]}...")
except Exception as e:
print(f"[エラー] GSCクエリテーブル処理: {e}")
# ========== 画像URLとCSVデータのGoogle Docs出力 (ログ・確認用) ==========
print("\nGoogleドキュメントへのログ出力を開始...")
def get_gdrive_folder_id(local_base_output_dir, service):
if not local_base_output_dir.startswith("/content/drive/MyDrive/"):
print(f"[警告] DriveパスがMyDrive配下ではありません: {local_base_output_dir}。ルートを使用します。")
return 'root'
relative_path = os.path.normpath(local_base_output_dir.replace("/content/drive/MyDrive/", ""))
parts = relative_path.split(os.sep)
current_id = 'root'
for part in parts:
if not part:
continue
query = f"name = '{part}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false and '{current_id}' in parents"
response = service.files().list(q=query, fields="files(id)").execute()
found_folders = response.get('files', [])
if found_folders:
current_id = found_folders[0]['id']
else:
print(f"Driveにフォルダ「{part}」(親ID: {current_id})を作成します。")
folder_metadata = {'name': part, 'mimeType': 'application/vnd.google-apps.folder', 'parents': [current_id]}
try:
folder = service.files().create(body=folder_metadata, fields='id').execute()
current_id = folder.get('id')
print(f"フォルダ「{part}」作成成功。ID: {current_id}")
except Exception as e_create:
print(f"[エラー] Driveフォルダ「{part}」作成失敗: {e_create}。ルートを使用します。")
return 'root'
return current_id
gdrive_target_folder_id = get_gdrive_folder_id(OUTPUT_DIR, drive_service)
print(f"Google Drive出力先フォルダID: {gdrive_target_folder_id}")
def create_doc_in_gdrive(title, content_str, folder_id_on_drive, service):
try:
file_meta = {'name': title, 'mimeType': 'application/vnd.google-apps.document'}
if folder_id_on_drive and folder_id_on_drive != 'root':
file_meta['parents'] = [folder_id_on_drive]
media = MediaIoBaseUpload(io.BytesIO(content_str.encode('utf-8')), mimetype='text/plain', resumable=True)
gfile = service.files().create(body=file_meta, media_body=media, fields='id,webViewLink').execute()
service.permissions().create(fileId=gfile['id'], body={'type': 'anyone', 'role': 'reader'}).execute()
print(f"Gドキュメント「{title}」作成・共有: {gfile['webViewLink']}")
return gfile['id'], gfile['webViewLink']
except Exception as e:
print(f"[エラー] Gドキュメント「{title}」作成失敗: {e}")
return None, None
def get_and_log_image_drive_urls(doc_title, local_img_paths, target_folder_id_on_drive, service):
url_map = {}
doc_content = f"{doc_title} - 画像URL ({report_year_month_str})\n\n"
if not local_img_paths:
print("[情報] 画像リスト空。画像URL Doc作成スキップ。")
return url_map, None
# Drive同期待ち
print("Drive同期待ち(5秒)...")
time.sleep(5)
for img_path_local in local_img_paths:
if img_path_local is None or not os.path.exists(img_path_local):
print(f"[警告] ローカル画像なし: {img_path_local}")
continue
img_filename = os.path.basename(img_path_local)
print(f"[デバッグ] 検索対象ファイル名: {img_filename}")
print(f"[デバッグ] 検索対象フォルダーID: {target_folder_id_on_drive}")
try:
query = f"name='{img_filename}' and trashed=false"
if target_folder_id_on_drive and target_folder_id_on_drive != 'root':
query += f" and '{target_folder_id_on_drive}' in parents"
print(f"[デバッグ] Drive検索クエリ: {query}")
response = service.files().list(q=query, fields="files(id, webViewLink)").execute()
if response and response.get('files'):
file_id = response.get('files')[0]['id']
view_url = f"https://drive.google.com/uc?export=view&id={file_id}"
url_map[img_filename] = view_url
doc_content += f"{img_filename}: {view_url}\n"
print(f"○ 画像URL取得成功: {img_filename} -> {view_url}")
else:
# 追加の検索パターン
print(f"[警告] 通常検索で見つからず。全フォルダ検索を試行...")
query_wide = f"name='{img_filename}' and trashed=false"
response_wide = service.files().list(q=query_wide, fields="files(id, webViewLink, parents)").execute()
if response_wide and response_wide.get('files'):
file_id = response_wide.get('files')[0]['id']
view_url = f"https://drive.google.com/uc?export=view&id={file_id}"
url_map[img_filename] = view_url
doc_content += f"{img_filename}: {view_url} (全フォルダ検索で発見)\n"
print(f"○ 画像URL取得成功(全フォルダ検索): {img_filename} -> {view_url}")
else:
doc_content += f"{img_filename}: URL取得失敗 (Drive上で見つからない)\n"
print(f"× 画像未発見: {img_filename}")
url_map[img_filename] = f"ERROR_URL_NOT_FOUND_FOR_{img_filename}"
except Exception as e:
print(f"[エラー] 画像「{img_filename}」Drive URL取得中: {e}")
doc_content += f"{img_filename}: URL取得中エラー\n"
url_map[img_filename] = f"ERROR_DURING_URL_FETCH_FOR_{img_filename}"
doc_content += "\n"
_, doc_url_link = create_doc_in_gdrive(doc_title, doc_content, target_folder_id_on_drive, service)
return url_map, doc_url_link
def log_all_csv_data_to_doc(doc_title, local_csv_paths_map, target_folder_id_on_drive, service):
content_all_csv = f"データサマリー: {report_year_month_str} ({report_period_str})\n\n"
for name, path_local in local_csv_paths_map.items():
if path_local is None or not os.path.exists(path_local):
content_all_csv += f"--- {name} ---\nデータなし (ファイルなし: {path_local})\n\n"
print(f"[警告] CSVファイルなし: {path_local}")
continue
try:
with open(path_local, 'r', encoding='utf-8') as f:
data_csv = f.read()
content_all_csv += f"--- {name} ---\n{data_csv}\n\n"
except Exception as e:
print(f"[エラー] CSV「{path_local}」読込失敗: {e}")
content_all_csv += f"--- {name} ---\n[エラー]読込失敗\n\n"
_, doc_url_link = create_doc_in_gdrive(doc_title, content_all_csv, target_folder_id_on_drive, service)
return doc_url_link
image_paths_map = {
"{{GA4_SESSIONS_IMAGE_URL_PLACEHOLDER}}": ga4_sessions_yoy_png_path,
"{{GSC_CLICKS_IMAGE_URL_PLACEHOLDER}}": gsc_clicks_impressions_yoy_png_path,
"{{GA4_LP_TABLE_IMAGE_URL_PLACEHOLDER}}": ga4_lp_table_png_path,
"{{GSC_QUERY_TABLE_IMAGE_URL_PLACEHOLDER}}": gsc_query_table_png_path
}
valid_local_image_paths = [p for p in image_paths_map.values() if p is not None and os.path.exists(p)]
image_urls_for_gas_template = {}
temp_image_urls_map_from_drive, image_url_doc_link = get_and_log_image_drive_urls(
f"画像URL一覧_{yyyymm}",
valid_local_image_paths,
gdrive_target_folder_id,
drive_service
)
for gas_placeholder_key, local_path_val in image_paths_map.items():
if local_path_val and os.path.exists(local_path_val):
filename_key = os.path.basename(local_path_val)
image_urls_for_gas_template[gas_placeholder_key] = temp_image_urls_map_from_drive.get(filename_key, f"ERROR_URL_FOR_{filename_key}")
else:
image_urls_for_gas_template[gas_placeholder_key] = "ERROR_SOURCE_IMAGE_PATH_INVALID"
print(f"GASテンプレート用画像URLマップ: {image_urls_for_gas_template}")
if image_url_doc_link:
print(f"画像URL一覧Doc: {image_url_doc_link}")
csv_paths_map_for_doc = {
"GA4 日次セッション": ga4_daily_sessions_csv_path,
"GSC 日次指標": gsc_daily_metrics_csv_path,
"GA4 LP表": ga4_lp_table_csv_path,
"GSC クエリ表": gsc_query_table_csv_path,
}
all_data_doc_link = log_all_csv_data_to_doc(
f"全CSVデータ_{yyyymm}", csv_paths_map_for_doc, gdrive_target_folder_id, drive_service
)
if all_data_doc_link:
print(f"全CSVデータDoc: {all_data_doc_link}")
# ========== 強化されたAIプロンプトによる分析コメント生成 ==========
print("\n強化されたAIプロンプトによる分析コメント生成を開始...")
ai_generated_comments = {}
# ========== プロンプト1: 総合サマリー ==========
summary_prompt_text = f"""
あなたは経験豊富なデジタルマーケティングアナリストです。以下のWebサイトアクセス解析データに基づき、{report_year_month_str}のパフォーマンスについて、**経営陣および実務担当者の両方が状況を正確に把握できる総合サマリー**を250字程度で作成してください。
### 分析データ
- GA4概要: {ga4_session_summary_for_ai}
- GSC概要: {gsc_summary_for_ai}
- 主要LP: {ga4_lp_summary_for_ai}
- 主要検索クエリ: {gsc_query_summary_for_ai}
### 求める分析内容
1. **パフォーマンス要約**: 前年同期比の数値変化を明確に示す
2. **成果の背景要因**: 数値変化の主要な要因を推測・分析
3. **ビジネスへの影響**: 数値変化がビジネスに与える意味を評価
4. **総合判断**: 現状を一言で表現する評価コメント
### 出力形式
- 経営陣向けの要点を最初に記載
- 具体的な数値を交えた客観的な表現
- ネガティブな要素も含めたバランスの取れた評価
- 「認知拡大」「顧客獲得」「エンゲージメント」の観点から分析
"""
# ========== プロンプト2: GA4ハイライト ==========
highlight_prompt_text = f"""
あなたは上級Webアナリストです。以下のGA4データに基づき、{report_year_month_str}の**特に注目すべきハイライトポイント**を、実務担当者が次のアクションを検討できるレベルで200字程度で記述してください。
### 分析データ
- GA4セッション概要: {ga4_session_summary_for_ai}
- 主要LPトップ5: {ga4_lp_summary_for_ai}
- サイト全体平均エンゲージメント率: {avg_engagement_rate_current_str}
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得状況を中心に分析
- (not set)セッションとトップページは除外して評価
- 新規ユーザー獲得とエンゲージメント品質の両面から評価
### 求める内容
1. **最も注目すべき成果**: 数値で裏付けられた具体的な成果
2. **成功要因の推測**: なぜその成果が生まれたかの仮説
3. **ユーザー行動の特徴**: データから読み取れるユーザーの行動パターン
4. **改善機会の示唆**: データが示す次の施策のヒント
### 出力要件
- 具体的な数値を必ず含める
- 「なぜそうなったか」の推測を含める
- 実務者が具体的なアクションをイメージできる表現
"""
# ========== プロンプト3: ユーザー行動考察 ==========
insight_prompt_text = f"""
あなたはUXアナリティクスの専門家です。以下のGA4ランディングページデータとサイト全体指標を参考に、**ユーザー行動とコンテンツパフォーマンスに関する深い考察**を250字程度で記述してください。
### 分析データ
- 主要LPトップ5詳細: {ga4_lp_summary_for_ai}
- サイト全体の平均エンゲージメント率: {avg_engagement_rate_current_str}
- 対象期間: {report_period_str}
### 分析観点
- 見込み度の高いユーザーとの接点・認知獲得状況を中心
- エンゲージメント率の差から見るコンテンツ品質の違い
- ユーザーニーズとコンテンツのマッチング度合い
### 求める考察内容
1. **高パフォーマンスコンテンツの成功要因**: エンゲージメント率が高いページの特徴と理由
2. **コンテンツ格差の要因分析**: ページ間のパフォーマンス差が生まれる構造的要因
3. **ユーザーニーズの洞察**: データから推測できるユーザーの求めているもの
4. **改善の方向性**: データが示すコンテンツ戦略の改善ポイント
### 出力要件
- 表面的な数値比較にとどまらない構造的な分析
- ユーザー視点とビジネス視点の両方を含む
- 具体的な改善施策の方向性を示唆
- 推測部分は「~と考えられる」等で明示
"""
# ========== プロンプト4: GSC検索状況分析 ==========
gsc_prompt_text = f"""
あなたはSEO戦略アナリストです。以下のGoogle Search Consoleデータに基づき、{report_year_month_str}の**検索エンジンからの集客状況と検索ニーズの変化**について250字程度で分析してください。
### 分析データ
- GSC概要: {gsc_summary_for_ai}
- 主要検索クエリトップ5: {gsc_query_summary_for_ai}
- 対象期間: {report_period_str}
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得を中心に評価
- CTR・表示回数・クリック数の相関関係から市場動向を推測
- 検索クエリの特徴から ユーザーニーズの変化を分析
### 求める分析内容
1. **検索流入の量的変化**: クリック・表示回数の前年比分析と要因推測
2. **検索品質の評価**: CTRから見る検索結果の魅力度とユーザーマッチング
3. **検索ニーズの特徴**: 主要クエリから読み取れるユーザーの課題・関心
4. **競合環境の推測**: 表示回数とCTRから推測される競合状況
5. **SEO機会の発見**: データが示す次の SEO施策の方向性
### 出力要件
- 単なる数値報告ではなく、背景要因の推測を含む
- 検索市場での自社ポジションの評価
- 具体的な検索キーワードの特徴を分析
- 今後のSEO戦略への示唆を含む
"""
# AIによる分析コメント生成実行
ai_generated_comments['{{summary}}'] = generate_text_with_gemini(summary_prompt_text, "総合サマリー")
ai_generated_comments['{{highlight}}'] = generate_text_with_gemini(highlight_prompt_text, "GA4ハイライト")
ai_generated_comments['{{insight}}'] = generate_text_with_gemini(insight_prompt_text, "ユーザー行動考察")
ai_generated_comments['{{gsc}}'] = generate_text_with_gemini(gsc_prompt_text, "GSC検索状況")
# 全ての分析コメント生成完了後にアクション提案を実行
action_prompt_data = f"""
あなたは経験豊富なデジタルマーケティング戦略コンサルタントです。以下の各分析結果を総合的に評価し、**来月以降に取り組むべき具体的で実行可能なネクストアクション**を3~4点、約250字で提案してください。
### 各分析結果
- 総合サマリー: {ai_generated_comments.get('{{summary}}', '(サマリー生成エラー)')}
- GA4ハイライト: {ai_generated_comments.get('{{highlight}}', '(ハイライト生成エラー)')}
- ユーザー行動考察: {ai_generated_comments.get('{{insight}}', '(考察生成エラー)')}
- GSC検索状況: {ai_generated_comments.get('{{gsc}}', '(GSCコメント生成エラー)')}
### 分析の重点
- 見込み度の高いユーザーとの接点・認知獲得状況の改善
- 短期的成果(1-3ヶ月)と中期的価値(3-6ヶ月)の両面を考慮
- リソース効率と成果インパクトのバランス
### 求める提案内容
各アクションについて以下を明記:
1. **具体的な施策内容**: 何をするかを明確に
2. **期待される効果**: 定量的な目標値(可能な範囲で)
3. **優先順位の理由**: なぜその施策が重要か
4. **実行時期の目安**: いつまでに実施すべきか
5. **必要リソース**: 実行に必要な工数・担当者
### アクション分類
- **最優先(1ヶ月以内)**: 即効性が期待できる施策
- **重要(2-3ヶ月以内)**: 中期的な成果を狙う施策
- **継続実施**: 長期的な基盤強化施策
### 出力要件
- 抽象的な提案ではなく、実際に実行可能な具体的施策
- 期待効果は可能な限り数値目標を含める
- 実務担当者が明日から動けるレベルの具体性
- 「やること」だけでなく「やらないこと」も明記
"""
ai_generated_comments['{{action}}'] = generate_text_with_gemini(action_prompt_data, "戦略的アクション提案")
print("\nAIによる全分析コメント:")
for key_ph, gen_val in ai_generated_comments.items():
print(f"--- {key_ph} に挿入するコメント ---\n{gen_val}\n")
# ========== 新形式GASコード生成 ==========
print("新形式GASコードの生成を開始...")
# 新しいGASテンプレート(ボス指定の形式)
gas_template_string = '''let latestPresentationId = ""; // グローバル変数でIDを保持
// 実行関数(全部まとめて呼び出し)
function generateAnalysisReport() {
try {
// 1. 画像フォルダーを公開に設定
makeImageFolderPublic();
// 2. 分析レポート生成処理
createMonthlyReport();
insertImageToSummarySlide();
insertGSCImageToSlide();
insertGA4LandingPageTable();
insertGSCLandingQueryTable();
// 3. 画像フォルダーを非公開に戻す
makeImageFolderPrivate();
Logger.log("分析レポート生成完了!");
Logger.log("スライドURL: https://docs.google.com/presentation/d/" + latestPresentationId + "/edit");
} catch (error) {
// エラー時も必ずフォルダーを非公開に戻す
Logger.log("エラー発生: " + error.toString());
makeImageFolderPrivate();
throw error;
}
}
// 強化版フォルダー公開設定
function makeImageFolderPublic() {
const folderId = "__IMAGE_FOLDER_ID_PLACEHOLDER__";
try {
const folder = DriveApp.getFolderById(folderId);
// フォルダー自体を公開
folder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// フォルダー内の全ファイルを個別に公開
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
Logger.log("画像公開設定完了: " + file.getName());
}
Logger.log("フォルダーと全画像の公開設定完了");
// 設定反映待ち時間を追加
Utilities.sleep(10000); // 10秒待機
} catch (error) {
Logger.log("フォルダー公開設定エラー: " + error.toString());
throw error;
}
}
// フォルダー非公開設定
function makeImageFolderPrivate() {
const folderId = "__IMAGE_FOLDER_ID_PLACEHOLDER__";
try {
const folder = DriveApp.getFolderById(folderId);
// フォルダー内の全ファイルを個別に非公開に戻す
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
file.setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.VIEW);
Logger.log("画像非公開設定完了: " + file.getName());
}
// フォルダー自体を非公開に戻す
folder.setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.VIEW);
Logger.log("フォルダーと全画像を非公開に戻しました");
} catch (error) {
Logger.log("フォルダー非公開設定エラー: " + error.toString());
// フォルダー設定エラーは致命的ではないので処理を継続
}
}
// 1. テンプレートから複製して分析データを差し込み
function createMonthlyReport() {
const templateId = "__TEMPLATE_ID_PLACEHOLDER__";
const newTitle = "__REPORT_TITLE_PLACEHOLDER__";
// 分析結果をJSON形式で埋め込むデータオブジェクト
const claudeAnalysisData = __JSON_DATA_PLACEHOLDER__;
const newPresentationFile = DriveApp.getFileById(templateId).makeCopy(newTitle);
latestPresentationId = newPresentationFile.getId();
const presentation = SlidesApp.openById(latestPresentationId);
const slides = presentation.getSlides();
slides.forEach(slide => {
for (const key_in_data in claudeAnalysisData) {
if (Object.prototype.hasOwnProperty.call(claudeAnalysisData, key_in_data)) {
let replacementText = String(claudeAnalysisData[key_in_data] || "");
slide.replaceAllText(key_in_data, replacementText);
}
}
});
Logger.log("分析スライド生成完了: https://docs.google.com/presentation/d/" + latestPresentationId + "/edit");
return latestPresentationId;
}
// 共通関数:画像を比率維持して挿入
function insertImagePreserveAspect(slide, imageUrl, targetWidth, top, left) {
if (!imageUrl || imageUrl.startsWith("{{") || imageUrl.includes("ERROR")) {
Logger.log("無効な画像URLまたは未置換プレースホルダのため画像挿入スキップ: " + imageUrl);
return;
}
try {
const image = slide.insertImage(imageUrl);
const originalWidth = image.getWidth();
const originalHeight = image.getHeight();
if (originalWidth === 0) {
Logger.log("画像幅0のためサイズ調整スキップ: " + imageUrl);
image.setTop(top).setLeft(left); return;
}
image.setWidth(targetWidth);
image.setHeight(targetWidth * (originalHeight / originalWidth));
image.setTop(top).setLeft(left);
} catch (e) { Logger.log("画像挿入エラー (" + imageUrl + "): " + e.toString()); }
}
// GA4セッション推移グラフ(スライド4)
function insertImageToSummarySlide() {
const imageUrl = "__GA4_SESSIONS_IMAGE_URL_PLACEHOLDER__";
const pres = SlidesApp.openById(latestPresentationId);
if (pres.getSlides().length < 4) { Logger.log("エラー: スライド4なし。GA4セッション画像挿入スキップ。"); return; }
insertImagePreserveAspect(pres.getSlides()[3], imageUrl, 600, 100, 50);
}
// GSCクリック・表示グラフ(スライド8)
function insertGSCImageToSlide() {
const imageUrl = "__GSC_CLICKS_IMAGE_URL_PLACEHOLDER__";
const pres = SlidesApp.openById(latestPresentationId);
if (pres.getSlides().length < 8) { Logger.log("エラー: スライド8なし。GSCグラフ画像挿入スキップ。"); return; }
insertImagePreserveAspect(pres.getSlides()[7], imageUrl, 600, 100, 50);
}
// GA4 LP表(スライド5)
function insertGA4LandingPageTable() {
const imageUrl = "__GA4_LP_TABLE_IMAGE_URL_PLACEHOLDER__";
const pres = SlidesApp.openById(latestPresentationId);
if (pres.getSlides().length < 5) { Logger.log("エラー: スライド5なし。GA4 LP表画像挿入スキップ。"); return; }
insertImagePreserveAspect(pres.getSlides()[4], imageUrl, 550, 80, 75);
}
// GSC クエリ表(スライド9)
function insertGSCLandingQueryTable() {
const imageUrl = "__GSC_QUERY_TABLE_IMAGE_URL_PLACEHOLDER__";
const pres = SlidesApp.openById(latestPresentationId);
if (pres.getSlides().length < 9) { Logger.log("エラー: スライド9なし。GSCクエリ表画像挿入スキップ。"); return; }
insertImagePreserveAspect(pres.getSlides()[8], imageUrl, 550, 80, 75);
}
// ========== 分析実行ログ ==========
// 生成日時: __GENERATION_DATETIME_PLACEHOLDER__
// レポート期間: __REPORT_PERIOD_PLACEHOLDER__
// 生成元データサマリー:
// - GA4セッション数: __GA4_SESSION_TOTAL_PLACEHOLDER__
// - GSCクリック数: __GSC_CLICK_TOTAL_PLACEHOLDER__
// - 生成画像数: __GENERATED_IMAGE_COUNT_PLACEHOLDER__
'''
# レポートタイトル生成
report_title = f"月次レポート_{report_period_str}_Gemini分析版"
# テキスト置換用データ準備
gas_data_values = {
"{{yearmonth}}": report_year_month_str,
"{{period}}": report_period_str,
"{{summary}}": ai_generated_comments.get('{{summary}}', '(サマリー生成エラー)'),
"{{highlight}}": ai_generated_comments.get('{{highlight}}', '(ハイライト生成エラー)'),
"{{insight}}": ai_generated_comments.get('{{insight}}', '(考察生成エラー)'),
"{{gsc}}": ai_generated_comments.get('{{gsc}}', '(GSCコメント生成エラー)'),
"{{action}}": ai_generated_comments.get('{{action}}', '(アクション提案生成エラー)')
}
# JSONデータ文字列作成
gas_data_json_string = json.dumps(gas_data_values, ensure_ascii=False, indent=2)
# GASテンプレートの置換処理
final_gas_code_string = gas_template_string
# 基本設定値の置換
final_gas_code_string = final_gas_code_string.replace("__TEMPLATE_ID_PLACEHOLDER__", TEMPLATE_ID)
final_gas_code_string = final_gas_code_string.replace("__IMAGE_FOLDER_ID_PLACEHOLDER__", IMAGE_FOLDER_ID)
final_gas_code_string = final_gas_code_string.replace("__REPORT_TITLE_PLACEHOLDER__", report_title)
final_gas_code_string = final_gas_code_string.replace("__JSON_DATA_PLACEHOLDER__", gas_data_json_string)
# 画像URLの置換処理(デバッグ強化版)
print("\n=== 画像URL置換処理(デバッグ情報付き)===")
print("置換前の画像URLマップ:")
for key, value in image_urls_for_gas_template.items():
print(f" {key}: {value}")
for img_placeholder_key, img_actual_url in image_urls_for_gas_template.items():
# プレースホルダー形式の変換({{}} → __PLACEHOLDER__)
placeholder_key = img_placeholder_key.replace("{{", "__").replace("}}", "_PLACEHOLDER__")
print(f"\n置換処理:")
print(f" 元のキー: {img_placeholder_key}")
print(f" 変換後キー: {placeholder_key}")
print(f" 置換URL: {img_actual_url}")
# 置換前の確認
if placeholder_key in final_gas_code_string:
print(f" ○ プレースホルダー発見: {placeholder_key}")
final_gas_code_string = final_gas_code_string.replace(placeholder_key, img_actual_url or f"ERROR_URL_FOR_{placeholder_key}")
print(f" ○ 置換完了")
else:
print(f" × プレースホルダー未発見: {placeholder_key}")
print(f" GASコード内を検索中...")
# 部分一致検索
if "GA4_SESSIONS" in placeholder_key and "__GA4_SESSIONS" in final_gas_code_string:
print(f" 部分一致発見: GA4_SESSIONS関連")
elif "GSC_CLICKS" in placeholder_key and "__GSC_CLICKS" in final_gas_code_string:
print(f" 部分一致発見: GSC_CLICKS関連")
elif "GA4_LP_TABLE" in placeholder_key and "__GA4_LP_TABLE" in final_gas_code_string:
print(f" 部分一致発見: GA4_LP_TABLE関連")
elif "GSC_QUERY_TABLE" in placeholder_key and "__GSC_QUERY_TABLE" in final_gas_code_string:
print(f" 部分一致発見: GSC_QUERY_TABLE関連")
print("\n=== 置換後の確認 ===")
# 置換後もプレースホルダーが残っていないかチェック
remaining_placeholders = []
placeholder_patterns = ["__GA4_SESSIONS_IMAGE_URL_PLACEHOLDER__", "__GSC_CLICKS_IMAGE_URL_PLACEHOLDER__",
"__GA4_LP_TABLE_IMAGE_URL_PLACEHOLDER__", "__GSC_QUERY_TABLE_IMAGE_URL_PLACEHOLDER__"]
for pattern in placeholder_patterns:
if pattern in final_gas_code_string:
remaining_placeholders.append(pattern)
if remaining_placeholders:
print("× 未置換のプレースホルダーあり:")
for placeholder in remaining_placeholders:
print(f" - {placeholder}")
# 強制置換(デバッグ用)
print("\n強制置換を実行...")
final_gas_code_string = final_gas_code_string.replace("__GA4_SESSIONS_IMAGE_URL_PLACEHOLDER__",
image_urls_for_gas_template.get("{{GA4_SESSIONS_IMAGE_URL_PLACEHOLDER}}", "ERROR_GA4_SESSIONS_URL"))
final_gas_code_string = final_gas_code_string.replace("__GSC_CLICKS_IMAGE_URL_PLACEHOLDER__",
image_urls_for_gas_template.get("{{GSC_CLICKS_IMAGE_URL_PLACEHOLDER}}", "ERROR_GSC_CLICKS_URL"))
final_gas_code_string = final_gas_code_string.replace("__GA4_LP_TABLE_IMAGE_URL_PLACEHOLDER__",
image_urls_for_gas_template.get("{{GA4_LP_TABLE_IMAGE_URL_PLACEHOLDER}}", "ERROR_GA4_LP_TABLE_URL"))
final_gas_code_string = final_gas_code_string.replace("__GSC_QUERY_TABLE_IMAGE_URL_PLACEHOLDER__",
image_urls_for_gas_template.get("{{GSC_QUERY_TABLE_IMAGE_URL_PLACEHOLDER}}", "ERROR_GSC_QUERY_TABLE_URL"))
print("○ 強制置換完了")
else:
print("○ 全ての画像URLプレースホルダーが正常に置換されました")
# 実行ログ情報の置換
current_datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
final_gas_code_string = final_gas_code_string.replace("__GENERATION_DATETIME_PLACEHOLDER__", current_datetime)
final_gas_code_string = final_gas_code_string.replace("__REPORT_PERIOD_PLACEHOLDER__", report_period_str)
# 統計情報の計算と置換
try:
ga4_total = int(ga4_session_summary_for_ai.split("合計")[1].split(" ")[0]) if "合計" in ga4_session_summary_for_ai else "N/A"
gsc_total = int(gsc_summary_for_ai.split("総クリック数 ")[1].split(" ")[0]) if "総クリック数" in gsc_summary_for_ai else "N/A"
ga4_change = ga4_session_summary_for_ai.split("前年同期比 ")[1].split("。")[0] if "前年同期比" in ga4_session_summary_for_ai else "N/A"
gsc_change = gsc_summary_for_ai.split("前年比 ")[1].split(")")[0] + ")" if "前年比" in gsc_summary_for_ai else "N/A"
final_gas_code_string = final_gas_code_string.replace("__GA4_SESSION_TOTAL_PLACEHOLDER__", f"{ga4_total} ({ga4_change})")
final_gas_code_string = final_gas_code_string.replace("__GSC_CLICK_TOTAL_PLACEHOLDER__", f"{gsc_total} ({gsc_change})")
except:
final_gas_code_string = final_gas_code_string.replace("__GA4_SESSION_TOTAL_PLACEHOLDER__", "データ解析エラー")
final_gas_code_string = final_gas_code_string.replace("__GSC_CLICK_TOTAL_PLACEHOLDER__", "データ解析エラー")
# 生成画像数の計算
generated_image_count = len([p for p in [ga4_sessions_yoy_png_path, gsc_clicks_impressions_yoy_png_path, ga4_lp_table_png_path, gsc_query_table_png_path] if p and os.path.exists(p)])
final_gas_code_string = final_gas_code_string.replace("__GENERATED_IMAGE_COUNT_PLACEHOLDER__", f"{generated_image_count}/4")
# 生成されたGASコードをファイルに保存
generated_gas_file_path = f"{OUTPUT_DIR}/Generated_MonthlyReport_Script_{yyyymm}.txt"
try:
with open(generated_gas_file_path, 'w', encoding='utf-8') as f:
f.write(final_gas_code_string)
print(f"\n新形式GASコードを保存しました: {generated_gas_file_path}")
print(f"Googleドライブ上のパス例: {OUTPUT_DIR.replace('/content/drive/MyDrive/', '')}/Generated_MonthlyReport_Script_{yyyymm}.txt")
print("このファイルの内容をコピーして、Googleスライドに紐づいたApps Scriptエディタに貼り付けて実行してください。")
# 設定確認メッセージ
print(f"\n【設定確認】")
print(f"- テンプレートID: {TEMPLATE_ID}")
print(f"- 画像フォルダーID: {IMAGE_FOLDER_ID}")
print(f"- レポートタイトル: {report_title}")
print(f"- 生成画像数: {generated_image_count}/4")
print(f"- AI分析コメント: 5項目生成完了")
except Exception as e:
print(f"[エラー] 生成GASコードのファイル保存中: {e}")
print("\n--- 強化版プロンプトによる全ての処理が完了しました ---")
print("【改善点】")
print("1. プロンプトの大幅強化: 分析深度・実務価値・戦略性を向上")
print("2. GAS形式の更新: フォルダー公開/非公開・エラーハンドリング・実行ログ追加")
print("3. 設定の改善: テンプレートID・フォルダーIDの明確な分離")
print("4. 出力品質向上: より実行可能で具体的な分析コメント生成")
print("5. 月跨ぎ・年跨ぎ対応: 任意の期間設定に対応")
print("6. 画像URL取得の強化: デバッグ情報とDrive同期待ち追加")
■ 無料相談・お問合せ (弊社からの営業は一切しません)
「うちの場合は自動化・省力化できるのか?」
まずはお気軽に無料相談をご利用ください。
- オンライン全国対応
- 相談だけでもOK
- 営業メールなどは一切しません
- 技術者が直接ヒアリングして回答します
- 概算費用・工数をご提示します
弊社の主要サービス ▼
- GA4/GSC 定期レポート自動生成パッケージ
- カスタムAIアシスタント構築サービス(ChatGPT/Claude対応)
- 各種テクニカル診断(GA4, GTM, LookerStudio, Python, GAS, SQLなど)