概要 今回はGoogle Search Console Search Analytics APIをPythonから叩いて、自分のブログの各URLがどんなキーワードで検索流入してきたかをCSVに出力するツール を作ったので紹介していきます。
ブログを運営してると「どの記事がどんなキーワードで読まれてるんだろう?」って気になりますよね(^^
Search Consoleの管理画面でも見れるんですが、URLごとに一覧で出したい・CSVで持っておきたい・スクリプトで定期取得したい みたいな欲が出てくるはず。
そこで、完全無料・日次叩き放題・OAuth認証で安全 なSearch Console APIを使ったツールを作りました。
ちなみに最初は別のAPI(Custom Search JSON API)で実装しようとしてハマったので、その失敗談も含めて紹介します(- -; それではやっていきましょう!
目次
当初の構成と挫折ポイント(Custom Search API編) 最初はGoogle Custom Search JSON APIで「site:URL」クエリを投げてインデックス確認しよう と考えていました。
無料枠が100クエリ/日もあって80記事の日次チェックには十分。 GCPプロジェクトを作って、APIを有効化して、APIキーを発行して、CSE(Programmable Search Engine)も設定して…と一通り準備したんですが、いざ呼び出すと毎回これが返ってくる状態に。
エラーレスポンス
1 2 3 4 5 6 7 { "error" : { "code" : 403 , "message" : "This project does not have the access to Custom Search JSON API." , "status" : "PERMISSION_DENIED" } }
GCP割り当てとシステム上限の設定(Custom Search APIを100/日に制限)
APIキー作成時にCustom Search APIをちゃんと選択している
新規プロジェクトTESTPJのAPIダッシュボード(Custom Search APIは有効化済み)
プロジェクトもキーも作り直し、APIも有効化済み。 それでも弾かれる。
調べてみたら、2026年1月以降、Googleが新規GCPプロジェクト/組織からのCustom Search JSON APIアクセスに制限をかけており、設定が完璧でも事実上ブロックされている 状態でした。
これは結構罠で、設定方法を解説してる古い記事を見ながら作業すると延々ハマります。Custom Search JSON APIを今から新規導入するのは現実的に厳しいので、別の方法を検討しましょう。
このAPIは長年の定番だったようで、記事もこれが結構多かった印象です。公式アナウンスはあまり見つからず、なんならAPIキーの発行までできる。 かなり時間をロスしたので、普通に危険です(- -;
採用したGoogle Search Console Search Analytics API 方針転換して、Google Search ConsoleのSearch Analytics API を使うことにしました。
このAPIでできること
<返してくれるデータ>
過去N日間に自サイトに流入してきた検索クエリ
そのクエリで自サイトのどのURLが何位に表示されたか
表示回数 / クリック数 / CTR
メリット
<採用理由>
完全無料、クォータも実質叩き放題(1分あたり1200リクエストまで)
自分のサイトのデータなので、Custom Searchみたいなアクセス制限が無い
OAuth 2.0認証なので安全
デメリット
このAPIは「実際にユーザがGoogleに入力したクエリ 」の履歴を返すだけです。
例えば記事タイトル丸ごとを検索する人はほぼいないので、「タイトルでGoogle検索したら自分のURLが出るか?」のライブ確認はできません。
あくまで「過去にどんな検索ワードで流入があったか」を見るツールです。
必要なもの
<事前準備リスト>
Search Consoleで対象サイトを所有者として登録済み
GCPプロジェクト(HOGEPJなど任意の名前で新規作成可)
Pythonの実行環境(3.7以上)
セットアップ手順 Search Console APIを有効化する GCPコンソールで対象プロジェクトを選択して、「Google Search Console API」を有効化 します。
console.cloud.google.comhttps://console.cloud.google.com/apis/library/searchconsole.googleapis.com
手順はCustom Search APIと同じですが、普通に動くので安心ください!
OAuthクライアントIDを作成する 「認証情報を作成」→「OAuth クライアント ID 」を選択。
ここで間違えやすいポイントが1つあります。 「認証情報の作成」ウィザードを使うと「ユーザーデータ」「アプリケーションデータ」の2択を聞かれますが、どちらを選んでも今回欲しい「APIキー以外のOAuthクライアント」は作れません 。
このウィザードはAPIキー作成画面とは別物
ウィザードはキャンセルして、認証情報画面の「+ 認証情報を作成」ボタンから「OAuth クライアント ID」を直接選びましょう 。 種類は「デスクトップ アプリ」 を選択します。
作成したらJSONをダウンロードして、ファイル名を client_secret.json にリネーム してプロジェクトディレクトリに配置します。
OAuth同意画面のテストユーザー登録 個人開発で「テスト」モードのまま使う場合、アクセスする自分のGoogleアカウントを「テストユーザー」に追加 する必要があります。
これをやらないと、認証時にこんな画面が出てアクセスを拒否されます。
テストユーザー未登録だとアクセスがブロックされる
console.cloud.google.comOAuth同意画面の設定ページ を開いて、「対象」タブの「テストユーザー」セクション から自分のGoogleアカウントを追加すればOKです。
最初「テストユーザー」セクションが見つからずに迷ったんですが、新UIだと「対象」タブの中にあります。「OAuth同意画面」のタブだけ見ても無いので注意。
ツールの構成 ファイル構成はこんな感じにしました。
<ディレクトリ構成>
main.py
requirements.txt
.env
SC_SITE(対象サイトのSearch Console識別子)を記述
targets.txt
client_secret.json
GCPからダウンロードしたOAuthクライアント情報
token.json
results/
requirements.txt requirements.txt
1 2 3 4 google-api-python-client google-auth google-auth-oauthlib python-dotenv
.env .env
1 2 3 SC_SITE=sc-domain:[サイトドメイン] # 任意:取得期間(既定7日) # LOOKBACK_DAYS=7
SC_SITEの値はSearch Consoleのプロパティ種別で変わります。
ドメインプロパティなら「sc-domain:example.com」、URLプレフィックスなら「https://example.com/」のように指定してください。
targets.txt シンプルに調べたいURLを並べるだけ。
targets.txt
1 2 3 4 # コメント可、空行は無視 https://shinpinoshi.com/engineering/windows/tool/task/ https://shinpinoshi.com/engineering/python/serpApi/ https://shinpinoshi.com/money/point/lindt/
スクリプト 本体は1ファイルで完結させました。OAuth認証→Search Analytics API呼び出し→CSV出力 の流れです。
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 """Google Search Console Search Analytics APIで URLごとの実流入クエリと順位を取得する。 """ import csvimport datetimeimport osimport sysfrom dotenv import load_dotenvfrom google.auth.transport.requests import Requestfrom google.oauth2.credentials import Credentialsfrom google_auth_oauthlib.flow import InstalledAppFlowfrom googleapiclient.discovery import buildfrom googleapiclient.errors import HttpErrorif hasattr (sys.stdout, "reconfigure" ): sys.stdout.reconfigure(encoding="utf-8" ) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TARGETS_PATH = os.path.join(SCRIPT_DIR, "targets.txt" ) RESULTS_DIR = os.path.join(SCRIPT_DIR, "results" ) CLIENT_SECRET_PATH = os.path.join(SCRIPT_DIR, "client_secret.json" ) TOKEN_PATH = os.path.join(SCRIPT_DIR, "token.json" ) SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly" ] DEFAULT_LOOKBACK_DAYS = 7 ROW_LIMIT = 25000 def get_credentials (): creds = None if os.path.exists(TOKEN_PATH): creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) if creds and creds.valid: return creds if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) with open (TOKEN_PATH, "w" , encoding="utf-8" ) as f: f.write(creds.to_json()) return creds flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_PATH, SCOPES) creds = flow.run_local_server(port=0 ) with open (TOKEN_PATH, "w" , encoding="utf-8" ) as f: f.write(creds.to_json()) return creds def normalize_url (url ): return url.rstrip("/" ).lower() def fetch_search_analytics (service, site_url, start_date, end_date ): rows = [] start_row = 0 while True : body = { "startDate" : start_date, "endDate" : end_date, "dimensions" : ["query" , "page" ], "rowLimit" : ROW_LIMIT, "startRow" : start_row, } resp = service.searchanalytics().query(siteUrl=site_url, body=body).execute() chunk = resp.get("rows" , []) rows.extend(chunk) if len (chunk) < ROW_LIMIT: break start_row += ROW_LIMIT return rows def main (): load_dotenv(os.path.join(SCRIPT_DIR, ".env" )) site_url = os.getenv("SC_SITE" ) lookback = int (os.getenv("LOOKBACK_DAYS" , DEFAULT_LOOKBACK_DAYS)) today = datetime.date.today() start_date = (today - datetime.timedelta(days=lookback)).isoformat() end_date = today.isoformat() with open (TARGETS_PATH, encoding="utf-8" ) as f: urls = [ line.strip() for line in f if line.strip() and not line.lstrip().startswith("#" ) ] creds = get_credentials() service = build("searchconsole" , "v1" , credentials=creds, cache_discovery=False ) rows = fetch_search_analytics(service, site_url, start_date, end_date) page_lookup = {} for row in rows: query, page = row["keys" ] page_lookup.setdefault(normalize_url(page), []).append({ "query" : query, "position" : row.get("position" ), "impressions" : row.get("impressions" , 0 ), "clicks" : row.get("clicks" , 0 ), "ctr" : row.get("ctr" , 0 ), }) os.makedirs(RESULTS_DIR, exist_ok=True ) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S" ) out_path = os.path.join(RESULTS_DIR, f"{timestamp} _ranking.csv" ) with open (out_path, "w" , encoding="utf-8-sig" , newline="" ) as f: writer = csv.writer(f) writer.writerow(["URL" , "クエリ" , "順位" , "表示回数" , "クリック数" , "CTR(%)" ]) for url in urls: recs = page_lookup.get(normalize_url(url), []) if not recs: writer.writerow([url, "(流入なし)" , "" , 0 , 0 , "" ]) continue recs.sort(key=lambda r: -r["impressions" ]) for r in recs: writer.writerow([ url, r["query" ], f"{r['position' ]:.1 f} " , int (r["impressions" ]), int (r["clicks" ]), f"{r['ctr' ] * 100 :.2 f} " , ]) print (f"結果: {out_path} " ) if __name__ == "__main__" : main()
ROW_LIMITの25000は1リクエストあたりの上限です。それを超える行数があれば自動でページング取得します。個人ブログでは1回で取り切れるはず。
実行と結果 セットアップが終わったら、初回だけブラウザが開いて認証画面が出ます 。
コマンド
1 2 pip install -r requirements.txt python main.py
許可すればtoken.jsonが保存されて、以降はリフレッシュトークンで自動更新してくれるので、無人実行も可能になります。
実行結果のCSVはこんな感じ。
出力CSV例
1 2 3 4 5 6 7 URL,クエリ,順位,表示回数,クリック数,CTR(%) https://shinpinoshi.com/money/point/vpointget/,ウエルシア vポイント 使える 2026,9.0,1,0,0.00 https://shinpinoshi.com/engineering/systemwalker/command/,jobschprintcsv,2.5,8,2,25.00 https://shinpinoshi.com/engineering/systemwalker/command/,jobschcontrol,4.0,5,1,20.00 https://shinpinoshi.com/introduction/3coins/chair/,スリコ 折りたたみ 椅子 たたみ 方,7.0,3,1,33.33 https://shinpinoshi.com/money/point/lindt/,リンツ 会員登録 500円,7.8,4,2,50.00 https://shinpinoshi.com/travel/solo/isozaki/,(流入なし),,0,0,
URLごとに、過去7日間に流入してきたクエリ・順位・表示回数・クリック数・CTRが一覧で取れる ので、Excelで開けば各記事のSEOパフォーマンスを一目で把握できます。
「タイトルには入ってないけど実はこのキーワードで流入してるんだ」 みたいな発見があって、リライトのヒントになります。 たとえば自分の場合、engineering/systemwalker/command/ の記事はタイトルに無い「jobschprintcsv」「jobschcontrol」など個別コマンド名で上位ランクインしていることがわかりました。 これらは記事の見出しに昇格させたり、より詳しい解説を追加することで上位安定が狙えそうです(^^b
おまけ:ハマったときの確認ポイント 検索結果には1位なのに「流入なし」と出る Search Console Search Analytics APIは「実際にユーザがGoogleに入力したクエリ」しか返しません 。つまり、技術的には1位表示されてても、過去N日間にそのクエリで誰も検索していなければ「データなし」になります。
「ライブで順位をチェックしたい」用途には別途SerpAPIなどの実検索APIが必要です(^^;
エラー:403: access_denied OAuth同意画面が「テスト」モードで、アクセスするGoogleアカウントが「テストユーザー」に未登録 。 console.cloud.google.comOAuth同意画面 の「対象」タブから追加してください。
エラー:403 PERMISSION_DENIED “This project does not have the access to Custom Search JSON API” これはCustom Search APIで発生する別問題 。本記事のSearch Console APIでは出ません。 2026年1月以降の制限なので、Search Console APIに切り替えるのが現実的です。
締め 今回はSearch Console APIを使ってブログのキーワード流入を分析するPythonツール を紹介しました。
ポイントをおさらいすると以下のとおりです。
<まとめ>
Custom Search APIは2026年以降、新規プロジェクトから事実上使えない
Search Console Search Analytics APIなら無料・日次叩き放題でSEOデータが取れる
OAuthのテストユーザー登録だけ忘れずに
「タイトル外のキーワードで流入してる」発見はリライトの宝の山
データを定期的に取って眺めるだけでも、どの記事を強化すべきか が見えてきます。 SEO観察のお供にぜひ作ってみてください!
以上! APIの廃止などは設定するときにでかでかと書いてほしいですね(- -; 以上お疲れさまでした。