Webサイトの情報をWebフォームに自動で転記するChrome拡張機能の紹介です。
あるサイトから情報をコピペして、Webフォームに転記するという作業を毎日されている方がいたため、AIで自動化しました。
ちなみにほぼ以下の方法で、AIが実装しています。

CursorのRuleを使って簡単にSpec駆動開発
Cursorとは?Cursorとは、AIを活用した次世代のコードエディタです。Visual Studio Code(VS Code)をベースに開発されており、GitHub CopilotのようなAI補完機能に加えて、自然言語でコードを生成・...
仕様
機能
- フォーム項目の自動検出: 現在開いているWebページの入力項目を自動で取得・理解
- データスクレイピング: 指定されたURLからWebページの情報を取得
- AI自動マッピング: Gemini Flash APIを使用してフォーム項目とスクレイピングデータを自動マッピング
- 自動入力: マッピングされたデータをフォームに自動入力
アーキテクチャ

主要コンポーネント
- popup.js: ユーザーインターフェース処理
- content.js: Webページのフォーム操作とスクレイピング
- background.js: 拡張機能のバックグラウンド処理
API連携
- Gemini Flash API: データマッピング用AI処理
- Chrome Extension API: ブラウザ機能へのアクセス
ソースコード
早速作成された以下のソースコードを掲載します。
プログラムファイルのフォルダ構成
chrome_auto_form/
manifest.json
popup.html
popup.js
content.js
background.js
実装したプログラム
manifest.json
{
"manifest_version": 3,
"name": "Auto Form Filler",
"version": "1.0",
"description": "Webサイトの情報をWebフォームに自動で転記するChrome拡張機能",
"permissions": [
"activeTab",
"scripting",
"storage"
],
"host_permissions": [
"https://*/*",
"http://*/*"
],
"action": {
"default_popup": "popup.html",
"default_title": "Auto Form Filler"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": ["popup.html"],
"matches": ["<all_urls>"]
}
]
}
popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {width: 400px;padding: 16px;font-family: system-ui, -apple-system, sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;margin: 0;}
.header {text-align: center;margin-bottom: 24px;}
.title {font-size: 20px;font-weight: bold;margin: 0;text-shadow: 0 2px 4px rgba(0,0,0,0.3);}
.form-group {margin-bottom: 16px;
label {display: block;margin-bottom: 4px;font-weight: 600;font-size: 14px;}
input[type="text"], input[type="url"] {width: 100%;padding: 10px;border: none;border-radius: 6px;background: rgba(255,255,255,0.9);color: #333;font-size: 14px;box-sizing: border-box;}
input[type="text"]:focus, input[type="url"]:focus {outline: none;background: white;box-shadow: 0 0 0 2px rgba(255,255,255,0.5);}
button {background: linear-gradient(45deg, #ff6b6b, #ee5a52);color: white;border: none;padding: 12px 24px;border-radius: 6px;font-size: 14px;font-weight: 600;cursor: pointer;transition: all 0.3s ease;width: 100%;text-transform: uppercase;letter-spacing: 0.5px;}
button:hover {transform: translateY(-2px);box-shadow: 0 4px 12px rgba(238, 90, 82, 0.4);}
button:active {transform: translateY(0);}
button:disabled {opacity: 0.6;cursor: not-allowed;transform: none;}
.status {margin-top: 16px;padding: 12px;border-radius: 6px;background: rgba(255,255,255,0.1);font-size: 14px;min-height: 20px;backdrop-filter: blur(10px);}
.status.success {background: rgba(76, 175, 80, 0.3);}
.status.error {background: rgba(244, 67, 54, 0.3);}
.form-info {background: rgba(255,255,255,0.1);padding: 12px;border-radius: 6px;margin-bottom: 16px;backdrop-filter: blur(10px);}
.form-info h3 {margin: 0 0 8px 0;font-size: 16px;}
.field-list {font-size: 12px;opacity: 0.9;}
.loading {display: inline-block;width: 16px;height: 16px;border: 2px solid rgba(255,255,255,0.3);border-radius: 50%;border-top-color: white;animation: spin 1s ease-in-out infinite;margin-right: 8px;}
@keyframes spin {to { transform: rotate(360deg); }}
</style>
</head>
<body>
<div class="header">
<h1 class="title">Auto Form Filler</h1>
</div>
<div class="form-info" id="formInfo">
<h3>現在のフォーム情報</h3>
<div class="field-list" id="fieldList">フォーム項目を読み込み中...</div>
</div>
<div class="form-group">
<label for="apiKey">Gemini API キー:</label>
<input type="text" id="apiKey" placeholder="AIZaSy..." />
</div>
<div class="form-group">
<label for="sourceUrl">データ取得元URL:</label>
<input type="url" id="sourceUrl" placeholder="https://example.com" />
</div>
<button id="fillButton">取込・自動入力</button>
<div class="status" id="status">準備完了</div>
<script src="popup.js"></script>
</body>
</html>
popup.js
document.addEventListener('DOMContentLoaded', async () => {
const apiKeyInput = document.getElementById('apiKey');
const sourceUrlInput = document.getElementById('sourceUrl');
const fillButton = document.getElementById('fillButton');
const statusDiv = document.getElementById('status');
const fieldListDiv = document.getElementById('fieldList');
// 保存されたAPIキーを読み込み
const result = await chrome.storage.local.get(['geminiApiKey']);
if (result.geminiApiKey) {
apiKeyInput.value = result.geminiApiKey;
}
// 現在のページのフォーム情報を取得
async function loadFormInfo() {
try {
const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {action: 'getFormInfo'});
if (response.success) {
const fields = response.fields;
if (fields.length > 0) {
fieldListDiv.innerHTML = fields.map(field =>
`<div>• ${field.label || field.name || field.id || 'unnamed'} (${field.type})</div>`
).join('');
} else {
fieldListDiv.innerHTML = 'フォーム項目が見つかりません';
}
} else {
fieldListDiv.innerHTML = 'フォーム情報の取得に失敗しました';
}
} catch (error) {
console.error('Error loading form info:', error);
fieldListDiv.innerHTML = 'フォーム情報の取得に失敗しました';
}
}
// 初期読み込み
loadFormInfo();
// ステータス更新関数
function updateStatus(message, type = '') {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
}
// データ取得とフォーム入力
fillButton.addEventListener('click', async () => {
const apiKey = apiKeyInput.value.trim();
const sourceUrl = sourceUrlInput.value.trim();
if (!apiKey) {
updateStatus('APIキーを入力してください', 'error');
return;
}
if (!sourceUrl) {
updateStatus('データ取得元URLを入力してください', 'error');
return;
}
try {
// APIキーを保存
await chrome.storage.local.set({geminiApiKey: apiKey});
fillButton.disabled = true;
updateStatus('処理中...', '');
statusDiv.innerHTML = '<span class="loading"></span>処理中...';
// 現在のタブを取得
const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
// フォーム情報を取得
const formResponse = await chrome.tabs.sendMessage(tab.id, {action: 'getFormInfo'});
if (!formResponse.success) {
throw new Error('フォーム情報の取得に失敗しました');
}
// データをスクレイピング
const scrapingResponse = await chrome.tabs.sendMessage(tab.id, {
action: 'scrapeData',
url: sourceUrl
});
if (!scrapingResponse.success) {
throw new Error('データの取得に失敗しました: ' + scrapingResponse.error);
}
console.log(formResponse.fields);
console.log(scrapingResponse.data);
// AIで入力項目とデータをマッピング
const mappingResponse = await chrome.tabs.sendMessage(tab.id, {
action: 'mapDataToForm',
apiKey: apiKey,
formFields: formResponse.fields,
scrapedData: scrapingResponse.data
});
if (!mappingResponse.success) {
throw new Error('データマッピングに失敗しました: ' + mappingResponse.error);
}
console.log(mappingResponse.mappedData);
// フォームに自動入力
const fillResponse = await chrome.tabs.sendMessage(tab.id, {
action: 'fillForm',
mappedData: mappingResponse.mappedData
});
if (fillResponse.success) {
updateStatus(`${fillResponse.filledCount}件の項目を入力しました`, 'success');
} else {
throw new Error('フォーム入力に失敗しました');
}
} catch (error) {
console.error('Error:', error);
updateStatus('エラー: ' + error.message, 'error');
} finally {
fillButton.disabled = false;
}
});
});
content.js
// コンテンツスクリプト - Webページに注入される
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getFormInfo') {
getFormInfo(sendResponse);
return true; // 非同期レスポンス
}
if (request.action === 'scrapeData') {
scrapeData(request.url, sendResponse);
return true;
}
if (request.action === 'mapDataToForm') {
mapDataToForm(request.apiKey, request.formFields, request.scrapedData, sendResponse);
return true;
}
if (request.action === 'fillForm') {
fillForm(request.mappedData, sendResponse);
return true;
}
});
// フォーム情報を取得
function getFormInfo(sendResponse) {
try {
const formFields = [];
// 入力可能な要素を取得
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
// 隠しフィールドや無効なフィールドをスキップ
if (input.type === 'hidden' || input.type === 'button' || input.type === 'submit' || input.type === 'file' || input.disabled || input.readOnly) {
return;
}
const fieldInfo = {
id: input.id,
name: input.name,
type: input.type || input.tagName.toLowerCase(),
placeholder: input.placeholder,
label: getFieldLabel(input),
value: input.value,
required: input.required,
selector: getUniqueSelector(input)
};
formFields.push(fieldInfo);
});
sendResponse({success: true, fields: formFields});
} catch (error) {
console.error('Error getting form info:', error);
sendResponse({success: false, error: error.message});
}
}
// フィールドのラベルを取得
function getFieldLabel(input) {
// label要素から取得
if (input.id) {
const label = document.querySelector(`label[for="${input.id}"]`);
if (label) return label.textContent.trim();
}
// 親要素のlabelから取得
const parentLabel = input.closest('label');
if (parentLabel) {
return parentLabel.textContent.replace(input.value || '', '').trim();
}
// 前の兄弟要素から取得
let prevElement = input.previousElementSibling;
while (prevElement) {
if (prevElement.tagName === 'LABEL' || prevElement.textContent.trim()) {
return prevElement.textContent.trim();
}
prevElement = prevElement.previousElementSibling;
}
// テーブル構造対応: td → tr → th をたどる
const td = input.closest('td');
if (td) {
const tr = td.closest('tr');
if (tr) {
const th = tr.querySelector('th');
if (th) {
return th.textContent.replace(/\s+/g, ' ').trim();
}
}
}
return '';
}
// 要素の一意なセレクタを生成
function getUniqueSelector(element) {
if (element.id) return `#${element.id}`;
if (element.name) return `[name="${element.name}"]`;
// より複雑なセレクタ生成
const path = [];
let current = element;
while (current && current.nodeType === Node.ELEMENT_NODE) {
let selector = current.tagName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
path.unshift(selector);
break;
}
if (current.className) {
selector += `.${current.className.split(' ').join('.')}`;
}
const parent = current.parentNode;
if (parent) {
const siblings = Array.from(parent.children);
const index = siblings.indexOf(current);
if (index > 0) {
selector += `:nth-child(${index + 1})`;
}
}
path.unshift(selector);
current = parent;
}
return path.join(' > ');
}
// データをスクレイピング
async function scrapeData(url, sendResponse) {
try {
const response = await fetch(url);
const html = await response.text();
// 簡単なHTMLパースでテキストコンテンツを抽出
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// スクリプト、スタイル、コメントを除去
const elementsToRemove = doc.querySelectorAll('script, style, noscript, meta, link');
elementsToRemove.forEach(el => el.remove());
// 構造化データを抽出
const extractedData = {
title: doc.title || '',
headings: Array.from(doc.querySelectorAll('h1, h2, h3, h4, h5, h6')).map(h => h.textContent.trim()),
paragraphs: Array.from(doc.querySelectorAll('p')).map(p => p.textContent.trim()).filter(text => text.length > 0),
lists: Array.from(doc.querySelectorAll('ul, ol')).map(list =>
Array.from(list.querySelectorAll('li')).map(li => li.textContent.trim())
).flat(),
links: Array.from(doc.querySelectorAll('a[href]')).map(a => ({
text: a.textContent.trim(),
href: a.href
})),
images: Array.from(doc.querySelectorAll('img[alt]')).map(img => ({
alt: img.alt,
src: img.src
})),
tables: Array.from(doc.querySelectorAll('table')).map(table => {
const rows = Array.from(table.querySelectorAll('tr'));
return rows.map(row =>
Array.from(row.querySelectorAll('td, th')).map(cell => cell.textContent.trim())
);
}),
bodyText: doc.body.textContent.trim().replace(/[ \t]+/g, ' ')
};
sendResponse({success: true, data: extractedData});
} catch (error) {
console.error('Error scraping data:', error);
sendResponse({success: false, error: error.message});
}
}
// AIでデータをフォーム項目にマッピング(※10000文字まで)
async function mapDataToForm(apiKey, formFields, scrapedData, sendResponse) {
try {
const prompt = `
あなたは自動フォーム入力アシスタントです。以下のWebページから取得したデータを、フォーム項目に適切にマッピングしてください。
【フォーム項目】
${formFields.map(field => `
- ID: ${field.id || 'なし'}
- Name: ${field.name || 'なし'}
- Type: ${field.type}
- Label: ${field.label || 'なし'}
- Placeholder: ${field.placeholder || 'なし'}
- Selector: ${field.selector}
`).join('\n')}
【スクレイピングデータ】
タイトル: ${scrapedData.title}
見出し: ${scrapedData.headings.join(', ')}
本文: ${scrapedData.bodyText.substring(0, 10000)}...
【指示】
1. 各フォーム項目に適切な値を入力してください
2. データが見つからない場合は空文字を返してください
3. 以下のJSON形式で回答してください:
{
"mappings": [
{
"selector": "フィールドのセレクタ",
"value": "入力する値",
"confidence": "信頼度(0-1)"
}
]
}
`;
// console.log(prompt);
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contents: [
{
parts: [
{
text: prompt
}
]
}
]
})
});
const result = await response.json();
console.log(result);
if (!response.ok) {
throw new Error(`Gemini API error: ${result.error?.message || 'Unknown error'}`);
}
const aiResponse = result.candidates[0].content.parts[0].text;
// JSONを抽出
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('AI response format error');
}
const mappedData = JSON.parse(jsonMatch[0]);
sendResponse({success: true, mappedData: mappedData.mappings});
} catch (error) {
console.error('Error mapping data:', error);
sendResponse({success: false, error: error.message});
}
}
// フォームに自動入力
function fillForm(mappedData, sendResponse) {
try {
let filledCount = 0;
mappedData.forEach(mapping => {
if (mapping.value && mapping.confidence > 0.5) {
const element = document.querySelector(mapping.selector);
if (element) {
// 値を設定
element.value = mapping.value;
// イベントを発火(React等のフレームワーク対応)
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
filledCount++;
}
}
});
sendResponse({success: true, filledCount});
} catch (error) {
console.error('Error filling form:', error);
sendResponse({success: false, error: error.message});
}
}
background.js
// バックグラウンドスクリプト - 拡張機能のメイン処理
chrome.runtime.onInstalled.addListener(() => {
console.log('Auto Form Filler extension installed');
});
// タブが更新されたときの処理
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
// ページが完全に読み込まれた時の処理
console.log('Page loaded:', tab.url);
}
});
// 拡張機能アイコンクリック時の処理
chrome.action.onClicked.addListener((tab) => {
// ポップアップが設定されているのでここは実行されない
console.log('Extension icon clicked');
});
// メッセージハンドリング(必要に応じて)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'background_action') {
// バックグラウンドでの処理が必要な場合
console.log('Background action received:', request);
sendResponse({success: true});
}
});
// ストレージ変更の監視
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local') {
console.log('Storage changed:', changes);
}
});
// エラーハンドリング
chrome.runtime.onSuspend.addListener(() => {
console.log('Extension suspended');
});
// 拡張機能の初期化処理
async function initializeExtension() {
try {
// 初期設定の確認
const result = await chrome.storage.local.get(['geminiApiKey']);
if (!result.geminiApiKey) {
console.log('API key not found - user needs to set it up');
}
// 必要に応じて初期設定
await chrome.storage.local.set({
extensionVersion: '1.0',
initialized: true
});
console.log('Extension initialized successfully');
} catch (error) {
console.error('Extension initialization failed:', error);
}
}
// 初期化実行
initializeExtension();
使用方法
インストール方法
- Chrome拡張機能の開発者モードを有効にする
- Chrome → 設定 → 拡張機能 → 開発者モードをON
- 拡張機能ファイルを準備
manifest.jsonpopup.htmlpopup.jscontent.jsbackground.js
- 「パッケージ化されていない拡張機能を読み込む」でフォルダを選択
自動入力の実行
今回は、以下画像の右画面から情報を取得し、左画面のフォームに自動転記してみます。

- フォームがあるWebページを開く
- 入力したいフォームがあるページを表示
- 登録した拡張機能のポップアップを開く

- データ取得元URLを指定
- Google AI StudioでAPIキーを取得し、Gemini API キーを設定(初回のみ)
- 「データ取得元URL」欄にスクレイピングしたいWebページのURLを入力

- 自動入力を実行
- 「取込・自動入力」ボタンをクリック
- AIが自動でデータをマッピングしてフォームに入力
まとめ
今回紹介したChrome拡張機能を使えば、毎日のコピペ作業を自動化し、業務効率を大幅に向上させることができます。
Web上の情報を正確かつ素早くフォームに転記できるため、入力ミスの防止にも役立ちます。
同様のルーチン作業にお悩みの方は、ぜひ本記事の手順を参考に導入してみてください。



コメント