MENU

架空の個人情報を作成する(テストデータ用)

業務改善に取り組んでいると本番データではなく検証データが欲しいときが多々あると思うんですが、毎回本番をもじって作るのが面倒でした。
今回、CopilotにJavaScriptをHTMLに埋め込んだジェネレータを作って欲しいと頼んだら、ほとんど一瞬で作ってくれて、ちゃんと動作してくれて、感動したっていうお話。

画面はこんな感じ

HTML+JavaScriptのソースはこちら

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>架空個人データ ジェネレーター(オフライン)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root{--fg:#222;--bg:#fff;--muted:#666;--pri:#2563eb;--bdr:#ddd}
  body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Noto Sans JP","Apple Color Emoji","Segoe UI Emoji";color:var(--fg);background:var(--bg);margin:0}
  header{padding:18px 20px;border-bottom:1px solid var(--bdr)}
  header h1{font-size:18px;margin:0 0 4px}
  header p{margin:0;color:var(--muted);font-size:13px}
  main{max-width:1100px;margin:20px auto;padding:0 16px 40px}
  .panel{border:1px solid var(--bdr);border-radius:8px;padding:14px;margin-bottom:16px}
  .row{display:flex;flex-wrap:wrap;gap:12px}
  .row > div{flex:1 1 220px;min-width:220px}
  label{font-size:14px}
  input[type="number"],input[type="text"]{width:100%;padding:8px;border:1px solid var(--bdr);border-radius:6px;font-size:14px}
  fieldset{border:none;padding:0;margin:0}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;margin-top:8px}
  .btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
  button{background:var(--pri);color:#fff;border:none;border-radius:8px;padding:9px 14px;font-weight:600;cursor:pointer}
  button.secondary{background:#f3f4f6;color:#111;border:1px solid var(--bdr)}
  button:disabled{opacity:.6;cursor:not-allowed}
  .note{color:var(--muted);font-size:12px;margin-top:6px}
  table{width:100%;border-collapse:collapse;margin-top:10px;font-size:13px}
  th,td{border-bottom:1px solid #eee;padding:8px 6px;text-align:left;vertical-align:top}
  thead th{position:sticky;top:0;background:#fafafa}
  .summary{font-size:13px;color:var(--muted);margin:6px 0}
  .danger{color:#b91c1c}
  .ok{color:#047857}
  .footer{font-size:12px;color:var(--muted);margin-top:12px}
  .right{margin-left:auto}
  @media (max-width:640px){table{display:block;overflow:auto}}
</style>
</head>
<body>
<header>
  <h1>架空個人データ ジェネレーター(HTML + JavaScript)</h1>
  <p>選択した項目で1~1000件のテストデータを生成。すべて架空データです。</p>
</header>
<main>
  <section class="panel">
    <div class="row">
      <div>
        <label for="count">データ件数(1~1000)</label>
        <input id="count" type="number" min="1" max="1000" step="1" value="100" />
      </div>
      <div>
        <label for="seed">乱数シード(任意:同じシードで再現可)</label>
        <input id="seed" type="text" placeholder="例: 2025-11-01 or abcd1234" />
      </div>
    </div>

    <fieldset style="margin-top:12px">
      <legend style="font-weight:700;margin-bottom:6px">項目を選択</legend>
      <div class="grid">
        <label><input type="checkbox" class="field" value="id" checked> ID</label>
        <label><input type="checkbox" class="field" value="employeeCode" checked> 社員コード(10桁)</label>
        <label><input type="checkbox" class="field" value="taskId" checked> タスクID(T+6桁)</label>
        <label><input type="checkbox" class="field" value="date" checked> 日付(yyyy/MM/dd)</label>
        <label><input type="checkbox" class="field" value="priority" checked> 優先順位(高/中/低)</label>

        <label><input type="checkbox" class="field" value="name" checked> 氏名</label>
        <label><input type="checkbox" class="field" value="kana" checked> フリガナ</label>
        <label><input type="checkbox" class="field" value="gender" checked> 性別</label>
        <label><input type="checkbox" class="field" value="birthday" checked> 生年月日</label>
        <label><input type="checkbox" class="field" value="age" checked> 年齢</label>
        <label><input type="checkbox" class="field" value="email" checked> メール</label>
        <label><input type="checkbox" class="field" value="mobile" checked> 携帯電話</label>
        <label><input type="checkbox" class="field" value="phone"> 固定電話</label>
        <label><input type="checkbox" class="field" value="zip" checked> 郵便番号</label>
        <label><input type="checkbox" class="field" value="pref" checked> 都道府県</label>
        <label><input type="checkbox" class="field" value="city" checked> 市区町村</label>
        <label><input type="checkbox" class="field" value="address" checked> 住所(番地)</label>
      </div>
    </fieldset>

    <div class="btns">
      <button id="genBtn">生成</button>
      <button id="clearBtn" class="secondary">クリア</button>
      <span class="note">※ 生成はすべてブラウザ内で完結し、サーバー送信は行いません。</span>
    </div>
  </section>

  <section class="panel">
    <div class="btns">
      <button id="dlCsv">CSVダウンロード</button>
      <button id="dlJson" class="secondary">JSONダウンロード</button>
      <button id="copyBtn" class="secondary">コピー(CSV)</button>
      <span class="right summary" id="summary">未生成</span>
    </div>
    <div id="warn" class="summary danger" style="display:none"></div>
    <div id="ok" class="summary ok" style="display:none"></div>
    <div id="tableWrap"></div>
    <div class="footer">※ CSVはUTF-8(BOM付き)。Excelでも文字化けしにくくしています。</div>
  </section>
</main>

<script>
/* ====== 調整しやすい定数(「日付」の範囲など) ====== */
const RANDOM_DATE_MIN = new Date(Date.UTC(1990, 0, 1));   // 1990-01-01
const RANDOM_DATE_MAX = new Date(Date.UTC(2035, 11, 31)); // 2035-12-31

/* ===== 小さなユーティリティ(シード付き乱数、日付、IDなど) ===== */

// 文字列→32bitハッシュ
function hash32(str){
  let h = 2166136261 >>> 0;
  for (let i=0;i<str.length;i++){
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}
// Mulberry32: 軽量なシード乱数
function mulberry32(seed){
  let t = seed >>> 0;
  return function(){
    t += 0x6D2B79F5;
    let r = Math.imul(t ^ (t >>> 15), 1 | t);
    r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
    return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
  }
}
function choice(rng, arr){ return arr[Math.floor(rng()*arr.length)]; }
function intN(rng, min, max){ // [min, max]
  return Math.floor(rng()*(max-min+1)) + min;
}
function pad(n, len){ return String(n).padStart(len,'0'); }

function randDate(rng, minY, maxY){
  const y = intN(rng, minY, maxY);
  const m = intN(rng, 1, 12);
  const lastDay = new Date(y, m, 0).getDate();
  const d = intN(rng, 1, lastDay);
  const dt = new Date(Date.UTC(y, m-1, d));
  return dt;
}
// 任意範囲からランダム日付
function randDateBetween(rng, minDate, maxDate){
  const minMs = minDate.getTime();
  const maxMs = maxDate.getTime();
  const t = minMs + Math.floor(rng() * (maxMs - minMs + 1));
  return new Date(t);
}
function toISODate(d){
  return d.getUTCFullYear()+'-'+pad(d.getUTCMonth()+1,2)+'-'+pad(d.getUTCDate(),2);
}
// yyyy/MM/dd
function toSlashDate(d){
  const y = d.getUTCFullYear();
  const m = pad(d.getUTCMonth()+1, 2);
  const day = pad(d.getUTCDate(), 2);
  return `${y}/${m}/${day}`;
}
function calcAge(birth){
  const today = new Date();
  let age = today.getFullYear() - birth.getUTCFullYear();
  const m = today.getMonth() + 1;
  const bdM = birth.getUTCMonth()+1;
  const bdD = birth.getUTCDate();
  if (m < bdM || (m===bdM && today.getDate() < bdD)) age--;
  return age;
}
// UUID v4(簡易)
function uuidv4(rng){
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c=>{
    const r = Math.floor(rng()*16);
    const v = (c==='x') ? r : (r&0x3|0x8);
    return v.toString(16);
  });
}

/* ===== データ候補(日本語名・かな・ローマ字をペアで最小限) ===== */
const SURNAMES = [
  {kanji:'佐藤',kana:'サトウ',romaji:'sato'},
  {kanji:'鈴木',kana:'スズキ',romaji:'suzuki'},
  {kanji:'高橋',kana:'タカハシ',romaji:'takahashi'},
  {kanji:'田中',kana:'タナカ',romaji:'tanaka'},
  {kanji:'伊藤',kana:'イトウ',romaji:'ito'},
  {kanji:'山本',kana:'ヤマモト',romaji:'yamamoto'},
  {kanji:'中村',kana:'ナカムラ',romaji:'nakamura'},
  {kanji:'小林',kana:'コバヤシ',romaji:'kobayashi'},
  {kanji:'加藤',kana:'カトウ',romaji:'kato'},
  {kanji:'吉田',kana:'ヨシダ',romaji:'yoshida'}
];

const GIVEN_MALE = [
  {kanji:'太郎',kana:'タロウ',romaji:'taro'},
  {kanji:'翔',kana:'ショウ',romaji:'sho'},
  {kanji:'蓮',kana:'レン',romaji:'ren'},
  {kanji:'陽斗',kana:'ハルト',romaji:'haruto'},
  {kanji:'颯太',kana:'ソウタ',romaji:'sota'},
  {kanji:'大和',kana:'ヤマト',romaji:'yamato'},
  {kanji:'蒼',kana:'アオイ',romaji:'aoi'},
  {kanji:'悠真',kana:'ユウマ',romaji:'yuma'},
  {kanji:'陸',kana:'リク',romaji:'riku'},
  {kanji:'湊',kana:'ミナト',romaji:'minato'}
];
const GIVEN_FEMALE = [
  {kanji:'花子',kana:'ハナコ',romaji:'hanako'},
  {kanji:'葵',kana:'アオイ',romaji:'aoi'},
  {kanji:'結衣',kana:'ユイ',romaji:'yui'},
  {kanji:'陽菜',kana:'ヒナ',romaji:'hina'},
  {kanji:'さくら',kana:'サクラ',romaji:'sakura'},
  {kanji:'美咲',kana:'ミサキ',romaji:'misaki'},
  {kanji:'凛',kana:'リン',romaji:'rin'},
  {kanji:'楓',kana:'カエデ',romaji:'kaede'},
  {kanji:'愛',kana:'アイ',romaji:'ai'},
  {kanji:'結菜',kana:'ユイナ',romaji:'yuina'}
];

const PREFS = [
 '北海道','青森県','岩手県','宮城県','秋田県','山形県','福島県',
 '茨城県','栃木県','群馬県','埼玉県','千葉県','東京都','神奈川県',
 '新潟県','富山県','石川県','福井県','山梨県','長野県',
 '岐阜県','静岡県','愛知県','三重県',
 '滋賀県','京都府','大阪府','兵庫県','奈良県','和歌山県',
 '鳥取県','島根県','岡山県','広島県','山口県',
 '徳島県','香川県','愛媛県','高知県',
 '福岡県','佐賀県','長崎県','熊本県','大分県','宮崎県','鹿児島県','沖縄県'
];

const CITY_SUFFIX = ['市','区','町','村'];
const CITY_BASE = ['中央','北','南','東','西','旭','桜','若葉','美浜','青葉','緑','千代田','港','大和','藤','川','山田','高崎','堺','東雲','新都','朝日'];
const TOWN_SUFFIX = ['町','丁目','ヶ丘','台','の里','原','新田','本町','松原'];

/* ===== 擬似データ生成器 ===== */
function genMobile(rng){
  const head = choice(rng, ['090','080','070']);
  return `${head}-${pad(intN(rng,0,9999),4)}-${pad(intN(rng,0,9999),4)}`;
}
function genPhone(rng){
  const head = choice(rng, ['03','045','06','052','075','082','092','011']);
  return `${head}-${pad(intN(rng,100,9999),4)}-${pad(intN(rng,0,9999),4)}`;
}
function genZip(rng){
  return `${pad(intN(rng,0,999),3)}-${pad(intN(rng,0,9999),4)}`;
}
function genAddress(rng){
  const pref = choice(rng, PREFS);
  const city = choice(rng, CITY_BASE) + choice(rng, CITY_SUFFIX);
  const town = choice(rng, CITY_BASE) + choice(rng, TOWN_SUFFIX);
  const chome = intN(rng,1,6);
  const ban = intN(rng,1,20);
  const go = intN(rng,1,30);
  const addr = `${town}${chome}丁目${ban}-${go}`;
  return {pref, city, address: addr};
}
function genName(rng){
  const gender = rng() < 0.5 ? '男' : '女';
  const sur = choice(rng, SURNAMES);
  const giv = gender === '男' ? choice(rng, GIVEN_MALE) : choice(rng, GIVEN_FEMALE);
  const name = `${sur.kanji} ${giv.kanji}`;
  const kana = `${sur.kana} ${giv.kana}`;
  const romaji = `${giv.romaji}.${sur.romaji}`;
  return {gender, name, kana, romaji};
}
function genEmail(rng, romaji){
  const num = pad(intN(rng,0,99),2);
  const domain = choice(rng, ['example.jp','sample.co.jp','test.local','mail.example']);
  return `${romaji.replace(/[^a-z.]/g,'')}${num}@${domain}`.toLowerCase();
}

/* === 新規: 社員コード(10桁数値) === */
function genEmployeeCode(rng){
  let s = '';
  for (let i=0;i<10;i++){ s += intN(rng,0,9); }
  return s;
}
/* === 新規: タスクID(T + 6桁) === */
function genTaskId(rng){
  return 'T' + pad(intN(rng,0,999999), 6);
}
/* === 新規: 過去~未来のランダム日付(yyyy/MM/dd)=== */
function genRandomDateStr(rng){
  const d = randDateBetween(rng, RANDOM_DATE_MIN, RANDOM_DATE_MAX);
  // UTC基準で整形(時差の影響を避ける)
  const utc = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
  return toSlashDate(utc);
}
/* === 新規: 優先順位 === */
function genPriority(rng){
  return choice(rng, ['高','中','低']);
}

function buildRecord(rng){
  const id = uuidv4(rng);
  const person = genName(rng);
  const bd = randDate(rng, 1955, 2015);
  const age = calcAge(bd);
  const mobile = genMobile(rng);
  const phone = genPhone(rng);
  const zip = genZip(rng);
  const addr = genAddress(rng);
  const email = genEmail(rng, person.romaji);
  // 新規項目の生成
  const employeeCode = genEmployeeCode(rng);
  const taskId = genTaskId(rng);
  const date = genRandomDateStr(rng);
  const priority = genPriority(rng);

  return {
    id,
    employeeCode, taskId, date, priority,
    name: person.name, kana: person.kana, gender: person.gender,
    birthday: toISODate(bd), age,
    email, mobile, phone,
    zip, pref: addr.pref, city: addr.city, address: addr.address
  };
}

/* ===== CSV / JSON 出力 ===== */
function toCSV(rows, headers){
  const escape = (v)=>{
    if (v === null || v === undefined) return '';
    const s = String(v);
    if (/[",\n]/.test(s)) return '"' + s.replace(/"/g,'""') + '"';
    return s;
  };
  const head = headers.join(',');
  const body = rows.map(r => headers.map(h=>escape(r[h])).join(',')).join('\n');
  return head + '\n' + body;
}
function downloadText(filename, text, withBOM=false){
  const blob = new Blob([withBOM ? '\uFEFF' + text : text], {type:'text/plain;charset=utf-8'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  a.click();
  URL.revokeObjectURL(a.href);
}

/* ===== UI ロジック ===== */
let DATA = [];
let HEADERS = [];

function getSelectedFields(){
  const checks = Array.from(document.querySelectorAll('.field:checked')).map(el=>el.value);
  return checks;
}

function renderTable(rows, headers){
  const wrap = document.getElementById('tableWrap');
  if (!rows.length){ wrap.innerHTML = ''; return; }
  const thead = `<thead><tr>${headers.map(h=>`<th>${h}</th>`).join('')}</tr></thead>`;
  const tbody = `<tbody>` + rows.map(r=>{
    return `<tr>` + headers.map(h=>`<td>${r[h] ?? ''}</td>`).join('') + `</tr>`;
  }).join('') + `</tbody>`;
  wrap.innerHTML = `<table>${thead}${tbody}</table>`;
}

function showMsg(id, text){
  const el = document.getElementById(id);
  el.style.display = text ? '' : 'none';
  el.textContent = text || '';
}

document.getElementById('genBtn').addEventListener('click', ()=>{
  showMsg('warn',''); showMsg('ok','');
  const countEl = document.getElementById('count');
  const seedStr = (document.getElementById('seed').value || '').trim();
  let n = Number(countEl.value);
  if (!Number.isInteger(n) || n < 1 || n > 1000){
    showMsg('warn','件数は1~1000の整数で指定してください。');
    return;
  }
  const fields = getSelectedFields();
  if (!fields.length){
    showMsg('warn','少なくとも1つの項目を選択してください。');
    return;
  }
  // シード決定
  const seed = seedStr ? hash32(seedStr) : hash32(String(Date.now()));
  const rng = mulberry32(seed);

  // 生成
  const rows = [];
  for (let i=0; i<n; i++){
    rows.push(buildRecord(rng));
  }
  // 列の順序(選択順に合わせる)
  const headers = fields;
  DATA = rows.map(r=>{
    const o = {};
    headers.forEach(h=>{ o[h] = r[h]; });
    return o;
  });
  HEADERS = headers;

  renderTable(DATA, HEADERS);
  document.getElementById('summary').textContent = `生成件数: ${DATA.length}件 / 項目数: ${HEADERS.length}(シード: ${seedStr || '自動'})`;
  showMsg('ok', '生成が完了しました。');
});

document.getElementById('clearBtn').addEventListener('click', ()=>{
  DATA = []; HEADERS = [];
  renderTable([], []);
  document.getElementById('summary').textContent = '未生成';
  showMsg('warn',''); showMsg('ok','');
});

document.getElementById('dlCsv').addEventListener('click', ()=>{
  if (!DATA.length){ showMsg('warn','先にデータを生成してください。'); return; }
  const csv = toCSV(DATA, HEADERS);
  downloadText('testdata.csv', csv, true); // BOM付き
});

document.getElementById('dlJson').addEventListener('click', ()=>{
  if (!DATA.length){ showMsg('warn','先にデータを生成してください。'); return; }
  const json = JSON.stringify(DATA, null, 2);
  downloadText('testdata.json', json, false);
});

document.getElementById('copyBtn').addEventListener('click', async ()=>{
  if (!DATA.length){ showMsg('warn','先にデータを生成してください。'); return; }
  const csv = toCSV(DATA, HEADERS);
  try{
    await navigator.clipboard.writeText(csv);
    showMsg('ok','CSVをクリップボードへコピーしました。');
  }catch(e){
    showMsg('warn','コピーに失敗しました。ダウンロードをご利用ください。');
  }
});
</script>
</body>
</html>

このままメモ帳に貼り付けて、拡張子をHTMLに変更するだけで動く。
細かい項目の調整はソースを改めてCopilotに投げるか、皆さんの力でちょちょいとソースを変更すれば何とかなる。

広告

目次