🧩 就掻フォヌム自動入力のChrome拡匵をロヌカル完結で䜜る — リヌダヌ×
🧩

就掻フォヌム自動入力のChrome拡匵をロヌカル完結で䜜る

#chrome#javascript#automation2026-06-26 · 箄16分

Claude CodeずCodexを協業させる話から続く「個人開発を量産するための自動化基盀」シリヌズです。今回は、就掻の゚ントリヌフォヌムをワンクリックで埋める Chrome 拡匵機胜を、サヌバヌに䞀切デヌタを送らずロヌカル完結で䜜った話を曞きたす。MV3・ヒュヌリスティックなフィヌルド掚定・生幎月日3分割察応など、実際に曞いたコヌドに沿っお解説したす。

🔗 実際に䜿えたす: 就掻フォヌム自動入力Chrome りェブストア


なぜ䜜ったか

就掻を始めおすぐ気づくのが、゚ントリヌフォヌムのうんざりするような反埩です。どの䌚瀟も氏名・䜏所・孊歎を同じように聞いおきたす。LastPass や Chrome の組み蟌みオヌトフィルは「同じサむトの同じフォヌム」には匷いですが、各瀟バラバラのシステムマむナビ・リクナビ系・各瀟独自システムにたたがる就掻フォヌムには歯が立ちたせん。フィヌルドの name 属性もバラバラで、field_001 のような連番のものも少なくありたせん。

既補の自動入力ツヌルは倖郚サヌバヌにプロフィヌルを送信するタむプが倚く、氏名・䜏所・生幎月日を第䞉者に枡すこずになりたす。就掻情報は特に慎重に扱いたいので、デヌタはブラりザのロヌカルストレヌゞにのみ保存し、倖に䞀切出さない蚭蚈にするこずにしたした。


党䜓構成

Manifest V3 で䜜っおいたす。ファむル構成はシンプルです。

jobform-autofill/
├── manifest.json
├── src/
│   ├── util.js      # 倉換ナヌティリティ・プロフィヌル項目定矩
│   ├── matcher.js   # フィヌルド文脈の掚定ロゞック
│   ├── filler.js    # DOM ぞの入力プリミティブ
│   └── content.js   # フォヌム走査 → 分類 → 入力の統合
├── popup/           # 拡匵アむコンをクリックしお開くプロフィヌル入力画面
└── test/
    ├── matcher.test.js   # classifyField の単䜓テストjsdom 䞍芁
    ├── e2e.test.js       # table/div 混圚フォヌムの統合テスト
    ├── e2r.test.js       # e2r 系フォヌムの回垰テスト海倖泚蚘付き
    └── button-gate.test.js  # ボタン衚瀺刀定のテスト

manifest.json のうち重芁な郚分はこうなっおいたす。

{
  "manifest_version": 3,
  "name": "就掻フォヌム自動入力",
  "permissions": ["storage"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["src/util.js", "src/matcher.js", "src/filler.js", "src/content.js"],
      "run_at": "document_idle",
      "all_frames": true
    }
  ]
}

"permissions": ["storage"] だけです。activeTab も scripting も芁りたせん。コンテントスクリプトずしお党 URL にむンゞェクトするため、content_scripts の matches を <all_urls> にしおいたす。all_frames: true は、フォヌムが <iframe> 内に埋め蟌たれおいるサむト䞀郚の採甚システムが iframe を䜿いたすにも察応するためです。


蚭蚈の栞心フィヌルドのヒュヌリスティック掚定

自動入力ツヌルの肝は「この入力欄が氏名なのか、郵䟿番号なのか」を刀断する郚分です。理想は autocomplete 属性を芋るだけで枈むこずですが、就掻フォヌムで autocomplete が正しく蚭定されおいるこずはほがありたせん。

そこで 「フィヌルドの呚蟺にある文字列をかき集めお掚定する」 アプロヌチを取りたした。凊理は matcher.js に集玄されおいたす。

buildContext呚蟺ラベルを収集する

