Spaces:
Runtime error
Runtime error
Download src/google_api.py from BinbomJared/DM-Agent: direct link, hf CLI and curl.
- Browser
- Download file 8.35 kB
-
https://huggingface.co/spaces/BinbomJared/DM-Agent/resolve/main/src/google_api.py
- Command line
-
hf download hf://spaces/BinbomJared/DM-Agent/src/google_api.py
-
curl -L -o google_api.py https://huggingface.co/spaces/BinbomJared/DM-Agent/resolve/main/src/google_api.py
8.35 kB
| """ | |
| Google Ads API 数据拉取引擎。 | |
| 通过 REST API 拉取 campaign 级日度数据,转换为与 Google Sheets 相同的列结构。 | |
| """ | |
| import json | |
| import logging | |
| import os | |
| from datetime import date, timedelta | |
| from typing import Any | |
| import pandas as pd | |
| import requests | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| log = logging.getLogger("ad-agent") | |
| API_VERSION = "v23" | |
| BASE_URL = f"https://googleads.googleapis.com/{API_VERSION}" | |
| CHANNEL_TYPE_MAP = { | |
| "SEARCH": "Conversion", | |
| "SHOPPING": "Conversion", | |
| "PERFORMANCE_MAX": "Conversion", | |
| "DISPLAY": "Awareness", | |
| "VIDEO": "Consideration", | |
| "MULTI_CHANNEL": "Conversion", | |
| "DEMAND_GEN": "Consideration", | |
| "LOCAL": "Conversion", | |
| "SMART": "Conversion", | |
| "DISCOVERY": "Consideration", | |
| } | |
| GAQL_QUERY = """ | |
| SELECT | |
| campaign.name, | |
| campaign.id, | |
| campaign.advertising_channel_type, | |
| metrics.cost_micros, | |
| metrics.impressions, | |
| metrics.clicks, | |
| metrics.ctr, | |
| metrics.average_cpc, | |
| metrics.average_cpm, | |
| metrics.conversions, | |
| metrics.conversions_value, | |
| segments.date | |
| FROM campaign | |
| WHERE segments.date BETWEEN '{since}' AND '{until}' | |
| AND metrics.impressions > 0 | |
| ORDER BY segments.date DESC, campaign.name | |
| """ | |
| def _get_access_token() -> str: | |
| resp = requests.post("https://oauth2.googleapis.com/token", data={ | |
| "client_id": os.getenv("GOOGLE_ADS_CLIENT_ID"), | |
| "client_secret": os.getenv("GOOGLE_ADS_CLIENT_SECRET"), | |
| "refresh_token": os.getenv("GOOGLE_ADS_REFRESH_TOKEN"), | |
| "grant_type": "refresh_token", | |
| }) | |
| token = resp.json().get("access_token") | |
| if not token: | |
| raise RuntimeError(f"Google OAuth token 刷新失败: {resp.text[:200]}") | |
| return token | |
| def _fetch_campaigns( | |
| customer_id: str, access_token: str, since: str, until: str | |
| ) -> list[dict]: | |
| """分页拉取一个广告账户的 campaign 级日度数据。""" | |
| login_id = os.getenv("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "") | |
| dev_token = os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", "") | |
| headers = { | |
| "Authorization": f"Bearer {access_token}", | |
| "developer-token": dev_token, | |
| "Content-Type": "application/json", | |
| } | |
| if login_id: | |
| headers["login-customer-id"] = login_id | |
| query = GAQL_QUERY.format(since=since, until=until) | |
| all_results: list[dict] = [] | |
| page_token = None | |
| while True: | |
| body: dict[str, Any] = {"query": query} | |
| if page_token: | |
| body["pageToken"] = page_token | |
| url = f"{BASE_URL}/customers/{customer_id}/googleAds:search" | |
| resp = requests.post(url, headers=headers, json=body) | |
| if resp.status_code != 200: | |
| detail = resp.text[:800] | |
| log.error(f"Google Ads API error ({resp.status_code}): {detail}") | |
| raise RuntimeError(f"Google Ads API 请求失败 ({resp.status_code}): {detail}") | |
| data = resp.json() | |
| all_results.extend(data.get("results", [])) | |
| page_token = data.get("nextPageToken") | |
| if not page_token: | |
| break | |
| return all_results | |
| def _parse_row(row: dict, country: str) -> dict[str, Any]: | |
| """将一条 Google Ads API 返回数据转换为与 Google Sheets 相同的列结构。""" | |
| campaign = row.get("campaign", {}) | |
| metrics = row.get("metrics", {}) | |
| segments = row.get("segments", {}) | |
| cost = int(metrics.get("costMicros", 0)) / 1_000_000 | |
| impressions = int(metrics.get("impressions", 0)) | |
| clicks = int(metrics.get("clicks", 0)) | |
| ctr = float(metrics.get("ctr", 0)) | |
| avg_cpc = int(metrics.get("averageCpc", 0)) / 1_000_000 | |
| avg_cpm = int(metrics.get("averageCpm", 0)) / 1_000_000 | |
| conversions = float(metrics.get("conversions", 0)) | |
| conv_value = float(metrics.get("conversionsValue", 0)) | |
| video_views = int(metrics.get("videoViews", 0)) | |
| bounce_rate = None | |
| roas = conv_value / cost if cost > 0 else None | |
| cvr = conversions / clicks if clicks > 0 else None | |
| cplpv = cost / clicks if clicks > 0 else None | |
| campaign_name = campaign.get("name", "") | |
| name_parts = campaign_name.split("_") | |
| name_funnel = name_parts[1] if len(name_parts) > 1 else "" | |
| if name_funnel in ("Awareness", "Consideration", "Conversion"): | |
| campaign_type = name_funnel | |
| else: | |
| channel_type = campaign.get("advertisingChannelType", "") | |
| campaign_type = CHANNEL_TYPE_MAP.get(channel_type, "Conversion") | |
| name_lower = campaign_name.lower() | |
| if "awareness" in name_lower or "aw_" in name_lower: | |
| campaign_type = "Awareness" | |
| elif "consideration" in name_lower or "traffic" in name_lower: | |
| campaign_type = "Consideration" | |
| elif "conversion" in name_lower or "purchase" in name_lower or "pmax" in name_lower: | |
| campaign_type = "Conversion" | |
| return { | |
| "country": country, | |
| "platform": "Google", | |
| "campaign_type": campaign_type, | |
| "campaign_name": campaign_name, | |
| "date": segments.get("date", ""), | |
| "Cost": cost, | |
| "Ads.Sales": conv_value, | |
| "Ads.Sales(1d+7d)": None, | |
| "SPF.Sales": None, | |
| "ROAS": roas, | |
| "GA4.Sessions": clicks, | |
| "SPF.Sessions": None, | |
| "LPV": clicks, | |
| "CTR": ctr, | |
| "CVR": cvr, | |
| "CPC": avg_cpc, | |
| "CPSE": None, | |
| "CPLPV": cplpv, | |
| "CPM": avg_cpm, | |
| "Impressions": impressions, | |
| "Bounce Rate": bounce_rate, | |
| "Average Duration": None, | |
| "ATC": None, | |
| "CP-ATC": None, | |
| "Email": None, | |
| "CP-Email": None, | |
| "Video Views": video_views if video_views > 0 else None, | |
| "CP-Video Views": (cost / video_views) if video_views > 0 else None, | |
| "50% Play Rate": None, | |
| "100% Play Rate": None, | |
| "Hook Rate": None, | |
| } | |
| def fetch_google_data( | |
| since: date | None = None, | |
| until: date | None = None, | |
| days_back: int = 90, | |
| ) -> pd.DataFrame: | |
| """ | |
| 从所有配置的 Google Ads 账户拉取数据,返回与 Google Sheets 相同结构的 DataFrame。 | |
| """ | |
| refresh_token = os.getenv("GOOGLE_ADS_REFRESH_TOKEN") | |
| if not refresh_token: | |
| raise RuntimeError("未配置 GOOGLE_ADS_REFRESH_TOKEN") | |
| accounts_str = os.getenv("GOOGLE_ADS_CUSTOMER_IDS", "") | |
| if not accounts_str: | |
| raise RuntimeError("未配置 GOOGLE_ADS_CUSTOMER_IDS (格式: customer_id:country,...)") | |
| accounts = [] | |
| for pair in accounts_str.split(","): | |
| pair = pair.strip() | |
| if ":" in pair: | |
| cid, country = pair.split(":", 1) | |
| accounts.append((cid.strip(), country.strip())) | |
| if not accounts: | |
| raise RuntimeError("GOOGLE_ADS_CUSTOMER_IDS 格式错误") | |
| if until is None: | |
| until = date.today() - timedelta(days=1) | |
| if since is None: | |
| since = until - timedelta(days=days_back) | |
| since_str = since.strftime("%Y-%m-%d") | |
| until_str = until.strftime("%Y-%m-%d") | |
| access_token = _get_access_token() | |
| all_parsed: list[dict] = [] | |
| for customer_id, country in accounts: | |
| log.info(f"拉取 Google Ads 数据: customer={customer_id} country={country} {since_str}~{until_str}") | |
| try: | |
| rows = _fetch_campaigns(customer_id, access_token, since_str, until_str) | |
| log.info(f" 获取 {len(rows)} 条记录") | |
| for row in rows: | |
| parsed = _parse_row(row, country) | |
| all_parsed.append(parsed) | |
| except Exception as e: | |
| log.error(f" 拉取失败: {e}") | |
| raise | |
| if not all_parsed: | |
| return pd.DataFrame() | |
| df = pd.DataFrame(all_parsed) | |
| df["date"] = pd.to_datetime(df["date"]) | |
| return df | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.INFO) | |
| df = fetch_google_data(days_back=7) | |
| print(f"\n拉取到 {len(df)} 条数据") | |
| print(f"日期范围: {df['date'].min()} ~ {df['date'].max()}") | |
| print(f"国家: {df['country'].unique()}") | |
| print(f"Campaign Types: {df['campaign_type'].unique()}") | |
| print(f"\n汇总:") | |
| print(f" 总花费: ${df['Cost'].sum():,.2f}") | |
| print(f" 总销售: ${df['Ads.Sales'].sum():,.2f}") | |
| total_cost = df["Cost"].sum() | |
| total_sales = df["Ads.Sales"].sum() | |
| print(f" ROAS: {total_sales / total_cost:.2f}" if total_cost > 0 else " ROAS: N/A") | |