Google Ads APIでキーワードプランナーの検索ボリューム・CPC・競合性を取得する

# はじめに
以前、AdWords APIを使ってキーワードプランナーのデータを取得する、というシステムを作ったのですが、
最近AdWords APIが終了する（？）という噂を耳にし、慌てて新しいGoogle Ads APIを試してみることになりました。

Google Ads APIの公式サイトはこちら
[Google Ads API Beta](https://developers.google.com/google-ads/api/docs/start)
※ 執筆時点（2020年8月3日時点）でまだBetaは取れていないようです。

前回記事はこちら
[AdWords APIでキーワードプランナーの検索ボリューム・CPC・競合性を取得する](https://qiita.com/zak_y/items/58e07ef042605b113f86)

# キーワードプランナーのデータを取得するまで
前回と同様、以下の流れで説明します。

1. Google Ads APIを利用するための準備
2. Google Ads API Client Library for Python のインストール
3. OAuth2 認証の設定
4. APIで検索ボリューム・CPC・競合性の取得

## 1. Google Ads APIを利用するための準備
Google Ads APIは無料で利用できますが、利用にあたってはMCCアカウントと開発者トークンの申請/承認手続きが必要です。
前回記事で取得しているため、ここでは省略します。
・[Google Ads API 公式（クイックスタート）](https://developers.google.com/google-ads/api/docs/first-call/overview)
・[前回記事はこちら](https://qiita.com/zak_y/items/58e07ef042605b113f86#1-adwords-api%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99)

## 2. Google Ads API Client Library for Python のインストール
※ 既にインストール済みであればこの項は不要です。

今回はGoogleが提供しているAPIクライアントライブラリ（Python）を使って取得してみます。
こちらを参考にインストールしておきます。
[Google Ads API Beta > クライアントライブラリを取得する](https://developers.google.com/google-ads/api/docs/first-call/get-client-lib#python)
具体的には下記コマンドで入ります（pip）

```sh:
$ pip install google-ads
```

※ 本記事で検証している`google-ads`のバージョンは`6.0.0`です。
※ Pythonのバージョンは`3.7`以上

## 3. OAuth2 認証の設定
前回記事で設定しているため、ここでは省略します。
・[Google Ads API（クイックスタート）](https://developers.google.com/google-ads/api/docs/first-call/oauth-cloud-project)
・[前回記事はこちら](https://qiita.com/zak_y/items/58e07ef042605b113f86#3-oauth2-%E8%AA%8D%E8%A8%BC%E3%81%AE%E8%A8%AD%E5%AE%9A)
※ 前回のAdWords APIとは変わって、今回の設定ファイルは`google-ads.yaml`です。
※ `google-ads.yaml`はこちらを参考にしました。
　 https://github.com/googleads/google-ads-python/blob/master/google-ads.yaml
※ Google Ads APIを有効にする必要があります。
　 Google Ads APIは、Google APIコンソールでAPIライブラリを広告に絞ると表示されます。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270785/00eca1b9-c374-48db-b539-9bc5389ba7e2.png)
![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270785/c4a5fb71-13c8-1752-b4b5-06871f61c1fc.png)


## 4. APIで検索ボリューム・CPC・競合性の取得
処理の流れをざっくり説明します。
公式の説明はここにあります。
https://developers.google.com/google-ads/api/docs/keyword-planning/generate-historical-metrics
キーワードプランナーのデータを取得するためには`KeywordPlanService.GenerateHistoricalMetrics`を使う必要があります。
このAPIでは、流れとして
1. キーワードプラン（`KeywordPlan`, `KeywordPlanCampaigns`, `KeywordPlanAdGroups`, `KeywordPlanKeywords`, and `KeywordPlanNegativeKeywords`.）の作成
2. 過去の指標データ（`KeywordPlanService.GenerateHistoricalMetrics`）を取得
となるので、
まずは公式を参考にキーワードプランの作成部を起こし、
※ ここは公式のサンプルを参考にしました。`KeywordPlanNegativeKeywords`については今回は不要だと考えて省いてます。
その後過去の指標データを取得する部分を作ります。
※ 過去の指標データ（Historical Metrics）を取得するサンプルがないため、予測データ（Forecast Metrics）を取得するサンプルを参考にして過去の指標データ取得に書き換えます。

以下が完成したサンプルコードです（Python3.7）

```py:get_historical_metrics.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import sys
import uuid

from google.ads.google_ads.client import GoogleAdsClient
from google.ads.google_ads.errors import GoogleAdsException

ONE_MILLION = 1.0e6

def main(client, customer_id, target_keywords):
    # キーワードプランの作成
    try:
        resource_name = add_keyword_plan(client, customer_id, target_keywords)
    except GoogleAdsException as ex:
        print(f'Request with ID "{ex.request_id}" failed with status '
              f'"{ex.error.code().name}" and includes the following errors:')
        for error in ex.failure.errors:
            print(f'\tError with message "{error.message}".')
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print(f'\t\tOn field: {field_path_element.field_name}')
        sys.exit(1)

    # 過去の指標データを取得
    keyword_plan_service = client.get_service('KeywordPlanService')
    keyword_competition_level_enum = client.get_type('KeywordPlanCompetitionLevelEnum', version='v4').KeywordPlanCompetitionLevel

    try:
        response = keyword_plan_service.generate_historical_metrics(resource_name)
    except GoogleAdsException as ex:
        print('Request with ID "{}" failed with status "%s" and includes the '
              'following errors:'.format(ex.request_id, ex.error.code().name))
        for error in ex.failure.errors:
            print('\tError with message "{}".'.format(error.message))
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print('\t\tOn field: {}'.format(field_path_element.field_name))
        sys.exit(1)

    results = []
    for i, historical in enumerate(response.metrics):
        metrics = historical.keyword_metrics
        results.append({
            'keyword':              historical.search_query.value,
            'avg_monthly_searches': metrics.avg_monthly_searches.value,
            'competition':          keyword_competition_level_enum.Name(metrics.competition),
            'competition_index':    metrics.competition_index.value,
            'low_top_of_page_bid':  metrics.low_top_of_page_bid_micros.value / ONE_MILLION,
            'high_top_of_page_bid': metrics.high_top_of_page_bid_micros.value / ONE_MILLION
        })

    print(results)


def add_keyword_plan(client, customer_id, target_keywords):
    keyword_plan = create_keyword_plan(client, customer_id)
    keyword_plan_campaign = create_keyword_plan_campaign(client, customer_id, keyword_plan)
    keyword_plan_ad_group = create_keyword_plan_ad_group(client, customer_id, keyword_plan_campaign)
    create_keyword_plan_ad_group_keywords(client, customer_id, keyword_plan_ad_group, target_keywords)

    return keyword_plan

def create_keyword_plan(client, customer_id):
    operation = client.get_type('KeywordPlanOperation', version='v4')
    keyword_plan = operation.create

    keyword_plan.name.value = (f'Keyword plan for traffic estimate {uuid.uuid4()}')

    forecast_interval = client.get_type('KeywordPlanForecastIntervalEnum', version='v4').NEXT_MONTH
    keyword_plan.forecast_period.date_interval = forecast_interval

    keyword_plan_service = client.get_service('KeywordPlanService', version='v4')
    response = keyword_plan_service.mutate_keyword_plans(customer_id, [operation])
    resource_name = response.results[0].resource_name

    print(f'Created keyword plan with resource name: {resource_name}')

    return resource_name


def create_keyword_plan_campaign(client, customer_id, keyword_plan):
    operation = client.get_type('KeywordPlanCampaignOperation', version='v4')
    keyword_plan_campaign = operation.create

    keyword_plan_campaign.name.value = f'Keyword plan campaign {uuid.uuid4()}'
    # 上限クリック単価は画面で作成した時のデフォルト値（260円）で設定してみる
    # この設定はキャンペーンではなくて広告グループや個々のキーワード毎でも設定できる
    keyword_plan_campaign.cpc_bid_micros.value = 260000000
    keyword_plan_campaign.keyword_plan.value = keyword_plan

    keyword_plan_network = client.get_type('KeywordPlanNetworkEnum', version='v4')
    network = keyword_plan_network.GOOGLE_SEARCH
    keyword_plan_campaign.keyword_plan_network = network

    geo_target = client.get_type('KeywordPlanGeoTarget', version='v4')
    # 2392:日本
    geo_target.geo_target_constant.value = 'geoTargetConstants/2392'
    keyword_plan_campaign.geo_targets.extend([geo_target])

    language = client.get_type('StringValue', version='v4')
    # 1005:日本語
    language.value = 'languageConstants/1005'
    keyword_plan_campaign.language_constants.extend([language])

    keyword_plan_campaign_service = client.get_service('KeywordPlanCampaignService', version='v4')
    response = keyword_plan_campaign_service.mutate_keyword_plan_campaigns(customer_id, [operation])

    resource_name = response.results[0].resource_name

    print(f'Created keyword plan campaign with resource name: {resource_name}')

    return resource_name


def create_keyword_plan_ad_group(client, customer_id, keyword_plan_campaign):
    operation = client.get_type('KeywordPlanAdGroupOperation', version='v4')
    keyword_plan_ad_group = operation.create

    keyword_plan_ad_group.name.value = f'Keyword plan ad group {uuid.uuid4()}'
    #keyword_plan_ad_group.cpc_bid_micros.value = 2500000
    keyword_plan_ad_group.keyword_plan_campaign.value = keyword_plan_campaign

    keyword_plan_ad_group_service = client.get_service('KeywordPlanAdGroupService', version='v4')
    response = keyword_plan_ad_group_service.mutate_keyword_plan_ad_groups(customer_id, [operation])

    resource_name = response.results[0].resource_name

    print(f'Created keyword plan ad group with resource name: {resource_name}')

    return resource_name


def create_keyword_plan_ad_group_keywords(client, customer_id, plan_ad_group, target_keywords):
    match_types = client.get_type('KeywordMatchTypeEnum', version='v4')

    keywords = []
    for target_keyword in target_keywords:
        keyword_plan_ad_group_keyword = client.get_type('KeywordPlanAdGroupKeyword', version='v4')
        keyword_plan_ad_group_keyword.text.value = target_keyword
        #keyword_plan_ad_group_keyword.cpc_bid_micros.value = 2000000
        keyword_plan_ad_group_keyword.match_type = match_types.BROAD
        keyword_plan_ad_group_keyword.keyword_plan_ad_group.value = plan_ad_group
        keywords.append(keyword_plan_ad_group_keyword)

    operations = []
    for keyword in keywords:
        operation = client.get_type('KeywordPlanAdGroupKeywordOperation', version='v4')
        operation.create.CopyFrom(keyword)
        operations.append(operation)

    keyword_plan_ad_group_keyword_service = client.get_service('KeywordPlanAdGroupKeywordService', version='v4')

    response = (keyword_plan_ad_group_keyword_service.mutate_keyword_plan_ad_group_keywords(customer_id, operations))

    for result in response.results:
        print('Created keyword plan ad group keyword with resource name: '
              f'{result.resource_name}')


if __name__ == '__main__':
    # GoogleAdsClient will read the google-ads.yaml configuration file in the
    # home directory if none is specified.
    google_ads_client = GoogleAdsClient.load_from_storage()

    parser = argparse.ArgumentParser(description='Creates a keyword plan for specified customer.')
    # The following argument(s) should be provided to run the example.
    parser.add_argument('-c', '--customer_id', type=str, required=True, help='The Google Ads customer ID.')
    args = parser.parse_args()

    target_keywords = ['ダイエット', '筋トレ', 'グルテンフリー']

    main(google_ads_client, args.customer_id, target_keywords)
```

※ ここでは「ダイエット」「筋トレ」「グルテンフリー」というキーワードでキーワードプランナーのデータを取得しています。
※ `google-ads.yaml`はホームディレクトリに配置している想定です。場所を指定する場合は`GoogleAdsClient.load_from_storage()`の引数にパスを指定します。
※ 上限クリック単価は画面で作成した時のデフォルト値（260円）で設定
※ APIでは、`low_top_of_page_bid`（低額帯CPC）と`high_top_of_page_bid`（高額帯CPC）は単位がmicroのため、1000000で割っています。

# おわりに
Google広告に関するAPIは刷新され、今後はGoogle Ads APIに統一される流れのようです。
今回 AdWords API で行っていたキーワードプランナーのデータ取得を Google Ads API 移行してみましたが、APIだけでなくデータ項目等も変わっていて少し戸惑いました。
特にCPCが今回から低額帯と高額帯の2つに分かれており（キーワードプランナーの画面もそうなっている）今まで AdWords API で取得していた平均CPCとの整合性はどうすべきか。
低額帯と高額帯を単純に2つの平均で計算するか、もしくは今回使った過去の指標データ（Historical Metrics）ではなく予測データ（Forecast Metrics）のCPCを使うか。
時間ができればそのあたりも調べてみようと思っています。