GoogleAdsAPI：キーワードリストのInvalidされる文字列の解決方法

# 内容構成

・結論
・はじめに
・キーワードプラン作成時に立ちはだかった正規表現の壁
・これ知ってたら沼にハマることなく、すぐに解決してたかも
・課題
・最後に

# 結論
忙しい方向けに、結論から書きます。
（Google Ads API v1２）

1.下記画像、キーワードプランナーの
「検索のボリュームと予測のデータを確認する」
の検索欄にキーワードのリストをコピペ＆「開始する」を押す

2.弾かれるキーワードをAPIに投げるキーワードリストから削除、あるいは修正を行う

![スクリーンショット 2023-06-02 12.15.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004597/d0c70e36-b90f-706f-ff4c-253585879633.png)
![スクリーンショット 2023-06-02 12.15.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004597/aaac8c64-3c32-7e8e-61a4-802aeadb15bd.png)

エラーとなる文字列を含むキーワードが入っていた場合、結果の出力を行わず、エラー文字列を弾いてまとめてくれます。

![スクリーンショット 2023-06-02 12.58.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004597/53dd90cb-2662-1a0f-9aaf-9d195ddd392e.png)

尚、この検索欄には１万キーワード分まで入るので、GoogleAdsAPIの1日で投げられるキーワードリストの件数上限分、APIに投げる際に弾かれるキーワードを一括で調べることができます。

# はじめに
社内の広告運用を行う部署の方からの依頼を受けて、10万程度のキーワード検索におけるCPC単価とその周辺情報を取得することになりました。
APIでキーワードプランを作成時、キーワードの正規表現の仕様に苦しめられたので、そこでの奮闘と解決策をまとめたいと思います。
（APIを使用するのに使ったコード全体は記事の最後に記述します）

# キーワードプラン作成時に立ちはだかった正規表現の壁
例えば以下のキーワードリストを作成し、APIにキーワードプランの作成を行なったとします。
```
    target_keywords = ['ダイエット', '筋トレ', '黒﨑', "遊☆戯☆王"]
```
すると以下のエラーメッセージが表示されました。
```
FaultMessage: Keyword text has invalid characters or symbols.
Request with ID "cznjijaQ2mvivJ_-g96zaw" failed with status "INVALID_ARGUMENT" and includes the following errors:
	Error with message "Keyword text has invalid characters or symbols.".
		On field: operations
	Error with message "Keyword text has invalid characters or symbols.".
		On field: operations
```
一言で言うと、
「キーワードの文字列中に対応していない文字列があるから、キーワードプラン作れないよ」
とのことです。
更に厳しいのが、エラーが起こったとしても、その日にGoogleAdsAPIで作成できるキーワードプランのキーワード件数が読み取らせた分、減ってしまうと点です。（恐ろしや・・・）

ここで問題となってくるのが、エラーメッセージが教えてくれる内容が「エラーの内容」と、「そのエラーを引き起こしたキーワードが何個あるか」だけという点です。
別の言い方をすれば、僕が知りたい内容である「どのキーワードでエラーが起きたか」と「どの文字列が不適切だったか」を教えてくれないという点です。
今回の例はキーワード５つですが、実際は数百〜１万までのキーワードを投げるわけなので、キーワード一つずつ調べるのは流石にできません。

一度、GoogleAdsAPIの正規表現の仕様を調べることにしました。
以下、GoogleAdsAPIの公式ドキュメントエラー一覧のページ（https://developers.google.com/google-ads/api/docs/best-practices/common-errors）
![スクリーンショット 2023-06-02 13.40.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004597/b1026110-c0bd-78b3-53b2-b4ef6e4c5df2.png)
使えない正規表現が一例しか載っていない！？
対処方法が「ない」！？

ということわけで、自分なりにエラー文字列になりそうなものを片っ端からファイルにまとめたりもしました。（今思えば砂漠を彷徨うような途方もないアホなことをしていたと思います）
実際、絵文字や環境依存文字、多数の言語を考慮すると弾かれる文字列なんてとんでもない数あるから、Google側も書き切れるわけがないですよね。

そこで「APIで弾いているからには、ウェブ上の検索欄のところでも何かしらの処理が働いており、そこで非対応の文字列を弾いているはずだ！」と思い、記事の最初にある「結論」の一時的な解決策に至りました。

ちなみに、デベロッパーツールでフロント側でキーワードの文字列に対して、どのような処理が働いているのか調べようとしましたが、うまくいきませんでした。

#これ知ってたら沼にハマることなく、すぐに解決してたかも
僕はシステム開発側の人間だったため、ウェブ上のキーワード入力でキーワードプランを作成するという前提がありませんでした。もし広告運用の部署であったら、過去にキーワードを入力して弾かれた経験や、その他仕様によって弾かれた経験があったはずです。その経験があれば、APIで弾かれた際に、「ウェブ上でも試してみるか」とすぐに解決策を導けていたかもしれません。
システム側の人間だからこそ解決に時間がかかってしまったのかな？とも思います。（そもそもこんなところで躓いているのは僕だけ説も・・・笑）

# 課題
APIに投げるデータの前処理の段階で、ウェブ上の検索欄にキーワードリストを貼り付け&検索するのは結構億劫です。うまい具合にreモジュールを使って前処理を行いたいですが・・・、現状いい解決策が見つからないです。
もし見つかったり、これからのアップデートでGoogleAdsAPI側から提供されたりしたら、それも記事にしたいです。

