投稿

9月, 2026の投稿を表示しています

Yahoo広告APIのレポートをPythonで取る4手順

イメージ
Yahoo広告のレポートをPythonで取ろうとして、最初の1本が動くまでに半日が溶けた。原因はコードの書き方ではない。Google広告やMeta広告と違い、Yahoo検索広告APIのレポートは「その場で数値が返るAPI」ではないからだ。定義を登録し、ジョブの完了を待ち、CSVをダウンロードし、後片付けをする。この4手順を知らずにレスポンスをJSONとして読もうとすると、出てくる例外の文面からは原因がまったく読み取れない。ここでは当月消化を毎日取得している実運用コードをもとに、4手順の流れとつまずいた箇所を書く。対象はYahoo検索広告API v19だ。 レポート取得は「4手順のジョブ処理」になる Yahoo検索広告APIのレポートは、1回のリクエストで数値が返らない。実際の流れは次の4手順だ。 (1) ReportDefinitionService/add でレポート定義を登録し、reportJobId を受け取る (2) ReportDefinitionService/get を叩き、reportJobStatus が COMPLETED になるまで待つ (3) ReportDefinitionService/download でCSVを受け取る (4) 不要になった定義を remove で片づける 実測では、(2)の完了まで3秒から15秒かかった。運用コードでは3秒間隔で最大40回ポーリングしている。日次バッチなら十分な余裕だ。エンドポイントは ads-search.yahooapis.jp/api/v19 になる。ドメインを ads.yahoo.co.jp と書くと404が返る。旧バージョンのv16やv17を叩くと code:0004 URL not found だ。 もう1つ、最初に必ず詰まるのがヘッダーである。リクエストには MCC のIDを x-z-base-account-id ヘッダーに入れる必要がある。これを付け忘れると code:0001 Invalid Request が返る。さらに厄介なのは、レポート以外の読み取り系APIだと エラーにならず0件で返ってくる ことだ。「キャンペーンが1本もない」と読み違えた。仕様の詳細は LINEヤフー広告の公式リファレンス にある。Yahoo広告API全般のつまずきどころは Yahoo広告APIをPyth...

Meta広告APIのcode:17を実運用で潰した4手順

イメージ
Meta広告APIで自動化を書くと、どこかで必ず code:17 に当たる。やっかいなのは、このエラーが HTTP 400 として返ってくる点だ。本文を読まないと「リクエストが不正」と誤診する。実際、2026年8月にクリエイティブ監査スクリプトが「HTTP Error 400」とだけ出力した。原因の切り分けに時間を要した。コードは正しかった。単に呼びすぎていただけである。 先に結論を置く。code:17 は時間で回復する一時エラーだ。対処は4つしかない。(1)HTTPステータスでなく本文の error.code で分岐する。(2)成功後も2秒待つ。(3)30秒刻みのバックオフで再試行する。(4)書込系は再試行しない。以下、実運用で踏んだ順に書く。 1. code:17 は HTTP 400 で返るので誤診する レート制限時のレスポンス本文は type が OAuthException になる。code は 17、error_subcode は 2446079 だ。メッセージは "User request limit reached" である。日本語環境では「広告アカウントのAPI呼び出しが多すぎます」と出る。 問題は、これが 429 ではなく 400 Bad Request で返ることだ。Pythonの urllib.request.urlopen は 400 で HTTPError を投げる。素直に書くと例外で落ち、ログには「取得失敗」としか残らない。パラメータの綴りを疑って時間を溶かす典型パターンだ。 もう一つの罠が is_transient が false になっている点である。恒久エラーに見えるが、実際は数分から1時間で回復する。この値を見て「仕様変更で弾かれた」と判断してはいけない。仕様の詳細は Meta 公式の Rate Limiting ドキュメントが一次情報になる。URLは https://developers.facebook.com/docs/marketing-api/overview/rate-limiting/ だ。 対処は単純で、例外ハンドラの中でエラー本文をパースし、ステータスではなく code の値で分岐することだ。これだけで「原因不明の400」が「待てば通るやつ」に変わる。 2. クォータは広告アカウント...

validate_onlyで入稿ミスを実行前に弾く3手順

