ota2000
8 min read

LINEヤフー広告 検索広告の dlt ソースを作った

dlt-community-sources に LINEヤフー広告検索広告(旧 Yahoo!検索広告 / SS)のソースを追加した。v0.7.1 でリリース済み。

なぜ dlt か

Yahoo!広告のデータを BigQuery に入れたかった。キャンペーンやターゲティング設定などのエンティティデータも含めて。

自前でスクリプトを書くと、ページネーション、リトライ、スキーマ管理、差分取得あたりを毎回実装することになる。dlt はそのへんを引き受けてくれるので、source(API からデータを取ってくる部分)の実装に集中できる。

dlt で source を書くと付いてくるもの。

  • BigQuery、Snowflake、DuckDB、Postgres など 30 以上のデスティネーション。source 側はデスティネーションを意識しなくていい
  • スキーマ進化。API にフィールドが追加されれば dlt が自動でカラムを追加する
  • dlt.sources.incremental による差分取得。レポートの attribution window にも乗せやすい
  • JSON の nested 構造を自動展開してテーブルに分割してくれる
  • PyPI パッケージとして配布できる。dlt init 方式(ソースコードをプロジェクトにコピーする方式)と違って、バージョン管理やアップデートが普通の pip install で回る

使い方

pip install dlt-community-sources[yahoo-ads-search]
import dlt
from dlt_community_sources.yahoo_ads_search import yahoo_ads_search_source

source = yahoo_ads_search_source(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    refresh_token="YOUR_REFRESH_TOKEN",
    base_account_id="YOUR_MCC_ID",
    account_id="YOUR_ACCOUNT_ID",
)

pipeline = dlt.pipeline(
    pipeline_name="yahoo_ads",
    destination="bigquery",  # or "duckdb", "snowflake", etc.
    dataset_name="yahoo_ads",
)
pipeline.run(source)

41 エンティティリソースと 1 レポートリソース(22 レポートタイプ)。

認証

Yahoo!広告 API は OAuth 2.0 Authorization Code Grant を使う。

  1. Yahoo! Ads Developer Center でアプリを登録して client_id / client_secret を取得
  2. ブラウザで OAuth 認可フローを実行して Authorization Code を取得
  3. Authorization Code → refresh_token を取得

refresh_token は 4 週間未使用で失効する。日次でパイプラインを回していれば延長され続けるので実質期限なし。ローテーションもないので Secret Manager への書き戻しも不要。認証まわりの運用は他の広告媒体と比べてシンプル。

source 関数に client_idclient_secretrefresh_token を渡すと、内部で access_token を自動取得する。access_token の有効期限は 1 時間だが、毎回 refresh するので意識する必要はない。

MCC 対応

Yahoo!広告は MCC(マイクライアントセンター)で複数の広告アカウントを束ねる構造になっている。

source は 1 アカウント = 1 呼び出しの設計にしている。base_account_id に MCC の ID を、account_id に広告アカウントの ID を渡す。base_account_idx-z-base-account-id ヘッダーにセットされる。

複数アカウントを取りたい場合は、discover_accounts ヘルパーで配下アカウントを取得してループする。パイプラインをアカウントごとに分けることで、incremental のカーソルがアカウント間で混ざらない。

from dlt_community_sources.yahoo_ads_common import (
    discover_accounts,
    make_client,
    refresh_access_token,
)

tokens = refresh_access_token(client_id, client_secret, refresh_token)
client = make_client(tokens["access_token"], base_account_id)
accounts = discover_accounts(client, "https://ads-search.yahooapis.jp/api/v19")

for account_id in accounts:
    pipeline = dlt.pipeline(
        pipeline_name=f"yahoo_ads_{account_id}",
        destination="bigquery",
        dataset_name="yahoo_ads",
    )
    source = yahoo_ads_search_source(
        base_account_id=base_account_id,
        account_id=account_id,
    )
    pipeline.run(source)

discover_accounts は SERVING ステータスのアカウントだけ返す。ENDED アカウントのデータが必要なら account_id を明示指定する。