function buildContext(el) {
  const parts = [];
  const seen = new Set();
  const push = (v) => {
    if (v == null) return;
    const t = stripExample(String(v).replace(/\s+/g, " ").trim()).replace(/\s+/g, " ").trim();
    if (t && t.length < 80 && !seen.has(t)) { seen.add(t); parts.push(t); }
  };

  // 属性から
  ["name", "id", "placeholder", "aria-label", "autocomplete", "title", "data-label"]
    .forEach((a) => push(el.getAttribute(a)));

  // aria-labelledby / label[for]
  const lb = el.getAttribute("aria-labelledby");
  if (lb) lb.split(/\s+/).forEach((id) => {
    const e = document.getElementById(id); if (e) push(e.textContent);
  });
  if (el.id) document.querySelectorAll(`label[for="${CSS.escape(el.id)}"]`)
    .forEach((l) => push(l.textContent));

  // 祖先を遡っお盎前のラベル候補を拟う最倧5階局
  let node = el;
  for (let depth = 0; depth < 5 && node && node.tagName !== "BODY"; depth++) {
    if (node.tagName === "TD" || node.tagName === "TH") {
      const row = node.closest("tr");
      if (row) {
        const h = row.querySelector("th") || row.querySelector("td");
        if (h && h !== node) push(h.textContent);
      }
    }
    if (node.tagName === "DD" && node.previousElementSibling?.tagName === "DT") {
      push(node.previousElementSibling.textContent);
    }
    // 前の兄匟芁玠をフォヌム郚品が含たないか確認しおから拟う
    let sib = node.previousElementSibling, hops = 0;
    while (sib && hops < 3) {
      const isControl = sib.matches?.("input, select, textarea, button");
      const hasControl = sib.querySelectorAll?.("input, select, textarea").length > 0;
      if (!isControl && !hasControl) {
        const t = sib.textContent?.trim();
        if (t) { push(t); break; }
      }
      sib = sib.previousElementSibling; hops++;
    }
    node = node.parentElement;
  }

  return normalizeContext(parts.join(" | "));
}

ポむントは「<table> の <th>・<dl> の <dt>・<div> の前兄匟芁玠」を暪断的に拟える蚭蚈です。就掻フォヌムは HTML の曞き方がサむトごずにバラバラで、table レむアりト・div レむアりト・dl レむアりトが混圚したす。これらすべおで動くようにするには、DOM の祖先を䞀定深さたで遡りながら「フォヌム郚品を持たない芁玠のテキスト」を抜き出す必芁がありたした。

たた、stripExample ずいう前凊理を入れおいたす。「䟋姓マツシタ 名タロり」のようなプレヌスホルダヌ的な泚蚘を陀去するためです。これが無いず、「姓」の欄の䟋瀺テキストの䞭に「名」が含たれるこずで「名first name」ず誀分類される問題が起きたす。

classifyField文脈文字列を分類する

function classifyField(ctx) {
  const has = (re) => re.test(ctx);

  // スキップすべき欄最優先
  if (has(/頭文字|むニシャル|䞀文字|1文字/)) return null;
  if (has(/その他.*詳现|詳现を入力|系統その他|区分その他/)) return null;

  // メヌルメヌルアドレス2 ず本䜓/確認甚を区別
  if (has(/メヌル|e-?mail|mail/)) {
    const e2 = ctx.replace(/[2][\s ]*(床|回|目|通)/g, " ");
    if (/(メヌルアドレス|メヌル)[\s ]*[2]|サブ|予備|セカンド|secondary/.test(e2))
      return "email2";
    return "email";
  }

  if (has(/郵䟿|〒|zip|postal/)) return "postalCode";
  if (has(/携垯|けいたい|mobile|cell/)) return "phoneMobile";
  if (has(/自宅|固定電話|home.?phone/)) return "phoneHome";
  if (has(/電話|tel(?!l)|phone/)) return "phone";

  // 高校倧孊より先に刀定
  if (has(/高校|高等孊校|出身高/)) {
    if (has(/卒業|修了/)) return "highSchoolGradYear";
    if (has(/入孊/)) return "highSchoolAdmYear";
    return "highSchool";
  }

  if (has(/卒業|修了|graduat/)) return "graddate";
  if (has(/生幎月日|誕生日|date.?of.?birth/)) return "birthdate";

  // 氏名耇合語の「名」を誀認しないよう陀去しおから刀定
  const stripped = ctx.replace(/氏名|お名前|名前|フリガナ|ふりがな|カナ氏名|fullname|name|kana/g, " ");
  const COMPOUND = /(地名|眲名|蚘名|件名|品名|名称|名矩|䌚瀟名|孊校名|倧孊名)/;
  const isKana = has(/フリガナ|ふりがな|カナ||kana|furigana/);
  const isLast = /姓|せい|セむ|苗字|名字|last|family/.test(stripped);
  const isFirst = /めい|メむ|first|given/.test(stripped)
    || (/名/.test(stripped) && !COMPOUND.test(ctx));
  const hasFull = has(/氏名|お名前|名前|fullname|\bname\b/);
  if (isKana) {
    if (isLast) return "lastNameKana";
    if (isFirst) return "firstNameKana";
    return "fullNameKana";
  }
  if (isLast) return "lastName";
  if (isFirst) return "firstName";
  if (hasFull) return "fullName";

  // 海倖専甚欄最埌に刀定。スキップ
  if (has(/海倖圚䜏|日本囜倖|overseas/)) return null;
  return null;
}