イメージ
広告の入稿ミスは、気づいた時にはもう配信が始まっている。日予算の桁を1つ間違えれば、数時間で数十万円が溶ける。人力のダブルチェックは何度でもすり抜ける。API経由で入稿している運用者なら、実行前にサーバー側で検証させる手段がある。Google広告APIの validate_only だ。この記事では、validate_onlyで弾けるミスと弾けないミスを切り分け、実行前チェックを3手順のルーティンに落とす。 入稿ミスは「実行後」に気づくと戻せない API入稿の事故が怖いのは、取り消しの難易度が操作ごとに違うからだ。日予算の変更は上書きで戻せる。だが入札戦略の変更は、戻しても学習がリセットされた事実は消えない。キャンペーンの有効化は、配信済みのインプレッションを取り消せない。 とくに事故りやすいのがGoogle広告APIの金額単位 micros (100万分の1通貨単位)だ。日予算3,000円は3000000000ではなく3000000と書く。ゼロを3つ多く打っても、APIは文法上正しいリクエストとして受け取る。人間の目視では、桁の多い数字ほど見落とす。 そこで運用では、書き込みを「①検証だけ実行 → ②APIで実値を読み返して照合 → ③有効化」の3段に分けている。読み返しの際は必ず1,000,000で割って円に戻してから、意図した値と突き合わせる。microsのまま比較すると、桁ミスを桁ミスのまま見逃す。APIの基本的な叩き方は Google広告APIをPythonで動かす手順 にまとめた。 validate_onlyの正体と、Pythonでの付け方 validate_onlyは、リクエストを実際には反映せず、バリデーションだけ走らせるフラグだ。trueを付けると、アカウントには何も作られない。にもかかわらず、入力に問題があれば通常どおり例外が返る。つまり「本番と同じ検証ロジックを、副作用なしで通せる」。 Pythonクライアントで最初につまずくのがここだ。validate_onlyは各サービスのメソッド引数として用意されていない。client.get_type("MutateCampaignsRequest") でリクエストオブジェクトを作り、request.validate_only = True と直接代入してから渡す必要がある。キーワー...

20枠中12枠が不発火 launchd自動化の3つの罠

イメージ
広告レポートの自動送信、日予算の自動計算、Slackへの異常通知。Pythonで書いたスクリプトをmacOSのlaunchdに登録し、そこで安心してしまう運用者は多い。だが登録した時刻に「静かに動かない」事故が起きる。 厄介なのはエラーも例外も出ないことだ。ログに何も残らず、翌朝そこに成果物だけが無い。実際に運用中のX投稿自動化では、2026年9月4日から8日の5日間で、1日4枠×5日=20枠のうち12枠が不発火した。原因はコードのバグではなく、launchdの仕様とプロセス設計の噛み合わせである。ここでは実測ログから特定した3つの罠と、取りこぼしを埋める追い実行ジョブの実装を示す。cronから移ってきた人ほど踏みやすい。 罠1: スリープ中の枠は発火せず、復帰後に1回へ丸められる StartCalendarIntervalは指定時刻にMacが起きていなければ発火しない。スリープからの復帰時には、未実行のジョブを1回だけまとめて実行する。つまり3枠ぶん眠っていても、走るのは1回だ。シャットダウンしていた場合は次回起動時に回る。この挙動はAppleのScheduled Jobsのドキュメント(https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html)に明記されている。cronのように「その時刻に居なければ捨てる」でも、anacronのように「全部後で流す」でもない中間の挙動が誤解を生む。 実測はこうだった。9月4日から8日にかけて、X投稿の公開枠は20枠。実際に公開されたのは8本で、12枠が消えていた。消えた枠は夜間にMacをシャットダウンしていた日と一致した。スクリプト側のログには失敗の記録が1行も無い。呼ばれていないのだから当然である。 この時点で設計方針を変えた。「指定時刻の正確さ」を守るのをやめ、「1日にN回実行された事実」を担保する方向に切り替える。時刻厳守が要るのは配信停止やレポート提出の締切がある処理だけで、大半の運用自動化は回数さえ満たせば実害が無い。 罠2: 長寿命プロセスと多重起動ガードが「正常なゼロ実行」を作る 2つ目はlaunchdではなくスクリプト側の設計に起因す...

