atomz technology logoatomz technologyBlog
記事一覧

定型業務のブラウザ自動化:Skills・Chrome MCP・Playwright どれを選ぶか

·10 分で読めます
atomz-okumuraatomz-okumura

はじめに

毎月、クライアントからメールでチラシの PDF が届きます。メールの添付ファイルは GAS(Google Apps Script)で Google ドライブに自動保存される仕組みにしてあるので、ファイルの取り込みまでは人手いらず。問題はその先で、保存された PDF を CMS の管理画面に登録して、さらに管理用の Google スプレッドシートにも記録する、という作業をずっと手でやっていました。PDF に「いつからいつまで・どの店舗向けか」が書かれていて、それを読み取って登録していく流れです。1件ごとに:

  • メールに添付された PDF を見てタイトルや期間を読み取る
  • 命名規則に沿ってタイトルを整える
  • 管理画面のフォームに入力して PDF をアップロード
  • スプレッドシートに同じ情報を追記

これを月に十数件。届くたびに同じことを繰り返す、地味だけど神経を使う作業で、コピペミスも起きます。

これを Claude(Claude Code)で自動化したんですが、ポイントは 「判断が必要な部分」と「ただの作業」を分けて、それぞれ別の道具に任せた ことでした。この記事ではその構成と、ブラウザ自動化の選択肢(Skills / Chrome MCP / Playwright)の使い分けをまとめます。

先に結論

ブラウザ自動化の選択肢は大きく3つ。住み分けはこうなります。

SkillsChrome MCPPlaywright
コーディング不要不要必要
実行速度ふつうふつう速い
繰り返し運用
クロスプラットフォーム手順次第
API コスト操作数しだい操作数しだいかからない
向いてる作業手順を自然言語で書ける作業判断が必要な作業毎回同じ定型作業

ポイントは、Skills と Chrome MCP はどちらも「LLM がページを見て1操作ずつ判断する」仕組み だということです。だから速度もコストもこの2つは同じ土俵で、1操作ごとに「ページを読む → LLM が判断 → 実行」の往復が入ります。数ステップなら気になりませんが、操作数に比例してトークンと時間がかかります。一方 Playwright はこの往復がなく、コードが直接ブラウザを叩くので速くて実行時のコストもゼロです。

そして今回いちばん効いたのは、1つに絞らず「判断が必要な部分は LLM、定型の作業は Playwright」と分業させた ことでした。以下、具体的にどう組んだかを書いていきます。

全体の流れ

最終的にこういう構成に落ち着きました。

図を描画中…

前段の Gmail → ドライブ保存 は GAS(Google Apps Script)で自動化してあります。クライアントからチラシのメールが届くと、添付 PDF を受信日ごとのフォルダに自動で振り分けて保存してくれます。ここまでは人手ゼロです。

そしてこの記事の本題は、保存された PDF から先の LLM → JSON → Playwright の3段です。肝は真ん中の JSON

  • 前半(①): PDF を読んで「何を登録すべきか」を判断する。ここは内容の解釈が必要なので Claude(LLM) に任せる
  • 後半(②): 決まったデータを決まった場所に入れる。ここは手順が固定なので Playwright(スクリプト) に任せる

JSON が両者の「契約」になっていて、前半と後半を完全に切り離せるのがミソです。

① LLM パート:Claude に PDF を読ませて JSON を作る

まず Claude に PDF を読ませます。やっていることは単純で、こんな指示です。

このPDFを読んで、チラシ登録用のJSONを作って。
- D:\...\stores.json で店舗の正式名を照合すること
- 命名規則は CLAUDE.md のとおり

命名規則みたいな「毎回守ってほしいルール」は、プロンプトに毎回書くのではなく CLAUDE.md に置いておきます。Claude Code はカレントディレクトリの CLAUDE.md を自動で読むので、指示しなくても守ってくれます。

CLAUDE.md(抜粋)
### 命名規則(厳守)
- 店名は短縮形(「○○ストア △△店」→「△△店」)
- 「10周年祭」などの企画名は入れない
- 日付と店名の間は全角スペース(U+3000)
 
### 登録値
- status: 必ず 0(非公開)
- 公開開始時間: 08:00:00 / 終了時間: 23:30:00
- テキスト: セール期間:{月/日}-{月/日}

ここが LLM の出番です。「PDF に書かれた長い店舗名を短縮形に直す」「複数の表記ゆれを正式名に揃える」といった作業は、ルールベースで書くと例外が多くて面倒ですが、LLM なら文脈で判断してくれます。