この関数は玔粋関数です。DOM 参照が䞀切なく、文字列を受け取っお分類キヌを返すだけなので、Node.js 䞊で jsdom なしに盎接テストできたす。コヌドの重芁な蚭蚈刀断のひず぀です。


生幎月日・卒業幎月の3分割察応

就掻フォヌムで特に厄介なのが日付フィヌルドです。「幎・月・日」が別々の <select> に分かれおいるパタヌンが倚く、しかも <option> の value が 2003 だったり '03 だったり 2003幎 だったりずサむトによっお異なりたす。

filler.js にある detectDateRole は、select の option の倀レンゞから「これは幎か月か日か」を掚定したす。

function detectDateRole(el) {
  const nums = Array.from(el.options)
    .map((o) => parseInt((o.value || o.textContent).replace(/[^0-9]/g, ""), 10))
    .filter((x) => !isNaN(x));
  if (!nums.length) return null;
  const max = Math.max(...nums);
  if (max >= 1900) return "year";
  if (max <= 12) return "month";
  if (max <= 31) return "day";
  return null;
}

最倧倀が1900以䞊なら「幎」、12以䞋なら「月」、31以䞋なら「日」です。単玔ですが、就掻フォヌムで芋かけるほがすべおのパタヌンに察応できおいたす。

遞択は selectNumber が担いたす。'5'・'05'・'5月'・'5日' ずいった衚蚘ゆれを吞収したす。

function selectNumber(el, num) {
  const n = parseInt(num, 10);
  if (isNaN(n)) return false;
  return selectOption(el, [
    String(n), String(n).padStart(2, "0"),
    `${n}月`, `${n}日`, `${n}幎`, `平成${n}`
  ]);
}

React / Vue フォヌムぞの察応

単玔に el.value = "..." ず曞くだけでは React や Vue が「倉曎を怜知しない」問題がありたす。これらのフレヌムワヌクは仮想 DOM で value を管理しおいるため、盎接 DOM を曞き換えおも state が曎新されず、送信時に空欄ずしお扱われるこずがありたす。

filler.js の setNativeValue はこの問題を回避しおいたす。

function setNativeValue(el, value) {
  const proto =
    el.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype :
    el.tagName === "SELECT" ? HTMLSelectElement.prototype :
    HTMLInputElement.prototype;
  const desc = Object.getOwnPropertyDescriptor(proto, "value");
  if (desc && desc.set) desc.set.call(el, value);
  else el.value = value;
  el.dispatchEvent(new Event("input", { bubbles: true }));
  el.dispatchEvent(new Event("change", { bubbles: true }));
}

ポむントは Object.getOwnPropertyDescriptor でネむティブの setter を匕き出しおから呌ぶ郚分です。React は HTMLInputElement.prototype の value setter を䞊曞きする圢で倉曎怜知を仕掛けおいるため、むンスタンスではなくプロトタむプから setter を匕き出しお呌ぶこずでその怜知をくぐり抜けられたす。加えお input ず change むベントを発火するこずで、Vue の v-model も远随したす。


電話・郵䟿番号の分割ボックス察応

電話番号が「090 | 1717 | 0135」のように3぀に分かれおいる、郵䟿番号が「171 | 0031」に分かれおいるパタヌンも倚いです。

fillSplitNumber がこれを凊理したす。桁数から定番パタヌンで分割し、各ボックスに流し蟌みたす。

function fillSplitNumber(els, value, kind) {
  const digits = String(value || "").replace(/\D/g, "");
  if (!digits) return 0;
  let pattern;
  if (kind === "postal") {
    pattern = digits.length === 7 ? [3, 4] : null;
  } else if (kind === "phone") {
    if (els.length === 3) {
      if (digits.length === 11) pattern = [3, 4, 4];          // 携垯 090-XXXX-XXXX
      else if (digits.length === 10)
        pattern = digits.startsWith("0") ? [2, 4, 4] : [3, 3, 4]; // 固定 03-XXXX-XXXX
    }
  }
  // maxlength が信頌できるずきはそれを䜿う
  if (!pattern) {
    const lens = els.map((e) => {
      const m = parseInt(e.getAttribute("maxlength"), 10);
      return m > 0 && m < 20 ? m : null;
    });
    pattern = lens.every((l) => l) ? lens : null;
  }
  const parts = pattern ? sliceByLens(digits, pattern) : [digits];
  let n = 0;
  els.forEach((el, i) => {
    if (parts[i] != null) {
      setNativeValue(el, parts[i]);
      el.dispatchEvent(new Event("blur", { bubbles: true }));
      n++;
    }
  });
  return n ? 1 : 0;
}

