【完全入門】Google Ads APIにハマりまくったのでめちゃくちゃ丁寧に説明する

# はじめに
このページに来た方はこれからGoogle Ads APIを始めようと思っている人かGoogle Ads APIを始めようとしたらハマってしまった人でしょう。
[公式リファレンス](https://developers.google.com/google-ads/api/docs/start)はあるにはありますが、**死ぬほど**わかりづらいので注意してください。
めちゃくちゃ詰まった挙句、光明を見出した筆者が懇切丁寧に説明していきます。

# アカウントについて
ここが一番の難所といっても過言ではありません。

正しいかは不安ですが、以下が私の分析結果です。
```
├── 本番用アカウント　# !注意　本番用アカウント≠テストには不要
│   └── MCCアカウント # マネージャーアカウント
└── テストアカウント
    ├── MCCアカウント # マネージャーアカウント
    └── 広告アカウント
```
ここの一番のミソは**本番用アカウントは本番用ではない**ということです。
さらに、**本番用アカウントの中にテストアカウントと本番アカウントが存在します。**

まず、テストアカウントは文字通りテスト用のアカウントであり、テストにしか使いません。
では本番用アカウントが本番にしか使わないといわれればそれは誤りであり、実際にはテストをするためだけであっても本番用アカウントとテストアカウントの二つを要します。厄介ですね。
構造としては、実際にAPIの窓口となるのが本番用アカウント、テスト用の広告を打つのがテストアカウントの広告アカウントであり、それを管理するのがテストアカウントのMCCアカウントという立ち位置です。（間違っていたら指摘してください）

ここで、本番用アカウントで開発者トークンを発行するのですが、**その際にアクセス権限がテストアカウントと本番用アカウントの二種類あります。**
紛らわしいことこの上ないです。
**なお、この記事で「本番用アカウント/テストアカウント」と言う際には基本的にはアクセス権限の話ではなく前述した二種のことだと思っていただいて差し支えありません。**

~~ちなみに、実際にGoogleアカウントが必要となるのは本番用アカウントとテストアカウントの二つであり、例えばテストアカウント配下のMCCアカウントと広告アカウントは一つのGoogleアカウントで管理可能です。~~
※（公式ドキュメントを読む限りは二つ必要みたいでしたが、一つでよさそうです。）

また、上図では書きませんでしたが、当然本番用アカウント配下にも広告アカウントは配置可能です。ただし、今回は使いません。
さらに、普段言語設定を英語にしている方はMCCアカウントが何なのか混乱するでしょうが、**MCCアカウント=Manager Account**であるので注意。

# まずは本番用アカウント発行から
[このページ](https://ads.google.com/home/tools/manager-accounts/)の「クライアントセンター(MCC)を作成」から作成できます。
適当に名前を決めて作成すれば良いです。
設定項目は他のアカウントを管理で問題ないはずです。

# 次にテストアカウントを発行
[このページ](https://developers.google.com/google-ads/api/docs/first-call/test-accounts)から「テスト用のMCCアカウントを作成する」を押します。~~公式サイトが役に立つ稀有な例。~~
これも適当に名前を決めて作成すれば良いです。
設定項目は他のアカウントを管理で問題ないはずです。
右上に赤色背景で「テストアカウント」と表示されていれば問題ありません。

# 開発者トークンを発行
**本番用アカウント**の方から、検索窓でAPIセンターを入力して選択。
開発者トークンを発行できます。
企業名、URL、企業の分類、APIの目的を適当に埋めて、利用規約にチェックすれば特に審査が挟まれることなく、即座に取得できます。

なお、この時点で取得できているのはアクセス権限を見ればわかる通り、**テストアカウント**です。先述した、テスト用の本番用アカウントです。
実際に業務運用する際には本番用の本番用アカウントにするために審査してもらう必要がありますが、今回はとりあえず動かしてみることを目標としているため掘り下げません。

# Googe Ads APIの有効化
晴れて開発者トークンを発行できたわけですが、まだまだ油断はできません。
厄介なことに、Google Ads APIの認証情報は全部で3つ必要になります。
気を取り直して2つ目の認証を突破しましょう。

開発者トークンを発行したGoogleアカウントから[GCP(Google Cloud Platform)](https://console.cloud.google.com/)にアクセスし、新しくプロジェクトを作成します。
作成するプロジェクトを選択して、検索窓から「Google Ads API」を選択、有効化。

次に左側の「OAuth同意画面」を押して適当に「アプリ名」「ユーザーサポートメール」「メールアドレス」を埋める(*がついてある項目)。保存して次へ。
スコープでは「スコープの追加」→Googe Ads APIを選択して、チェックボックスにチェックを入れて更新。保存して次へ。
テストユーザーに自分の今使っているGoogleアカウントを入力して、保存して次へ。
ダッシュボードへ戻る。

さらに左側の「🗝認証情報」を押して、上の「認証情報を作成」から「OAuthクライアントIDを作成」を選択。
アプリの種類を選択させられるので、「デスクトップアプリ」を選択して作成。
認証情報が表示されるため、JSON形式で保存。

これで認証その2が完了です。

# 更新トークンの取得
Google特製パズルの最後の1ピースがrefresh_tokenです。

先ほどのJSONを開いて、`client_id`と`redirect_uris`を確認して、
`https://accounts.google.com/o/oauth2/auth?client_id={client_idを入力}&redirect_uri={redirect_uri(http://から始まるもの)}&scope=https://www.googleapis.com/auth/adwords&access_type=offline&response_type=code`
のように入力する。
```
例: 
https://accounts.google.com/o/oauth2/auth?client_id=1234567890-thisissample1234567890.apps.googleusercontent.com&redirect_uri=http://localhost&scope=https://www.googleapis.com/auth/adwords&access_type=offline&response_type=code
```

Googleアカウントを選択させられるので、先ほどテストユーザーに追加したアカウント(今回では先ほどまで用いていたGoogleアカウント)を選択して、続行、続行。
**エラーを吐かれるかもしれませんが、URLさえ手に入れば問題ありません。**
`?code=...`と書かれている部分を確認しておきます。

MacのターミナルなりUbuntuのターミナルなりで、
`curl -d "client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}&grant_type=authorization_code&code={先ほどのURLから入手したcode}" https://accounts.google.com/o/oauth2/token`
を実行してください。

うまくゆけば、`refresh_token`が得られます。Windowsユーザーの方はWSLなりAWSなりを用いるか、下の「上手くいかない人へ」を参照してください。
なお、初回しか得られないため注意です。

これで認証情報は取得完了です。
お疲れさまでした。

<details><summary>上手くいかない人へ</summary><div>

以下のpythonファイルを以下のように実行してください。
```
python3 generate_user_credentials.py -c {JSONのパス}
```
```generate_user_credentials.py
#!/usr/bin/env python
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This example will create an OAuth2 refresh token for the Google Ads API.

This example works with both web and desktop app OAuth client ID types.

https://console.cloud.google.com

IMPORTANT: For web app clients types, you must add "http://127.0.0.1" to the
"Authorized redirect URIs" list in your Google Cloud Console project before
running this example. Desktop app client types do not require the local
redirect to be explicitly configured in the console.

Once complete, download the credentials and save the file path so it can be
passed into this example.

This example is a very simple implementation, for a more detailed example see:
https://developers.google.com/identity/protocols/oauth2/web-server#python
"""

import argparse
import hashlib
import os
import re
import socket
import sys
from urllib.parse import unquote

# If using Web flow, the redirect URL must match exactly what’s configured in GCP for
# the OAuth client.  If using Desktop flow, the redirect must be a localhost URL and
# is not explicitly set in GCP.
from google_auth_oauthlib.flow import Flow

_SCOPE = "https://www.googleapis.com/auth/adwords"
_SERVER = "127.0.0.1"
_PORT = 8080
_REDIRECT_URI = f"http://{_SERVER}:{_PORT}"


def main(client_secrets_path, scopes):
    """The main method, starts a basic server and initializes an auth request.

    Args:
        client_secrets_path: a path to where the client secrets JSON file is
          located on the machine running this example.
        scopes: a list of API scopes to include in the auth request, see:
            https://developers.google.com/identity/protocols/oauth2/scopes
    """
    flow = Flow.from_client_secrets_file(client_secrets_path, scopes=scopes)
    flow.redirect_uri = _REDIRECT_URI

    # Create an anti-forgery state token as described here:
    # https://developers.google.com/identity/protocols/OpenIDConnect#createxsrftoken
    passthrough_val = hashlib.sha256(os.urandom(1024)).hexdigest()

    authorization_url, state = flow.authorization_url(
        access_type="offline",
        state=passthrough_val,
        prompt="consent",
        include_granted_scopes="true",
    )

    # Prints the authorization URL so you can paste into your browser. In a
    # typical web application you would redirect the user to this URL, and they
    # would be redirected back to "redirect_url" provided earlier after
    # granting permission.
    print("Paste this URL into your browser: ")
    print(authorization_url)
    print(f"\nWaiting for authorization and callback to: {_REDIRECT_URI}")

    # Retrieves an authorization code by opening a socket to receive the
    # redirect request and parsing the query parameters set in the URL.
    code = unquote(get_authorization_code(passthrough_val))

    # Pass the code back into the OAuth module to get a refresh token.
    flow.fetch_token(code=code)
    refresh_token = flow.credentials.refresh_token

    print(f"\nYour refresh token is: {refresh_token}\n")
    print(
        "Add your refresh token to your client library configuration as "
        "described here: "
        "https://developers.google.com/google-ads/api/docs/client-libs/python/configuration"
    )


def get_authorization_code(passthrough_val):
    """Opens a socket to handle a single HTTP request containing auth tokens.

    Args:
        passthrough_val: an anti-forgery token used to verify the request
          received by the socket.

    Returns:
        a str access token from the Google Auth service.
    """
    # Open a socket at _SERVER:_PORT and listen for a request
    sock = socket.socket()
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((_SERVER, _PORT))
    sock.listen(1)
    connection, address = sock.accept()
    data = connection.recv(1024)
    # Parse the raw request to retrieve the URL query parameters.
    params = parse_raw_query_params(data)

    try:
        if not params.get("code"):
            # If no code is present in the query params then there will be an
            # error message with more details.
            error = params.get("error")
            message = f"Failed to retrieve authorization code. Error: {error}"
            raise ValueError(message)
        elif params.get("state") != passthrough_val:
            message = "State token does not match the expected state."
            raise ValueError(message)
        else:
            message = "Authorization code was successfully retrieved."
    except ValueError as error:
        print(error)
        sys.exit(1)
    finally:
        response = (
            "HTTP/1.1 200 OK\n"
            "Content-Type: text/html\n\n"
            f"<b>{message}</b>"
            "<p>Please check the console output.</p>\n"
        )

        connection.sendall(response.encode())
        connection.close()

    return params.get("code")


def parse_raw_query_params(data):
    """Parses a raw HTTP request to extract its query params as a dict.

    Note that this logic is likely irrelevant if you're building OAuth logic
    into a complete web application, where response parsing is handled by a
    framework.

    Args:
        data: raw request data as bytes.

    Returns:
        a dict of query parameter key value pairs.
    """
    # Decode the request into a utf-8 encoded string
    decoded = data.decode("utf-8")
    # Use a regular expression to extract the URL query parameters string
    match = re.search("GET\s\/\?(.*) ", decoded)
    params = match.group(1)
    # Split the parameters to isolate the key/value pairs
    pairs = [pair.split("=") for pair in params.split("&")]
    # Convert pairs to a dict to make it easy to access the values
    return {key: val for key, val in pairs}


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=(
            "Generates OAuth2 refresh token using the Web application flow. "
            "To retrieve the necessary client_secrets JSON file, first "
            "generate OAuth 2.0 credentials of type Web application in the "
            "Google Cloud Console (https://console.cloud.google.com). "
            "Make sure 'http://_SERVER:_PORT' is included the list of "
            "'Authorized redirect URIs' for this client ID."
        ),
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "-c",
        "--client_secrets_path",
        required=True,
        type=str,
        help=(
            "Path to the client secrets JSON file from the Google Developers "
            "Console that contains your client ID, client secret, and "
            "redirect URIs."
        ),
    )
    parser.add_argument(
        "--additional_scopes",
        default=None,
        type=str,
        nargs="+",
        help="Additional scopes to apply when generating the refresh token.",
    )
    args = parser.parse_args()

    configured_scopes = [_SCOPE]

    if args.additional_scopes:
        configured_scopes.extend(args.additional_scopes)

    main(args.client_secrets_path, configured_scopes)
```
</div></details>

# 実際にAPIを叩いてみる
やっと本題です。大変でしたね。
以下に動作確認がてらPythonのサンプルコードを載せておくので参考にしてください。
pythonファイルに引数を渡すのを忘れないようにしてください。
また、`google-ads.yaml`はホームディレクトリに配置してください。
エラーが出なければ完了です。
それでは。
```
pip install google-ads
```

```~/google-ads.yaml
developer_token: {開発者トークン}
use_proto_plus: true
client_id: {client_id(JSONにあるやつ)}
client_secret: {client_secret(JSONにあるやつ)}
refresh_token: {更新トークン}
login_customer_id: {テストアカウントページの左上の数字(例：12-345-6789)}
```
```
python3 get_campaigns.py -c {login_customer_idと同じ}
```
```get_campaigns.py
#!/usr/bin/env python
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This example illustrates how to get all campaigns.
To add campaigns, run add_campaigns.py.
"""


import argparse
import sys

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


def main(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    query = """
        SELECT
          campaign.id,
          campaign.name
        FROM campaign
        ORDER BY campaign.id"""

    # Issues a search request using streaming.
    stream = ga_service.search_stream(customer_id=customer_id, query=query)

    for batch in stream:
        for row in batch.results:
            print(
                f"Campaign with ID {row.campaign.id} and name "
                f'"{row.campaign.name}" was found.'
            )


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

    parser = argparse.ArgumentParser(
        description="Lists all campaigns 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()

    try:
        main(googleads_client, args.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)
```

# 参考
https://developers.google.com/google-ads/api/docs/start
https://github.com/googleads/google-ads-python
https://qiita.com/akaaariiiiin/items/7db3d40ca5b92610d448
https://www.youtube.com/watch?v=HXKpfGqPRy0
https://zenn.dev/lsii/scraps/4c25ace2a39457
https://programmer-life.work/python/google-ads-api-execute-python