# 最後に
コンサル系の仕事だと、広告運用を取り扱う会社も多いと思います。また、僕のように広告運用を行う部署の方から、何十万単位のキーワードのCPC単価をまとめて欲しい等の依頼が入ることもあるのではないかと思います。
GoogleAdsAPIに限らず、データ取得にAPIを使う場合、広告運用の部署の方が直接APIを操作することは稀で、システム系部署との連携になると思います。
APIで行うからこそ、解決策を見つけるのに時間をとってしまうこともあるので、もしAPIの仕様で躓いたときは、APIを使わない場合や、デベロッパーツール等を使って、フロントサイドでどのような処理が働いているのかを調べてみるのも手だと思います。
広い視野って大切ですね！

拙い文章でしたが、読んでいただきありがとうございました！
以下、今回キーワードプランを作成することに使ったコードを置いておきます。
```
import argparse
import sys
import uuid
import re
import time

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

#ファイルを指定
input_path = '-----'
output_path = '-----'

# low_top_of_page_bidとhigh_top_of_page_bidの出力時の桁合わせに設定
ONE_MILLION = 1.0e6

def main(client, customer_id):

    # keyword_plan(キーワードプラン)の作成
    try:
        resource_name = add_keyword_plan(client, customer_id)
    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_id = re.sub(r"\D", "", resource_name.replace(customer_id, ""))
    time.sleep(5)  # 1秒毎に1クエリの制限用
    try:
        keyword_plan_service_historical_metrics(client, customer_id, keyword_plan_id)
    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_id)



def add_keyword_plan(client, customer_id):

    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)

    return keyword_plan


def create_keyword_plan(client, customer_id):

    keyword_plan_service = client.get_service("KeywordPlanService")
    operation = client.get_type("KeywordPlanOperation")
    keyword_plan = operation.create

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

    forecast_interval = (
        client.enums.KeywordPlanForecastIntervalEnum.NEXT_QUARTER)
    keyword_plan.forecast_period.date_interval = forecast_interval

    response = keyword_plan_service.mutate_keyword_plans(
        customer_id=customer_id, operations=[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):

    keyword_plan_campaign_service = client.get_service("KeywordPlanCampaignService")
    operation = client.get_type("KeywordPlanCampaignOperation")
    keyword_plan_campaign = operation.create

    keyword_plan_campaign.name = f"Keyword plan campaign {uuid.uuid4()}"
    keyword_plan_campaign.cpc_bid_micros = 260000000
    keyword_plan_campaign.keyword_plan = keyword_plan

    network = client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
    keyword_plan_campaign.keyword_plan_network = network

    geo_target = client.get_type("KeywordPlanGeoTarget")
    geo_target.geo_target_constant = "geoTargetConstants/2392"  # 2392:日本
    keyword_plan_campaign.geo_targets.append(geo_target)

    language = "languageConstants/1005"  # 1005:日本語
    keyword_plan_campaign.language_constants.append(language)

    response = keyword_plan_campaign_service.mutate_keyword_plan_campaigns(
        customer_id=customer_id, operations=[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")
    keyword_plan_ad_group = operation.create

    keyword_plan_ad_group.name = f"Keyword plan ad group {uuid.uuid4()}"
    # keyword_plan_ad_group.cpc_bid_micros = 250000000
    keyword_plan_ad_group.keyword_plan_campaign = keyword_plan_campaign

    keyword_plan_ad_group_service = client.get_service("KeywordPlanAdGroupService")
    response = keyword_plan_ad_group_service.mutate_keyword_plan_ad_groups(
        customer_id=customer_id, operations=[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):

    keyword_plan_ad_group_keyword_service = client.get_service("KeywordPlanAdGroupKeywordService")

    operations = []
    error_keyword_list = []
    math = 0  #キーワード件数取得用
    for target_keyword in target_keywords:
        operation = client.get_type("KeywordPlanAdGroupKeywordOperation")
        keyword_plan_ad_group_keyword = operation.create
        keyword_plan_ad_group_keyword.text = target_keyword
        keyword_plan_ad_group_keyword.match_type = (
            client.enums.KeywordMatchTypeEnum.EXACT)
        keyword_plan_ad_group_keyword.keyword_plan_ad_group = plan_ad_group
        operations.append(operation)
        math += 1
        print(math)  

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

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


def keyword_plan_service_historical_metrics(client, customer_id, keyword_plan_id):

    keyword_plan_service = client.get_service("KeywordPlanService")
    resource_name = keyword_plan_service.keyword_plan_path(customer_id, keyword_plan_id)
    response = keyword_plan_service.generate_historical_metrics(keyword_plan=resource_name)

    results_historical_metrics = []
    for metric in response.metrics:
        results_historical_metrics.append({
            'keyword':              metric.search_query,
            'avg_monthly_searches': metric.keyword_metrics.avg_monthly_searches,
            'competition_level':    metric.keyword_metrics.competition.name,
            'competition_index':    metric.keyword_metrics.competition_index,
            'low_top_of_page_bid':  metric.keyword_metrics.low_top_of_page_bid_micros / ONE_MILLION,
            'high_top_of_page_bid': metric.keyword_metrics.high_top_of_page_bid_micros / ONE_MILLION
        })
    print(results_historical_metrics)

    with open(output_path, 'w') as f:
        print(results_historical_metrics, file=f)


if __name__ == "__main__":

    googleads_client = GoogleAdsClient.load_from_storage("-----.yaml")

    parser = argparse.ArgumentParser(description="Creates a keyword plan for specified customer.")
    parser.add_argument("-c", "--customer_id", type=str, required=True, help="The Google Ads customer ID.",)
    args = parser.parse_args()

    target_keywords = ['ダイエット', '筋トレ', '黒﨑', "遊☆戯☆王"]


    main(googleads_client, args.customer_id)
```