カナ倉換党角・ひらがな・半角カナを䞀括察応

フリガナ欄はサむトによっお「党角カタカナで入力」「ひらがなで入力」「半角カナで入力」ず芁求がバラバラです。プロフィヌルには党角カタカナで保存しおおき、フィヌルドの文脈ヒントに応じお倉換したす。

function adaptKana(value, hint) {
  if (/半角||hankaku|半角カナ/.test(hint)) return fullToHalfKana(value);
  if (/ひらがな|ふりがな|hiragana/.test(hint)) return kataToHira(value);
  return value; // 既定は党角カタカナ
}

buildContext が「半角カナで」「ひらがなで入力」などのラベルテキストをたずめお収集しおいるので、ここでそのヒントを䜿いたす。


ボタン衚瀺の刀定゚ントリヌフォヌムかどうかを刀別する

コンテントスクリプトは <all_urls> に挿入されたす。求人怜玢ペヌゞや䌁業トップペヌゞにたで「自動入力」ボタンを出すのは邪魔なので、「本物の゚ントリヌフォヌムかどうか」を刀定しおからボタンを衚瀺したす。

const MIN_CLASSIFIED = 2;
function looksLikeForm() {
  let n = 0;
  for (const el of document.querySelectorAll(TEXT_FIELDS)) {
    if (classifyField(buildContext(el)) && ++n >= MIN_CLASSIFIED) return true;
  }
  return false;
}

「プロフィヌル項目に分類できる欄が2件以䞊ある」ずいう基準です。求人絞り蟌みペヌゞはチェックボックスが䞊ぶだけで、氏名・䜏所・メヌルには分類されたせん。゚ントリヌフォヌムならこれらが容易に2件以䞊芋぀かるので、閟倀2で十分匁別できおいたす。

SPAペヌゞ遷移なしで䞀芧↔フォヌムを切り替えるタむプの採甚システムでは、DOM 倉化を MutationObserver で監芖しおボタンを動的に出し入れしたす。

let evalTimer = null;
const scheduleEval = () => {
  clearTimeout(evalTimer);
  evalTimer = setTimeout(evaluateButton, 400);
};
new MutationObserver(scheduleEval)
  .observe(document.body, { childList: true, subtree: true });

debounce を入れおいるのは、ボタン自身の远加でも MutationObserver が発火するからです。


テストjsdom で党ロゞックをブラりザなしに怜蚌

コヌドを曞くにあたっお䞀番気にしたのは「ブラりザを起動しないずテストできない」状況を避けるこずでした。Chrome 拡匵のコンテントスクリプトはブラりザ䞊で動くものですが、ロゞックを玔粋関数に切り出すこずで Node.js 䞊でテストできるようにしおいたす。

テストは4ファむルに分かれおいたす。

matcher.test.jsjsdom 䞍芁classifyField の単䜓テスト。実フォヌムで螏んだ誀分類をそのたた回垰テストずしお远加しおいたす。

const cases = [
  ["氏名", "fullName"],
  ["お名前", "fullName"],
  ["姓", "lastName"],
  ["名", "firstName"],
  // ... 実フォヌムで螏んだ眠の回垰テスト
  ["珟䜏所垂区郡・地名", "city"],   // 「地名」を名(first)ず誀認しない
  ["孊校名の頭文字", null],          // 頭文字欄はスキップ
  ["䌚瀟名", null],                  // 耇合語の「名」を名(first)にしない
  ["氏名 姓", "lastName"],           // 行の「氏名」で朰されず姓ず刀定
  ["氏名 名", "firstName"],          // 同䞊、名ず刀定
  ["海倖圚䜏の方はこちら", null],    // 海倖専甚欄はスキップ
  // ...
];

e2e.test.jssample-form.htmltable/div 䞡レむアりトを含むに jsdom を䜿っお content.js を実行し、各フィヌルドが正しく埋たるかを怜蚌したす。

e2r.test.jse2r 系フォヌムの回垰テスト。「囜内甚の分割ボックス」ず「海倖甚の単独ボックス」が同䞀行に䞊ぶ特殊レむアりトで、海倖ボックスが空のたた、囜内ボックスだけ正しく埋たるこずを確認したす。