Google広告で実は止まっているKWを見抜く4手順

イメージ
キーワードの停止や除外を提案したあとで「それ、とっくに止まっています」と返された経験はないだろうか。広告運用の自動化を進めるほど、この誤判定は増える。原因ははっきりしている。Google広告APIで status を1階層しか見ていない からだ。キャンペーンが有効でも、広告グループが停止していれば配信はゼロ。ここでは実運用で3度同じミスを踏んだうえで固めた、実効的な配信状態を4階層で判定する手順を示す。GAQLのクエリと、そのままコピペできるチェックリストまで持ち帰れる構成にした。 1階層のstatusだけ見ると「止まっているKW」を提案してしまう Google広告の管理画面は、親が止まっていても子の行を「有効」と表示する。APIも同じだ。ad_group_criterion.status が ENABLED でも、それは そのキーワード自身の設定値 にすぎない。上位のキャンペーンや広告グループが PAUSED なら、実際のインプレッションは発生しない。 実際に踏んだ失敗はこうだ。90日間の消化額が大きいキーワードを抽出し、無駄打ちとして停止提案を出した。だがそのキーワードは、提案の3週間前にすでに停止されていた。90日という窓が、停止前の消化を丸ごと含んでいたわけだ。指摘を受けるまで、レポート上は完全に正しく見えていた。同じ種類のミスを3度繰り返して、ようやく判定ロジックそのものが誤っていると認めた。 誤判定の怖さは、間違った数字が出ることではない。 「もう対処済みの問題」を提案として出し続けること で、自動化の出力そのものが信用されなくなる点にある。 実効配信の判定は「4階層すべてENABLED」+「有効な広告が1本以上」 判定条件を言葉にすると単純だ。次の5つを同時に満たしたときだけ、そのキーワードは配信されている。 campaign.status が ENABLED ad_group.status が ENABLED ad_group_criterion.status が ENABLED キャンペーンの期間・予算が有効(campaign.primary_status で確認) その広告グループに有効な広告が1本以上ある 最後の条件が抜けやすい。キーワードが有効でも、広告グループ内の広告が全て停止・不承認なら配信は起きない。広告...

Google広告の品質スコアをPythonで一括監視する実装

イメージ
Google広告の品質スコアは、クリック単価に直結する指標だ。だが管理画面で列を足して1件ずつ眺める運用は、キーワードが増えるほど回らなくなる。運用中のアカウントで試したところ、PythonとGoogle Ads APIを使えば、全キーワードの品質スコアを数秒で一括取得できた。この記事では、実際に使ったGAQLクエリと、スコアがnullで返るキーワードへの対処、週1で変化を検知する運用手順まで、そのまま真似できる形でまとめる。 なぜ品質スコアは手作業で追いきれないのか 品質スコアは推定CTR・広告の関連性・ランディングページの利便性の3要素から、1〜10で決まる( 検索キャンペーンの品質スコアについて )。スコアが上がればクリック単価は下がる。逆に低いキーワードを放置すると、同じ順位でも余計な費用を払う。 問題は確認のコストだ。管理画面ではキーワードタブに列を追加すれば見えるが、キャンペーンをまたいだ横断比較や、先週との差分は取りにくい。運用しているアカウントではキーワードが約1,200件あり、目視の棚卸しに毎週30分近くかかっていた。ここを自動化する。 keyword_viewから3要素を一括取得するGAQL 品質スコアはキーワード単位の指標なので、GAQLではkeyword_viewから引く。総合スコアはad_group_criterion.quality_info.quality_score、内訳は同じquality_info配下のcreative_quality_score・post_click_quality_score・search_predicted_ctrに入っている( AdGroupCriterionリファレンス )。 ブックマークして、自分のアカウントに合わせて書き換えて使ってほしい。次の1行がそのまま動く土台になる。 SELECT campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.quality_info.quality_score, ad_group_criterion.quality_info.creative_quality_score, ad_group_criterion.quality_info.post_click_q...