Google Ad Managerで過去の記事ではコンテンツの取得を行ってきました。
https://qiita.com/WakameSun/items/08159f8b03c52a99fd58
https://qiita.com/WakameSun/items/c2fe5b6118c5e13125f3
https://qiita.com/WakameSun/items/53d26a8fcc28061a6740
この記事ではCSVファイルから広告申込の作成を目指します。
配信する広告
広告申込は設定する要素が多いです。
以下に今回設定する要素を列挙します。
| 英語名 | GAMでの表示名 or 日本語名 | 詳細 |
|---|---|---|
| name | 名前 | 広告申込の名前 今回はCSVに入力する |
| order_id | オーダーID | オーダーのID 今回はオーダー名をCSVに入力する |
| targeting | ターゲティング | 配信先の設定。広告ユニットや配信地域を設定する。 今回は広告ユニットを指定する |
| start_date_time_type | 開始時間の種類 | すぐ配信する(IMMIDATELY)か時間を指定する(USE_START_DATE_TIME)か選べる。今回は USE_START_DATE_TIME固定 |
| allow_overbook | 過剰予約の許可 | 予算に対して、impressionが多い場合にバリデーションで落とすかどうか。 今回はこの設定はoffにする |
| creative_rotation_type | クリエイティブのローテーション | 複数クリエイティブがある場合に、どのような配信の割り当てをするか。 今回は最適化( OPTIMIZED)固定とする |
| frequency_caps | ユーザーごとのフリークエンシー キャップ | 同じユーザーに同じ広告をどれだけ流すか。 今回は1日5impとする |
| delivery_rate_type | 広告の配信率 | 広告の配信ペースをどうするか。 今回は均等( EVENLY)を選択 |
| delivery_forecast_source | 配信ペースのリソース | どの程度、広告を配信するかという課題に対してどの情報を参考にするか。 今回は( HISTORICAL)固定 |
| roadblocking_type | 表示するクリエイティブ数 | 同じ広告を複数の箇所に出して良いか。 今回は一つのみ( ONLY_ONE)固定 |
| line_item_type | 広告申込情報タイプ | 広告申込がどのような案件か。 この値によって、設定できる優先度の値が決まり、今回は標準( STANDARD)とする |
| priority | 優先度 | 広告配信の優先度。低いほど優先度が高い。 今回は8とする |
| primary_goal | 目標 | どの程度配信するか。imp、クリック数など指標によって選べる。 今回は配信期間中( LIFETIME)で特定のimp数を達成することを目標とする |
| cost_per_unit | レート | 広告主がメディアにどの程度払うか。 今回はCPMで円単位で指定する |
| child_content_eligibility | 子供向け広告 | 子供に対して配信するかどうか。 今回は許可しない( DISALLOWED)固定とする。 |
| discount_type | ディスカウントタイプ | 割引をする場合の種類。 割合と絶対値で指定できるが、今回は割引をしないので割合( PERCENTAGE)を仮置きしている |
| discount | 割引 | 割引額。 今回は割引をしないものとして、0固定としている |
| creative_placeholders | 想定されるクリエイティブ | クリエイティブサイズや種類を記入する。 今回はクリエイティブのサイズを記入する方式のみを採用する |
| start_date_time | 開始時間 | 配信を開始する時間 |
| end_date_time | 終了時間 | 配信を終了する時間 |
| cost_type | コストのタイプ | 価格計算に使う単位で primary_goalで自動的に何を入れるかが決まる。 今回はimpがゴールとするのでCPMになる |
表を見てもわかにくいと思うので結論を書きますが、CSVの情報から取得する情報は以下となります。
それ以外は固定の値を入れています。
- name
- order_id
- targeting
- primary_goal
- creative_placeholders
- start_date_time
- end_date_time
- cost_per_unit
CSVの作成
以下のようなCSVを作成します(記事だと見にくいので表も掲載)。
名前,オーダー名,想定されるクリエイティブ,開始時間,終了時間,インプレッション数,CPM(円),広告ユニット
test1,テスト,"300x250|728x90",2025/11/06 0:00:00,2025/11/06 0:01:00,10,21,"質問機能_PC記事下|非ログイン広告②"
test2,テスト,"300x250",2025/11/06 0:00:00,2025/11/06 0:01:00,10,21,"質問機能_PC記事下"
| 名前 | オーダー名 | 想定されるクリエイティブ | 開始時間 | 終了時間 | インプレッション数 | CPM(円) | 広告ユニット |
|---|---|---|---|---|---|---|---|
| test1 | オーダー1 | 300x250|728x90 | 2025/11/06 0:00:00 | 2025/11/07 0:00:00 | 1000 | 4100 | 広告ユニット1|広告ユニット2 |
| test2 | オーダー2 | 300x250 | 2025/11/06 0:00:00 | 2025/11/07 0:00:00 | 1000 | 2100 | 広告ユニット1 |
想定されるクリエイティブと、広告ユニットは複数ある場合がありますが、その場合バーティカルバー(|)で区切っています。
また、オーダーと広告ユニットは本来IDが必要ですが、CSVの段階では名前を入力し、名前からIDを取得することで申込の情報を作成します。
実装
main.rb
まずはmainファイル。
環境変数を準備すると同時に、広告申込作成用のGoogleAdManager::LineItemImporterクラスの初期化と作成する関数を呼び出しています。
require 'dotenv'
require_relative './lib/google_ad_manager/line_item_importer'
Dotenv.load
importer = GoogleAdManager::LineItemImporter.new(ENV['CSV_FILE_PATH'])
importer.import_line_items
次にGoogleAdManager::LineItemImporterクラスを見たいところですが、その前に付随するクラス群を見ていきます。
GoogleAdManager::Order
まずは GoogleAdManager::Orderクラス。
Orderの名前から実際にOrderのオブジェクトを取得します。
詳細はほぼ過去の記事で書いているので割愛します。
コード
# frozen_string_literal: true
module GoogleAdManager
class Order
require 'ad_manager_api'
LIMIT = 20
# @rbs gam_client: AdManagerApi::Api
# @rbs order_names: String[]
def initialize(gam_client, order_names)
@gam_client = gam_client
@order_names = order_names
end
# @rbs order_name: String
# @rbs return: Hash
def find_order_by_name(order_name)
order = orders[order_name]
fail "Order is not found: #{order_name}" unless order
order
end
attr_reader :order_names, :gam_client
private
def client
@client ||= gam_client.service(:OrderService, :v202508)
end
def orders
return @orders if defined?(@orders)
in_str = order_names.each_with_index.map { |_, index| ":name_#{index}" }.join(', ')
offset = 0
all_orders = []
response = nil
loop do
statement = gam_client.new_statement_builder do |sb|
sb.where = "name IN (#{in_str})"
order_names.each_with_index do |name, index|
sb.with_bind_variable("name_#{index}", name)
end
sb.limit = LIMIT
sb.offset = offset
end.to_statement
response = client.get_orders_by_statement(statement)
offset += LIMIT
all_orders += response[:results] || []
break if response[:results].nil? || response[:results].size < LIMIT
sleep 1
end
@orders = all_orders.index_by { |o| o[:name] }
end
end
end
GoogleAdManager::Inventry
次にGoogleAdManager::Inventryクラス。
こちらも同様に広告ユニットの名前から実際にAdUnitオブジェクトを取得します。
コード
# frozen_string_literal: true
module GoogleAdManager
class Inventory
require 'ad_manager_api'
LIMIT = 20
# @rbs gam_client: AdManagerApi::Api
# @rbs ad_unit_names: String[]
def initialize(gam_client, ad_unit_names)
@gam_client = gam_client
@ad_unit_names = ad_unit_names
end
# @rbs ad_unit_name: String
def find_ad_unit_by_name(ad_unit_name)
ad_unit = ad_units[ad_unit_name]
fail "Ad unit is not found: #{ad_unit_name}" unless ad_unit
ad_unit
end
attr_reader :ad_unit_names, :gam_client
private
def client
@client ||= gam_client.service(:InventoryService, :v202508)
end
def ad_units
return @ad_units if defined?(@ad_units)
in_str = ad_unit_names.each_with_index.map { |_, index| ":name_#{index}" }.join(', ')
offset = 0
all_ad_units = []
response = nil
loop do
statement = gam_client.new_statement_builder do |sb|
sb.where = "status = :status AND name IN (#{in_str})"
ad_unit_names.each_with_index do |name, index|
sb.with_bind_variable("name_#{index}", name)
end
sb.with_bind_variable('status', 'ACTIVE')
sb.limit = LIMIT
sb.offset = offset
end.to_statement
response = client.get_ad_units_by_statement(statement)
offset += LIMIT
all_ad_units += response[:results] || []
break if response[:results].nil? || response[:results].size < LIMIT
sleep 1
end
@ad_units = all_ad_units.index_by { |unit| unit[:name] }
end
end
end
GoogleAdManager::LineItem
GoogleAdManager::LineItemクラスでは実際に広告申込を作成するreate_line_items関数をクライアントから呼び出せるようにしているのと、最終的に作成するデータのハッシュを作るための関数create_line_item_datumを定義しています。
固定値で良い部分はあえてハードコーディングし、変動する可能性のある値は引数や設定ファイルから取得できるようにしています。
コード
# frozen_string_literal: true
module GoogleAdManager
class LineItem
require 'ad_manager_api'
require 'active_support/core_ext/module/delegation'
# @rbs gam_client: AdManagerApi::Api
def initialize(gam_client)
@gam_client = gam_client
end
attr_reader :gam_client
delegate :create_line_items, to: :client
# @rbs name: String
# @rbs order_id: Integer
# @rbs start_date_time: Time
# @rbs end_date_time: Time
# @rbs inventory_targeting: Hash
# @rbs impressions: Integer
# @rbs cpm: Float
# @rbs return: Hash
def create_line_item_datum(name:, order_id:, start_date_time:, end_date_time:, creative_placeholders:, inventory_targeting:, impressions:, cpm:)
{
name: name,
order_id: order_id,
targeting: {
inventory_targeting: inventory_targeting,
},
start_date_time_type: 'USE_START_DATE_TIME',
allow_overbook: true,
creative_rotation_type: 'OPTIMIZED',
frequency_caps: [{ max_impressions: 3, num_time_units: 1, time_unit: 'DAY' }],
delivery_rate_type: 'EVENLY',
delivery_forecast_source: 'HISTORICAL',
roadblocking_type: 'ONLY_ONE',
line_item_type: 'STANDARD',
priority: 8,
primary_goal: { goal_type: 'LIFETIME', unit_type: 'IMPRESSIONS', units: impressions },
cost_per_unit: { currency_code: 'JPY', micro_amount: (cpm * 1_000_000).to_i },
child_content_eligibility: 'DISALLOWED',
discount_type: 'PERCENTAGE',
discount: 0,
creative_placeholders: creative_placeholders,
start_date_time: format_datetime(start_date_time),
end_date_time: format_datetime(end_date_time),
cost_type: 'CPM',
}
end
private
def client
@client ||= gam_client.service(:LineItemService, :v202508)
end
def format_datetime(datetime)
gam_client.datetime(
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.min,
datetime.sec,
'Asia/Tokyo'
).to_h
end
end
end
GoogleAdManager::LineItemImporter
最後に肝となるクラス。
CSVのデータをGAMのクライアントの繋ぎ込みを行います。
details
# frozen_string_literal: true
module GoogleAdManager
class LineItemImporter
require 'ad_manager_api'
require 'csv'
require_relative 'line_item'
require_relative 'order'
require_relative 'inventory'
require 'active_support/all'
# @rbs csv_file_path: String
def initialize(csv_file_path)
@csv_file_path = csv_file_path
end
# @rbs return: Hash[]
def import_line_items
import_data = csv_file.map do |row|
create_line_item_datum(row)
end
line_item_client.create_line_items(import_data)
end
private
attr_reader :csv_file_path
def csv_file
@csv_file ||= CSV.read(csv_file_path, headers: true)
end
def client
fail('Google Ad Manager private key is not configured.') if Setting['google_ad_manager_private_key'].blank?
@client ||= AdManagerApi::Api.new(
authentication: {
method: 'OAUTH2_SERVICE_ACCOUNT',
application_name: "Line Item Importer (#{Rails.env})",
network_code: Setting['google_ad_manager_network_code'],
oauth2_issuer: Setting['google_ad_manager_client_email'],
oauth2_key: OpenSSL::PKey::RSA.new(Setting['google_ad_manager_private_key']),
}
)
end
def line_item_client
@line_item_client ||= ::GoogleAdManager::LineItem.new(client)
end
def order_client
return @order_client if defined?(@order_client)
order_names = csv_file.pluck('オーダー名').uniq
@order_client = ::GoogleAdManager::Order.new(client, order_names)
end
# row['広告ユニット']'s format: Split by '|'
def inventory_client
return @inventory_client if defined?(@inventory_client)
ad_unit_names = csv_file.flat_map { |row| row['広告ユニット'].split('|').each(&:strip!).compact_blank }.uniq
@inventory_client = ::GoogleAdManager::Inventory.new(client, ad_unit_names)
end
def validate_creative_size(size)
size.present? && size > 0
end
# row['想定されるクリエイティブ']'s format: 'WIDTHxHEIGHT' split by '|'
def creative_placeholders_from_row(row)
(row['想定されるクリエイティブ'] || '').split('|').each(&:strip!).map do |size_str|
width, height = size_str.strip.split('x').each(&:strip!).map(&:to_i)
fail "Invalid creative size: #{size_str}" unless validate_creative_size(width) && validate_creative_size(height)
{
size: { width: width, height: height, is_aspect_ratio: false },
expected_creative_count: 1,
creative_size_type: 'PIXEL',
is_amp_only: false
}
end
end
def create_line_item_datum(row)
start_date_time = Time.parse(row['開始時間'])
end_date_time = Time.parse(row['終了時間'])
ad_unit_ids = (row['広告ユニット'] || '').split('|').map(&:strip).compact_blank.map do |ad_unit_name|
inventory_client.find_ad_unit_by_name(ad_unit_name)[:id]
end
inventory_targeting = { targeted_ad_units: ad_unit_ids.map { |id| { ad_unit_id: id, include_descendants: true } } }
order_id = order_client.find_order_by_name(row['オーダー名'])[:id]
line_item_client.create_line_item_datum(
name: row['名前'],
order_id: order_id,
creative_placeholders: creative_placeholders_from_row(row),
start_date_time: start_date_time,
end_date_time: end_date_time,
inventory_targeting: inventory_targeting,
cpm: row['CPM(円)'].to_f,
impressions: row['インプレッション数'].to_i
)
end
end
end
注目はcreate_line_item_datum関数で、CSVに記載されたGoogleAdManager::OrderクラスとGoogleAdManager::Inventryクラスを介してオーダー名と広告ユニット名からそのオーダーと広告ユニットのIDを取得しています。
プログラム上での工夫としては、少ない取得回数で済むように最初に全ての広告ユニットの行とオーダー名を元に利用予定のオーダーと広告ユニットオブジェクトを取得し、2度以上オーダーや広告ユニットを使う場合でも一回の呼び出しで済むようにしています。
def create_line_item_datum(row)
start_date_time = Time.parse(row['開始時間'])
end_date_time = Time.parse(row['終了時間'])
ad_unit_ids = (row['広告ユニット'] || '').split('|').map(&:strip).compact_blank.map do |ad_unit_name|
inventory_client.find_ad_unit_by_name(ad_unit_name)[:id]
end
inventory_targeting = { targeted_ad_units: ad_unit_ids.map { |id| { ad_unit_id: id, include_descendants: true } } }
order_id = order_client.find_order_by_name(row['オーダー名'])[:id]
line_item_client.create_line_item_datum(
name: row['名前'],
order_id: order_id,
creative_placeholders: creative_placeholders_from_row(row),
start_date_time: start_date_time,
end_date_time: end_date_time,
inventory_targeting: inventory_targeting,
cpm: row['CPM(円)'].to_f,
impressions: row['インプレッション数'].to_i
)
end
以下の実装でメインプログラムを実装すると、実際に広告申込が作成できます。
試験的に作る際によくある失敗として以下のものがあります。事前に注意しましょう。
- 広告申込の開始時間が作成しようとした時間よりも過去である
- 広告申込の開始時間が終了時間より後である
- 既に存在する申込名が存在する
まとめ
非常に難解なGAMの広告申込ですが、ある程度作る広告申込の形が決まっているならこのレベルまでは自動化することができます。
クリエイティブデータも一緒にアップロードできるようにすると、本当に配信できてしまうのでより自動化してみたい人は是非挑戦してみてください。