【Python】Search Console APIで自分のブログの流入キーワードをCSV出力するツールを作った

概要

今回は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/日に制限)
GCP Custom Search API クォータ設定画面
APIキー作成時にCustom Search APIをちゃんと選択している
GCP APIキー作成フォーム Custom Search API選択中
新規プロジェクトTESTPJのAPIダッシュボード(Custom Search APIは有効化済み)
GCP 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キー作成画面とは別物
GCP 認証情報の作成ウィザード ユーザーデータとアプリケーションデータの選択画面

ウィザードはキャンセルして、認証情報画面の「+ 認証情報を作成」ボタンから「OAuth クライアント ID」を直接選びましょう
種類は「デスクトップ アプリ」を選択します。

作成したらJSONをダウンロードして、ファイル名を client_secret.json にリネームしてプロジェクトディレクトリに配置します。

OAuth同意画面のテストユーザー登録

個人開発で「テスト」モードのまま使う場合、アクセスする自分のGoogleアカウントを「テストユーザー」に追加する必要があります。

これをやらないと、認証時にこんな画面が出てアクセスを拒否されます。

テストユーザー未登録だとアクセスがブロックされる
OAuth同意画面 アクセスをブロック テストユーザー未登録エラー

console.cloud.google.comOAuth同意画面の設定ページ を開いて、「対象」タブの「テストユーザー」セクションから自分のGoogleアカウントを追加すればOKです。

最初「テストユーザー」セクションが見つからずに迷ったんですが、新UIだと「対象」タブの中にあります。「OAuth同意画面」のタブだけ見ても無いので注意。

ツールの構成

ファイル構成はこんな感じにしました。

<ディレクトリ構成>
  • main.py
    • 本体スクリプト
  • requirements.txt
    • 依存パッケージ一覧
  • .env
    • SC_SITE(対象サイトのSearch Console識別子)を記述
  • targets.txt
    • チェック対象URLのリスト(1行1URL)
  • client_secret.json
    • GCPからダウンロードしたOAuthクライアント情報
  • token.json
    • 初回認証で自動生成されるトークン
  • results/
    • 実行ごとに日付付きCSVが出力される

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 csv
import datetime
import os
import sys

from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

if 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']:.1f}",
int(r["impressions"]), int(r["clicks"]),
f"{r['ctr'] * 100:.2f}",
])

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の廃止などは設定するときにでかでかと書いてほしいですね(- -;
以上お疲れさまでした。