2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GAMの広告申込をCSVからRubyを使って一括で作成してみる

Last updated at Posted at 2025-12-09

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のオブジェクトを取得します。
詳細はほぼ過去の記事で書いているので割愛します。

コード
lib/google_ad_manager/order.rb
# 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を定義しています。
固定値で良い部分はあえてハードコーディングし、変動する可能性のある値は引数や設定ファイルから取得できるようにしています。

コード
lib/google_ad_manager/line_item.rb
# 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
lib/google_ad_manager/line_item_importer.rb
# 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の広告申込ですが、ある程度作る広告申込の形が決まっているならこのレベルまでは自動化することができます。
クリエイティブデータも一緒にアップロードできるようにすると、本当に配信できてしまうのでより自動化してみたい人は是非挑戦してみてください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?