出力されるのはこんな JSON です。

json/example.json
{
  "name": "6/20-22 駅前店単独チラシ",
  "text": "セール期間:6/20-22",
  "beginDate": "2026/06/20",
  "beginTime": "08:00:00",
  "endDate": "2026/06/22",
  "endTime": "23:30:00",
  "status": "0",
  "stores": ["駅前店"],
  "pdfPath": "C:\\...\\tmp\\260620-22_ekimae.pdf"
}

この JSON さえ正しく作れれば、後半は完全に機械的に処理できます。「判断」をここで JSON という形に固めてしまう のがポイントです。

② Playwright パート:JSON を渡してフォームに流し込む

後半は判断ゼロです。JSON を受け取って、決まったフォームに決まった順で入れていくだけ。

node upload-flyer.mjs json/example.json

スクリプトは既存の Chrome に CDP 接続して動きます。ログイン済みの Chrome をそのまま使うので、認証情報をスクリプトに渡す必要がありません。

upload-flyer.mjs(抜粋)
import { chromium } from "playwright-core";
import { readFileSync } from "node:fs";
 
const data = JSON.parse(readFileSync(process.argv[2], "utf8"));
 
// 起動済み Chrome に CDP 接続(ログイン状態をそのまま使う)
const browser = await chromium.connectOverCDP("http://localhost:9222");
const context = browser.contexts()[0];
const page = await context.newPage();
 