動的フィールド取得

レポートのフィールドはハードコードしていない。実行時に getReportFields API を呼んで、そのレポートタイプで使えるフィールドの一覧を取得する。

# 内部でやっていること
body = {"reportType": "CAMPAIGN"}
data = post_rpc(client, f"{base_url}/ReportDefinitionService/getReportFields", body)
fields = [f["fieldName"] for f in data["rval"]["fields"]]

API バージョンが上がってフィールドが増減しても、ソースのコードを変える必要がない。ここは割と気に入っている。

impossibleCombinationFields の自動解決

Yahoo!広告のレポート API には「同時に指定できないフィールドの組み合わせ」がある。getReportFields のレスポンスに impossibleCombinationFields として入っている。

全フィールドをそのまま指定するとエラーになる。そこで貪欲法でコンフリクトの多いフィールドから順に除外して、最大の互換フィールドセットを組み立てている。CAMPAIGN レポートだと 68 フィールド中 60 が同時取得できる。除外されるのは CONVERSION_NAMEOBJECT_OF_CONVERSION_TRACKING 等のセグメント系。

個別にフィールドを指定したければ report_fields パラメータで上書きできる。

動的型変換

レポートの CSV は全部文字列で返ってくる。型変換もハードコードではなく getReportFieldsfieldType を見ている。

# getReportFields のレスポンスにこういう情報が入っている
{"fieldName": "COST", "fieldType": "LONG"}      # → int
{"fieldName": "CLICK_RATE", "fieldType": "DOUBLE"}  # → Decimal
{"fieldName": "CAMPAIGN_NAME", "fieldType": "STRING"}  # → そのまま

新しい数値フィールドが追加されても型が合う。金額系は Decimal で精度を保持している。

CSV カラム名のマッピング

地味にハマったポイント。Yahoo!広告のレポート CSV は、ヘッダーが API フィールド名(ACCOUNT_ID)ではなく表示名(Account ID)で返ってくる。

内部では reportLanguage=EN 固定にして、getReportFieldsdisplayFieldNameEn から表示名→フィールド名のマッピングを作り、ダウンロード後にカラム名を変換している。

サービスごとの body_style

これもハマった。Yahoo!広告の API はサービスによってリクエストボディの構造が違う。

  • CampaignService/get: accountId + startIndex + numberResults(普通のページネーション)
  • BalanceService/get: accountIds 配列(ページネーションなし)
  • CustomizerAttributeService/get: accountId のみ(ページネーションパラメータを渡すと 400)
  • AccountLinkService/get: 空ボディ

ドキュメントからは読み取れなかったので、実際に叩いて 400 のレスポンスボディから判別した。41 リソース分。

実装の判断

rest_api を使わなかった理由

dlt の宣言的 rest_api は REST API 向けに設計されている。Yahoo!広告の API は全エンドポイントが POST RPC スタイルなので合わない。レポート取得も非同期 3 ステップ(定義作成 → ポーリング → CSV ダウンロード)。カスタム @dlt.resource で書いた。

write_disposition の選択

マスタデータ(campaigns、ad_groups 等)は mergereplace だと毎回全件置換で、データ量が増えると遅い。レポートも merge + dlt.sources.incremental で差分更新しつつ、attribution window 分だけ遡って再取得する。

campaign_targets や balance のようなスナップショット系は replace。毎回入れ替える方が正しい。

ENDED アカウントの扱い

discover_accounts は SERVING のアカウントだけ返す。ENDED でも API 的にはデータを取れるが、配信実績がないとレポートは空になる。ENDED アカウントのデータが必要なら account_id で明示指定する。

疎通確認

複数の MCC と配下アカウントで動作確認済み。エンティティとレポートの両方で DuckDB へのロードまで通っている。

今後

YDA(ディスプレイ広告)は SS と共通コード(yahoo_ads_common)を共有する設計にしてある。YDA のデータが使えるようになったら PR #9 で追加する。