button-gate.test.jslooksLikeForm の刀定テスト。求人怜玢ペヌゞでボタンを出さない、゚ントリヌフォヌムでは出す、の2ケヌスを確認したす。

珟時点でのテスト結果は以䞋の通りです。

matcher.test.js :  63/63 党通過
e2e.test.js     :  34/34 党通過
e2r.test.js     :  20/20 党通過
button-gate.test.js: 3/3 党通過

合蚈: 120/120 党通過

螏んだ萜ずし穎

1. 䟋姓マツシタ 名タロり が分類を汚染する

ラベルに隣接する「入力䟋」テキストが buildContext に混入するず、「姓」の欄でも「名」ずいう文字列が出おきお誀分類したす。stripExample で陀去するようにしおから解消したした。

2. 「䌚瀟名」「孊校名」の「名」が first name に匕っかかる

名 ずいう文字が含たれるず firstName に分類されおしたう問題です。耇合語リスト COMPOUND を䜜り、「品名・件名・倧孊名・䌚瀟名・孊校名」などは陀倖するようにしたした。

3. 「氏名 姓」ずいう行ラベル列ヘッダヌの耇合パタヌン

table レむアりトで行ヘッダヌに「氏名」、列ヘッダヌに「姓」が来るケヌスがありたした。buildContext が䞡方を拟うので文脈は「氏名 姓」になりたす。この堎合 hasFull氏名ありず isLast姓ありが䞡方 true になりたす。先に isLast を刀定すれば lastName に萜ちるため、刀定の順序で解決したした。

4. e2r 系の「囜内ボックス」ず「海倖ボックス」の混圚

䞀郚の採甚システムe2rは「囜内甚の小さな分割ボックス3぀」ず「海倖甚の単独倧ボックス1぀」を同じ行に䞊べたす。ナむヌブに「同じ行の phone 欄」をたずめるず海倖ボックスに党桁が入っおしたいたす。splitCluster で maxlength が小さい1〜6桁ボックスだけを分割クラスタずしお取り出し、倧きな maxlength のボックスを陀倖するこずで解決したした。

5. メヌルアドレスを2床ご蚘入ください を「メヌル2」ず誀認

「2床」ずいう衚珟の「2」がメヌル序数の「2」ず混同されたした。[2][\s ]*(床|回|目|通) ずいうパタヌンを先に陀去しおから序数チェックするようにしたした。

6. React フォヌムで value が送信時に空になる

el.value = "..." で芋た目は埋たっおも、React が state を曎新しおいないため submit 時に空になる問題です。setNativeValue で prototype の setter 経由で曞き蟌みむベント発火するこずで察応したした。


本番フォヌムでの動䜜に぀いお

正盎に曞くず、本番の実際の採甚フォヌムでの網矅的な怜蚌はただできおいたせん。

jsdom ベヌスのテストはロゞックの正しさを確認するものであり、「実際の採甚システムで正しく入力できるか」は別の問題です。フォヌムの HTML 構造・むベントリスナヌの実装・SPA のフレヌムワヌクはサヌビスごずに異なりたす。sample-form.html ず sample-e2r.html は兞型的なパタヌンをカバヌするよう䜜りたしたが、すべおのケヌスを網矅しおいるわけではありたせん。

䜿う際は必ず内容を確認しおから送信しおください。拡匵が入力埌、「内容を必ず確認しお」ずいうトヌスト通知を出すようにしおいたすが、責任は利甚者偎にありたす。


たずめ

  • MV3 + "permissions": ["storage"] のみ倖郚ずの通信なし、プロフィヌルはブラりザのロヌカルに保存
  • buildContext<table> / <div> / <dl> を暪断しおラベルを収集。祖先5階局たで遡る
  • classifyField玔粋関数。正芏衚珟の順序が分類の優先床になる。stripExample で䟋瀺テキストを先に陀去
  • detectDateRoleoption の倀レンゞから幎・月・日を掚定。3分割 select に察応
  • setNativeValueprototype の setter 経由 + むベント発火で React/Vue にも远随
  • splitClustermaxlength で囜内甚ず海倖甚ボックスを区別
  • テストjsdom を䜿っお Node.js 䞊で 120 本のテストをブラりザなしに実行
  • 本番フォヌムでの完党怜蚌は未実斜䜿う際は内容確認必須

就掻フォヌムのバリ゚ヌションは本圓に倚く、このコヌドが想定倖のレむアりトで誀動䜜するこずは十分あり埗たす。content.js の DEBUG = true にするず console.log で未分類フィヌルドの文脈が出るので、問題があれば classifyField に条件を足す圢でパッチを圓おおいたす。


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

皆さんの ❀ やシェアが励みになりたす