1 min read · 295 words
活用法 / ブログ運営 / Python・自動化
約2,500字
Bloggerのデメリットの一つは、テーマのXMLを変更するたびに、管理画面のUIで「テーマ → バックアップ/復元 → アップロード」と3〜4回クリックし、さらにログイン確認と適用待ちまで含めると、1回につき5分近くかかってしまう点です。私たちはこれをPlaywright + Chrome 9222 CDPで自動化し、手動クリックを0回に減らしました。作成した意図や実際の動作, 効果、検証までをそのまま解説します。
開発した理由
テーマの修正作業は、頻繁に行う時期には1日に5〜10回発生します。色を1行、フォントの太さを1行、サイドバーの位置を1行変更して、本番環境(ライブ)で結果を確認するまでに毎回5分もかかっていては、作業のフローが途切れてしまいます。
また、Bloggerの古い「テーマの復元」ルートには、BTP(以前のサイト名)のような第1世代のClassic modeトリガーが存在し、一度押し間違えるとサイト全体のレイアウトが古い第1世代に逆戻りしてしまいました。私たちはこの現象を2回再現してしまった後、「復元」ルートをコード上で永久に遮断しました。安全なルートはただ一つ、管理画面の「現在のテーマのソースコードを編集」画面(CodeMirrorエディタ)にsetValueでXML全体を直接流し込む方法です。
この一連の流れを毎回手動で行わずに済むよう、自動化モジュールを作成しました。
動作原理
核心となるのは、ユーザーがすでに9222ポートで起動しているChromeにPlaywrightを接続(CDP attach)し、コマンドを送信する仕組みです。新しいブラウザを別途起動することはありません。そうすることで、Googleログインがすでに維持されているため、CAPTCHAや2段階認証(2FA)を自動的に突破できます。
手順:
- Chrome 9222の起動確認 —
http://127.0.0.1:9222/json/versionのレスポンスが200であれば起動中。停止していれば、ユーザーに「9222ポートでChromeを起動してください」と通知(Discord)。自動起動はOS別に分岐。 - Playwright CDP接続 —
playwright.chromium.connect_over_cdp("http://127.0.0.1:9222")の1行。新規ブラウザは起動しません。 - ターゲットタブの探索 — すでに開いているタブの中に
blogger.com/blog/themes/があればそこへ移動。なければ新規タブを開く。 - 「現在のテーマのソースコードを編集」ボタンのクリック — aria-labelまたはテキストセレクター。韓国語と英語の両方のロケールに対応。
- CodeMirrorエディタの有効化待ち —
document.querySelector('.CodeMirror')が表示されるまでポーリング。 CodeMirror.setValue(xml)の呼び出し — Playwrightのevaluate内で直接呼び出し。クリップボード経由やタイピング入力ではありません。50KBのXMLも即座に反映されます。- 「テーマを保存」ボタンのクリック — 保存後、「テーマが更新されました」というトースト通知が表示されればOK。
- 本番環境のスポットチェック — 60秒待機後に
https://blog-url/をフェッチ。theme.xmlのセンチネルコメント(例:)がレスポンスに含まれていれば verified=True。
全工程の平均時間は30秒。手動クリックは0回。
実際の効果
- 1回あたりのアップロード時間: 5分 → 30秒(90%短縮)
- 累計自動アップロード回数: 導入後、約320回
- 「復元」ルートの誤クリックによる第1世代Classicモードへの逆戻り事故: 導入前2件 → 導入後0件
- 失敗事例: 9222 Chromeの停止4件 / Googleログインの期限切れ2件 / CAPTCHA 1件。すべてユーザーへの通知 → 手動復旧。
- バックアップ: アップロードのたびに事前に
theme.xml.before.を自動保存。アップロードに失敗した場合、1秒以内に直前のバージョンにロールバック可能。
副次的な効果として、「テーマを1行だけ変えてみよう」という気軽な実験が増えました。毎回5分かかると考えると躊躇してしまいますが、30秒であれば気軽に試せます。結果として、デザインのイテレーションが高速化しました。
検証方法
3つの検証を行いました。
XMLのラウンドトリップ検証 — アップロード後、同じページのCodeMirrorで再び getValue() を呼び出し、その値が送信したXMLとバイト単位(byte-by-byte)で同一であるかを確認します。これは、BloggerのSkinVariablesパーサーがサイレントに拒否(silent reject)するケース(CDATA内に生のHTMLトークンがある場合など)を検知するための安全網です。320回のうち、サイレント拒否を検知したケースは5件ありました。
本番環境のスポットチェック — アップロード後に60秒待機し、本番サイトをフェッチ。theme.xmlのセンチネルマーカーがレスポンスに含まれているかを確認。320回中320回すべてパス。
第1世代Classicモードへの逆戻り防止テスト — 自動化処理が意図的に「復元」ボタンを押そうとした際、コード上で遮断されるかをユニットテストで確認。assert "restore" not in click_targets で常にパス。遮断フックが正常に動作していることを確認。
実装方法
モジュール全体を移植するよりも、核心となる数行だけを取り入れるのが実用的です。
まず、Chromeを9222ポートで起動します。
# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
--remote-debugging-port=9222 \
--user-data-dir=C:\chrome_debug_profile
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9222 \
--user-data-dir=$HOME/chrome_debug_profile
次に、Playwrightで接続(attach)します。
import asyncio
from playwright.async_api import async_playwright
BLOG_ID = "1234567890"
THEME_XML = open("theme.xml", encoding="utf-8").read()
async def upload_theme(xml: str):
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp("http://127.0.0.1:9222")
ctx = browser.contexts[0]
page = await ctx.new_page()
await page.goto(f"https://www.blogger.com/blog/themes/{BLOG_ID}")
# 「現在のテーマのソースコードを編集」
await page.click("text=현재 테마의 소스 코드 수정")
await page.wait_for_selector(".CodeMirror", timeout=30000)
await page.evaluate(
"(xml) => document.querySelector('.CodeMirror').CodeMirror.setValue(xml)",
xml,
)
await page.click("button:has-text('테마 저장')")
await page.wait_for_selector("text=테마가 업데이트되었습니다", timeout=60000)
await browser.close()
print("uploaded")
asyncio.run(upload_theme(THEME_XML))
ここで重要なのは、connect_over_cdpの1行とCodeMirror.setValueの1行です。残りはセレクターを調整する作業です。
復元ルートは絶対にクリックしないでください。同じページに「復元」ボタンが並んでいますが、それは私たちの事故事例の原因です。一度クリックすると第1世代のClassicモードに逆戻りし、本番サイトが崩れてしまいます。自動化処理においては、そのセレクターをコード上で明示的に遮断しておくのが安全です。
要約: Chromeを9222ポートで起動し、Playwrightで接続(attach)した後にCodeMirror.setValueを1行呼び出すだけで、Bloggerテーマの自動アップロードが可能になります。最初のセットアップさえ済ませておけば、その後はコマンド1行ですべてのアップデートが
Category Coverage Notice
This article follows our label-specific editorial criteria. Details: