
※この記事にはアフィリエイトリンクを含みます。
結論から言います。
Search Consoleは「見ようとしないと見ない」ツールです。
週1回手動で確認しようとしても、実際にはほとんど続きません。
ってことで、Pythonスクリプトで改善候補記事を自動抽出してMarkdownレポートに出力する仕組みを作りました。
一度セットアップすれば、コマンド1行で改善候補が出てくる状態になります。あとは1本ずつ直すだけです。
Contents
「とりあえず登録した」で終わっていた
WordPressでブログを続けていると、「SEOのためにとりあえず入れた」状態のツールが増えてきます。
Search Consoleもそのひとつでした。
登録はしていた。ダッシュボードは開いたことがある。でも何を見ればいいのかよくわからず、数字が増えても減っても、次のアクションが出てこない。
そういう状態が半年以上続いていました。
意識が低かったからではなかったと今は思っています。『見ても判断できない』から手が止まっていた、というのが正直なところです。
手動チェックが続かない理由
週1回、定期的に確認しようとした時期もありました。でも続きませんでした。
続かない理由は2つあります。
まず、『どこを見ればいいかを毎回判断するコストがある』こと。表示回数、クリック数、CTR、順位。4つある指標のどれを基準にするかを、毎回考える状態になっていました。
次に、『改善候補がどれかを自分で絞り込む必要がある』こと。記事が増えてくると、1本1本確認するのは現実的ではありません。
要は、「確認する前に判断が必要」な構造になっていたことが問題でした。
判断が先にあると、動けなくなります。
SEO改善が止まる4パターン
・『Search Consoleを登録したまま放置している』 ツールが「あるだけ」の状態。データは溜まっているが活かせていない。
・『ダッシュボードを眺めるだけで終わる』 数字は見ている。でも「で、どうすればいい?」が出てこない。
・『表示回数は増えているのにクリックが来ない』 Googleには評価されているが、タイトルやメタが読者に刺さっていない状態。気づきにくい。
・『週1確認しようとしたが3回で止まった』 「確認する前に判断が必要」な手順だと、思考が止まって続かない。
・表示されているのにクリックされていない記事が、今何本ありますか?
見るべき指標は2パターンだけ
Search Consoleで確認するのは4指標ですが、アクションにつながるのは2パターンだけです。
| 指標 | 意味 |
|---|---|
| 表示回数 | Googleの検索結果に出た回数 |
| クリック数 | 実際にクリックされた回数 |
| CTR(クリック率) | 表示されたうちクリックされた割合 |
| 掲載順位 | 平均何位に表示されているか |
『①表示回数が多いのにCTRが低い記事』は、Googleには評価されているのにタイトルやメタが刺さっていない状態です。タイトル改善が有効です。
『②掲載順位が11〜20位の記事』は、検索結果の2ページ目にいる記事です。記事を強化すれば1ページ目に上がる可能性があります。
この2パターンをスクリプトで自動抽出します。
セットアップ手順
セットアップは3ステップです。
まず、Google Cloud ConsoleでSearch Console APIを有効化します。プロジェクトを作成して「APIとサービスを有効化」からGoogle Search Console APIを選択します。認証情報でOAuthクライアントIDを作成するとき、同意画面のUser Typeは「外部」、アプリケーションの種類は「デスクトップアプリ」を選んでください。
ダウンロードしたJSONファイルは scripts/ に配置します。同意画面の「テストユーザー」に自分のGoogleアカウントを追加することも忘れずに。
次に、必要なライブラリをインストールします。
pip3 install google-auth google-auth-oauthlib google-api-python-client
最後に、スクリプトを scripts/search_console_report.py として保存します。
なお、パーマリンク設定を「基本」から変更すると既存URLがすべて変わります。運用中のサイトでは変更しないほうが安全です。
スクリプト全体
#!/usr/bin/env python3
import os
import re
import time
import urllib.request
from datetime import datetime, timedelta, timezone
from html.parser import HTMLParser
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
BASE_DIR = os.path.dirname(__file__)
CREDENTIALS_FILE = os.path.join(BASE_DIR, "client_secret_***.apps.googleusercontent.com.json")
TOKEN_FILE = os.path.join(BASE_DIR, "token.json")
SITE_URL = "https://yoursite.com/"
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
REPORT_DIR = os.path.join(BASE_DIR, "..", "posts", "report")
class TitleParser(HTMLParser):
def __init__(self):
super().__init__()
self._in_title = False
self.title = ""
def handle_starttag(self, tag, attrs):
if tag == "title":
self._in_title = True
def handle_endtag(self, tag):
if tag == "title":
self._in_title = False
def handle_data(self, data):
if self._in_title:
self.title += data
def fetch_title(url):
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=5) as res:
html = res.read().decode("utf-8", errors="ignore")
parser = TitleParser()
parser.feed(html)
title = parser.title.strip()
title = re.sub(r"\s*[|\-–]\s*.*$", "", title).strip()
return title or url
except Exception:
return url
def fetch_titles(urls):
titles = {}
for i, url in enumerate(urls):
titles[url] = fetch_title(url)
if i < len(urls) - 1:
time.sleep(0.5)
return titles
def get_credentials():
creds = None
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_FILE, "w") as f:
f.write(creds.to_json())
return creds
def fetch_page_data(service, days=28):
end_date = datetime.now(timezone.utc).date()
start_date = end_date - timedelta(days=days)
response = service.searchanalytics().query(
siteUrl=SITE_URL,
body={
"startDate": str(start_date),
"endDate": str(end_date),
"dimensions": ["page"],
"rowLimit": 500,
}
).execute()
return response.get("rows", [])
def analyze(rows):
low_ctr, borderline = [], []
for row in rows:
url = row["keys"][0]
clicks = row.get("clicks", 0)
impressions = row.get("impressions", 0)
ctr = row.get("ctr", 0) * 100
position = row.get("position", 0)
if impressions >= 50 and ctr < 3.0:
low_ctr.append({"url": url, "impressions": int(impressions), "clicks": int(clicks), "ctr": round(ctr, 1), "position": round(position, 1)})
if 11 <= position <= 20 and impressions >= 20:
borderline.append({"url": url, "impressions": int(impressions), "clicks": int(clicks), "ctr": round(ctr, 1), "position": round(position, 1)})
low_ctr.sort(key=lambda x: x["impressions"], reverse=True)
borderline.sort(key=lambda x: x["position"])
return low_ctr, borderline
def generate_report(low_ctr, borderline, titles):
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
lines = [
f"# Search Console 定点チェック ({today})", "",
"## CTR改善候補(表示多いのにクリック率低い)",
"表示回数50以上 & CTR3%未満", "",
"| 記事タイトル | 表示回数 | クリック | CTR | 順位 |",
"|------------|---------|--------|-----|------|",
]
for r in low_ctr[:20]:
title = titles.get(r["url"], r["url"])
lines.append(f"| [{title}]({r['url']}) | {r['impressions']} | {r['clicks']} | {r['ctr']}% | {r['position']} |")
lines += [
"", "## 圏外ギリ候補(11〜20位 → あと一押しで1ページ目)",
"表示回数20以上 & 掲載順位11〜20位", "",
"| 記事タイトル | 表示回数 | クリック | CTR | 順位 |",
"|------------|---------|--------|-----|------|",
]
for r in borderline[:20]:
title = titles.get(r["url"], r["url"])
lines.append(f"| [{title}]({r['url']}) | {r['impressions']} | {r['clicks']} | {r['ctr']}% | {r['position']} |")
return "\n".join(lines)
def main():
creds = get_credentials()
service = build("searchconsole", "v1", credentials=creds)
print("Search Consoleからデータ取得中...")
rows = fetch_page_data(service)
print(f"{len(rows)}件取得完了")
low_ctr, borderline = analyze(rows)
all_urls = list({r["url"] for r in low_ctr + borderline})
print(f"記事タイトル取得中({len(all_urls)}件)...")
titles = fetch_titles(all_urls)
report = generate_report(low_ctr, borderline, titles)
os.makedirs(REPORT_DIR, exist_ok=True)
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
report_path = os.path.join(REPORT_DIR, f"{today}_search_console_report.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(report)
print(f"レポート出力: {report_path}")
print(f"CTR改善候補: {len(low_ctr)}件 / 圏外ギリ候補: {len(borderline)}件")
if __name__ == "__main__":
main()
実行する
コマンド1行で動きます。
python3 scripts/search_console_report.py
初回はブラウザが開いてGoogleアカウントの認証が求められます。許可すると token.json が生成され、2回目以降は認証不要です。
実行後、posts/report/2026-04-20_search_console_report.md のようなレポートが出力されます。
# Search Console 定点チェック (2026-04-20)
## CTR改善候補(表示多いのにクリック率低い)
表示回数50以上 & CTR3%未満
| 記事タイトル | 表示回数 | クリック | CTR | 順位 |
|------------|---------|--------|-----|------|
| [記事タイトルA](https://...) | 187 | 1 | 0.5% | 7.7 |
| [記事タイトルB](https://...) | 82 | 1 | 1.2% | 18.0 |
## 圏外ギリ候補(11〜20位 → あと一押しで1ページ目)
...
認証ファイルはGitHubに上げないよう .gitignore に追加します。
token.json今日やること
client_secret*.json
自動化と組み合わせてやりたいこと
改善候補が出てくれば、次のステップは「タイトルを直す → 数字の変化を待つ」だけです。
その記録をGitHubで管理しておくと、どの記事をいつ改善したかが履歴として残ります。スクリプトで出したレポートをcommitするだけなので、手間はほとんどかかりません。
判断を減らせば動ける
Search Consoleが続かなかったのは、意識の問題ではありませんでした。
「見る前に判断が必要」な構造になっていたことが原因でした。
スクリプトで改善候補を自動抽出する形にすると、あとはそのリストの記事を1本ずつ直すだけになります。
判断を減らして、行動だけ残す。この構造が定点チェックを続けるコツです。