// フォームに値を流し込む
await page.goto(`${BASE_URL}/admin/flyer/add`);
await page.evaluate((d) => {
  const setVal = (sel, val) => {
    const el = document.querySelector(sel);
    el.value = val;
    // input/change/blur を発火させないと CMS 側の状態が更新されない
    el.dispatchEvent(new Event("input",  { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
    el.dispatchEvent(new Event("blur",   { bubbles: true }));
  };
  setVal("#FlyerName", d.name);
  setVal("#FlyerText", d.text);
  setVal("#FlyerPublishBeginDate", d.beginDate);
  // ...
}, data);
 
// PDF アップロードはファイル選択 input にパスを渡すだけ
await page.setInputFiles("#FlyerFile", data.pdfPath);
await page.click('form input[type="submit"]');

一度書いてしまえば LLM を挟まないので速いし、API コストもかかりません。同じスクリプトが Windows でも Mac でも動きます。

Tip

値を .value = ... で入れるだけだと、CMS 側の JS が「変更された」ことに気づかず保存されないことがあります。input / change / blur イベントを自分で発火させるのが定番の対策です。

なぜ「判断」と「作業」を分けたのか

実は最初から Playwright だったわけではなく、ブラウザ自動化の選択肢をいくつか検討しました。

Skills

.skill ファイルに手順を自然言語で書いておくと、Claude が読んで実行してくれる方式です。

  • メリット: コードを書かなくていい。手順を文章で表現できるので、エンジニア以外でも作れる・直せる
  • デメリット: 実行は LLM が担うので、スクリプトのように「毎回まったく同じ動作」が保証されるわけではない。SKILL.md に OS 固有の操作(Finder やエクスプローラ)が混ざると環境依存になる。毎操作で LLM を呼ぶので、繰り返すほど時間とトークンがかかる

今回のチラシ入稿だと: 毎月十数件を同じ手順で淡々と処理したい作業なので、「動作が決定論的に保証されない」「件数ぶんトークンがかかる」のが噛み合いませんでした。手順を自然言語で持つ強みも、最終的にコードで固めたい今回では活きません。

Chrome MCP(chrome-devtools MCP)

Claude がリアルタイムでページを見ながら操作を判断する方式です。

  • メリット: ページの状態を読んで「次に何をすべきか」をその場で判断できる。想定外の画面やエラーにも対応しやすい
    • コンソールエラーを読んで原因を診断させる
    • Lighthouse スコアを見て改善点を出させる
    • 内容を解釈してフォームの適切な場所に振り分ける
  • デメリット: 毎操作で「ページを読む → LLM が判断 → 実行」の往復が入るので、操作数に比例して時間とトークンがかかる

別案件(ドキュメントから内容を読み取って入稿する作業)ではこちらを使っています。あちらは「内容を解釈してフォームに振り分ける」判断が実行中ずっと必要なので MCP が最適でした。

今回のチラシ入稿だと: 判断が必要なのは「PDF を読んで何を登録するか決める」最初だけで、フォーム入力自体は完全に決まりきった手順です。その定型部分にまで毎回 LLM を挟むのは、遅いしトークンの無駄でした。

Playwright

手順をコードに固定する方式です。

  • メリット: 判断を挟まないので速く、実行時のトークンコストはゼロ。毎回まったく同じ動きをするので結果が予測できる。同じスクリプトが Windows でも Mac でも動く
  • デメリット: コードを書く必要がある。判断が要る作業(毎回違う画面・例外処理)は苦手

今回のチラシ入稿だと: フォーム入力という完全に定型の作業にぴったりでした。一度書けば毎月使い回せて、実行コストもかかりません。唯一の弱点だった「判断ができない」部分は、前段の LLM が JSON を作ることで補っています。

結局どう分けたか

今回の作業を分解すると:

工程性質任せた先
PDF の読み取り・命名・店舗名の照合判断が必要Claude(LLM)
フォーム入力・PDF アップロード・スプレッドシート追記完全に定型Playwright

判断が必要な前半だけ LLM に任せ、定型の後半はスクリプトで固める。 こうすると、毎回 LLM にブラウザ操作までさせるより速くて安定するし、後半はコードなので動作が完全に予測できます。Skills や Chrome MCP に全部やらせる選択肢もありましたが、「PDF 読み取り」と「フォーム入力」を JSON で分けたことで、それぞれを一番得意な道具に振れました。

ハマったポイント(Playwright 編)

後半のスクリプト、定型とはいえ正確に動かすまでに細かい罠がありました。

日本語が途中で消える(keyboard.type() 問題)

keyboard.type("セール期間:6/21-27") と打つと、全角コロン (U+FF1A)の手前で入力が止まって残りが消えました。

// NG: 全角コロンで止まる
await page.keyboard.type("セール期間:6/21-27");
 
// OK: クリップボード経由で貼り付ける
await page.evaluate((v) => navigator.clipboard.writeText(v), value);
await page.keyboard.press("Control+v");

クリップボードを使うには事前に権限付与が必要です。

const context = browser.contexts()[0];
await context.grantPermissions(["clipboard-read", "clipboard-write"]);

Google Sheets のプルダウンにテキストが貼れない

データ入力規則(プルダウン型)のセルに Ctrl+V で貼り付けようとしたら、データ検証に弾かれました。ドロップダウンを開いて項目をクリックするのが正解でした。

async function selectFromDropdown(page, items) {
  await page.keyboard.press("Enter");   // ドロップダウンを開く
  await page.waitForTimeout(600);
 
  for (let i = 0; i < items.length; i++) {
    // 検索ボックスに貼り付けて絞り込む
    await page.evaluate((v) => navigator.clipboard.writeText(v), items[i]);
    await page.keyboard.press("Control+v");
    await page.waitForTimeout(400);
 
    await page.locator('[role="menuitemcheckbox"]')
      .filter({ hasText: items[i] }).first().click();
 
    if (i < items.length - 1) {
      await page.keyboard.press("Control+a");
      await page.keyboard.press("Delete");
    }
  }
  await page.keyboard.press("Escape");  // 閉じて確定
}
Info

Alt+ArrowDown でプルダウンを開こうとしましたが Google Sheets では効きませんでした。Enter キーが正解です。

Ctrl+End で最終行が取れない

追記先の行を探すのに Ctrl+End を押したら、1000行目(シートのデフォルト最大行)に飛びました。データのある最終行ではなく、シートの物理的な末尾に飛んでしまいます。

// NG: シートの最大行(1000行目)に飛ぶ
await page.keyboard.press("Control+End");
 
// OK: A1 から Ctrl+↓ でデータのある最終行に移動
await goToCell(page, "A1");
await page.keyboard.press("Control+ArrowDown");
const cell = await getNameBoxValue(page); // 例: "A42"
const lastRow = parseInt(cell.match(/\d+$/)[0]);

まとめ

  • 判断と作業を分ける:内容の解釈が必要な前半は LLM、定型の後半はスクリプト。間を JSON でつなぐと、それぞれを得意な道具に任せられる
  • Skills:手順を自然言語で書ける作業に。OS 依存の操作が混ざると環境依存になるので注意
  • Chrome MCP:LLM がページを見て判断する作業に最適。ただし操作数に比例して時間とトークンがかかる
  • Playwright:定型作業の繰り返しに最適。一度書けば速くて安定。日本語入力や Google Sheets プルダウンには独特の詰まりポイントあり

「全部 AI にやらせる」より、「判断だけ AI に任せて、作業はスクリプトで固める」方が、今回のような定型業務では速くて安定しました!

Services

その他のサービスはatomz technology ↗で。