日程Fit|「いつ空いてますか?」の往復はもう不要。候補日を選んでURLを送るだけ|登録不要|今すぐ無料で使う →

Microsoft Edgeで公共ポータルのデータを抽出する完全ガイド|JavaScript・Playwright・WebView2・拡張機能・文字化け対策

給与明細・納税・申請履歴などの公共ポータルは、Ajaxや文字コードの違い、SPA化によって「画面には見えるのに取得できない」問題が起きがちです。本記事では Microsoft Edge(Chromium)を前提に、合法性と堅牢性、パフォーマンスを両立したデータ抽出の考え方と実装手順を、実運用レベルのコードとともに丁寧に解説します。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

想定シナリオとゴール

対象は「自分または組織が適法にアクセスできる」公共系ポータル(給与明細、年末調整、税・保険、申請管理など)。最終ゴールは、取得の安定化(動的DOM・Ajax・文字化け対策)適切な実装形態の選定(ブラウザ内JS/拡張/WebView2/ヘッドレス)パフォーマンス最適化・監査ログ・セキュリティを満たすことです。

結論(最短ルート)

  • 第一選択は公式API/構造化エンドポイント:JSONやCSVが公開されていればfetch()で直取りが最も堅実。
  • HTMLのみならDOMパースquerySelectorAllで抽出し、動的要素はMutationObserverで安定化。
  • Ajaxの実体を見つける:Edge DevToolsのNetworkでXHR/FetchのURL・形式・文字コードを特定。
  • 文字化けは明示デコードarrayBuffer()TextDecoderでUTF-8/Shift_JIS/EUC-JP等を確定。
  • レンダリング後DOMが必須なら:Playwright/Puppeteer(Edgeチャンネル)やWebView2/拡張機能で自動化。
  • 法令順守と最小権限:利用規約・robots.txt・個人情報保護に配慮し、資格情報の安全管理を徹底。

アーキテクチャ比較(早見表)

方式主な用途長所注意点運用適性
ブラウザ内JavaScript(ブックマークレット/DevTools/Userscript)都度の手動取得、素早い試行導入が速い、CORSの壁が少ない(同一タブ)手動依存、拡張性に限界単発・小規模
Edge 拡張機能(Manifest V3)日常業務での半自動抽出Content ScriptでDOM直読、権限と保存先を分離審査・権限設計、MV3の非同期SWに慣れが必要中規模・配布前提
WebView2(.NET/Win32)業務アプリに組込み、オフライン連携ランタイム一体、双方向メッセージで堅牢デスクトップ配布・更新設計が必要社内配布・監査重視
Playwright / Puppeteer(ヘッドレスEdge)定期バッチ、レンダリング依存ページ待機・操作が強力、CI/CDやタスクスケジューラと好相性実行環境の準備・ランタイムコスト定期・大量・自動

第一選択:公式API/構造化データを使う

多くの公共ポータルは、画面裏でJSON/XLSX/CSVのエンドポイントを叩いています。NetworkタブでURLとパラメータ、認可方式(Cookie、トークン、Referer等)を特定し、fetch()で直接取得しましょう。304(未変更)を活用して差分取得し、ETag/If-None-MatchやIf-Modified-Sinceを使えば通信量を削減できます。

// 再試行・差分対応つきfetch(JSON前提)
async function fetchJsonWithCache(url, { etag, maxRetries = 3, ...init } = {}) {
  for (let i = 0; i < maxRetries; i++) {
    const headers = new Headers(init?.headers || {});
    if (etag) headers.set('If-None-Match', etag);
    const res = await fetch(url, { ...init, headers, credentials: 'include' });
    if (res.status === 304) return { status: 304, etag, data: null };
    if (!res.ok) {
      // 429/503は指数バックオフ
      if (i < maxRetries - 1 && [429, 503].includes(res.status)) {
        await new Promise(r => setTimeout(r, 2 ** i * 500));
        continue;
      }
      throw new Error(`HTTP ${res.status}`);
    }
    const newEtag = res.headers.get('ETag') || null;
    const data = await res.json();
    return { status: 200, etag: newEtag, data };
  }
}

APIが見つからない/認証が厳格な場合

  • NetworkタブでXHR/Fetchを「type: xhr」に絞り、レスポンスがJSON/CSVかを確認。
  • 画面でフィルタ入力後にURLやQueryStringが変化するか観察。
  • トークンはセッション固有の場合があるため、毎回の取得前に当該画面を先に開くフローに。

HTMLのみ:DOMパースで確実に抜く

構造化エンドポイントが無い場合は、DOMから安全に抽出します。textContent.trim()で空白を整理し、列ヘッダが<th>で与えられている場合はキーとして利用します。

// テーブル → 配列(ヘッダ対応)
function tableToObjects(table) {
  const headers = [...table.querySelectorAll('thead th')].map(th =&gt; th.textContent.trim());
  const rows = [...table.querySelectorAll('tbody tr')].map(tr =&gt; {
    const cells = [...tr.querySelectorAll('td')].map(td =&gt; td.textContent.trim());
    if (headers.length === cells.length) {
      return Object.fromEntries(headers.map((h, i) =&gt; [h, cells[i]]));
    }
    return cells;
  });
  return rows;
}

// 画面上の最初のテーブルを抽出し、CSVでダウンロード
(function () {
  const table = document.querySelector('table');
  if (!table) { console.warn('table not found'); return; }
  const data = tableToObjects(table);
  const headers = Array.isArray(data[0]) ? data[0].map((_,i)=&gt;`Col${i+1}`) : Object.keys(data[0] || {});
  const rows = data.map(r =&gt; Array.isArray(r) ? r : headers.map(h =&gt; r[h] ?? ''));
  const csv = [headers, ...rows].map(cols =&gt; cols.map(v =&gt; `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\r\n');
  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = `export_${new Date().toISOString().slice(0,10)}.csv`;
  a.click(); URL.revokeObjectURL(url);
})();

動的追加要素はMutationObserverで安定化

// 目的のノードが現れるまで待機してから抽出
function waitForNode(selector, { timeout = 10000, root = document } = {}) {
  return new Promise((resolve, reject) => {
    const found = root.querySelector(selector);
    if (found) return resolve(found);
    const obs = new MutationObserver(() => {
      const el = root.querySelector(selector);
      if (el) { obs.disconnect(); resolve(el); }
    });
    obs.observe(root, { childList: true, subtree: true });
    if (timeout) setTimeout(() => { obs.disconnect(); reject(new Error('timeout')); }, timeout);
  });
}

// 例:Ajaxで描画される給与明細テーブルを待って抽出
(async () => {
try {
const table = await waitForNode('table#payslip');
console.table(tableToObjects(table));
} catch (e) {
console.error('抽出失敗', e);
}
})(); 

Ajaxの実体を特定する(Edge DevTools)

EdgeでF12NetworkFetch/XHRを選び、画面操作に伴って飛ぶリクエストを観察します。URL、クエリ、レスポンスのContent-Typecharset、Cookie/ヘッダ、ステータス、キャッシュ制御を確認し、直接fetch()するか、必要な前処理(トークン取得や画面遷移)を設計します。Preview/ResponseパネルでJSON/CSV/HTMLかを瞬時に見分けるのがコツです。

日本語の文字化けを根治する:TextDecoder戦略

公共ポータルではUTF-8/Shift_JIS/CP932/EUC-JP/UTF-16が混在します。fetch().arrayBuffer()で生バイトを取り、Content-Type<meta charset>からエンコーディングを推定し、TextDecoderで明示的にデコードします。

function sniffEncodingFromHeadersAndHtml(contentType, headChunk) {
  // 1) Content-Typeヘッダ
  if (contentType) {
    const m = contentType.match(/charset\s*=\s*([^\s;]+)/i);
    if (m) return m[1].toLowerCase();
  }
  // 2) HTML先頭からmeta読み取り(ASCIIで十分)
  const ascii = new TextDecoder('iso-8859-1', { fatal: false }).decode(headChunk);
  const m2 = ascii.match(/&lt;meta[^&gt;]*charset=["']?\s*([\w\-]+)\s*["']?/i);
  if (m2) return m2[1].toLowerCase();
  return null;
}

function decodeWithFallback(buffer, contentType) {
  const bytes = new Uint8Array(buffer);
  // BOM
  if (bytes[0] === 0xEF &amp;&amp; bytes[1] === 0xBB &amp;&amp; bytes[2] === 0xBF) {
    return new TextDecoder('utf-8').decode(bytes.subarray(3));
  }
  if ((bytes[0] === 0xFF &amp;&amp; bytes[1] === 0xFE) || (bytes[0] === 0xFE &amp;&amp; bytes[1] === 0xFF)) {
    return new TextDecoder('utf-16').decode(bytes);
  }
  const head = bytes.subarray(0, Math.min(bytes.length, 2048));
  const enc = sniffEncodingFromHeadersAndHtml(contentType, head) || 'utf-8';
  const candidates = enc.toLowerCase().includes('shift') ? ['shift_jis','cp932','utf-8'] :
                     enc.toLowerCase().includes('euc')    ? ['euc-jp','utf-8','shift_jis'] :
                     [enc,'utf-8','shift_jis','euc-jp'];
  for (const name of candidates) {
    try {
      const txt = new TextDecoder(name).decode(bytes);
      // 置換文字率が高い場合は次候補
      const bad = (txt.match(/\uFFFD/g) || []).length / Math.max(1, txt.length);
      if (bad &lt; 0.01) return txt;
    } catch (e) {}
  }
  // 最後にUTF-8で
  return new TextDecoder('utf-8').decode(bytes);
}

// 汎用:HTMLを文字化けなく取得
async function fetchHtmlDecoded(url, init = {}) {
  const res = await fetch(url, { ...init, credentials: 'include' });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const buf = await res.arrayBuffer();
  const text = decodeWithFallback(buf, res.headers.get('content-type'));
  return new DOMParser().parseFromString(text, 'text/html');
}

レンダリング後が必須なら:Playwright / Puppeteer(Edge)

SPAやCanvas描画など「ネットワークだけでは完結しない」場合は、ヘッドレスにページを開き、必要なUI操作と待機の上でDOMから取ります。Edgeをチャンネル指定すればローカルと近い挙動を再現できます。

// Playwright(Edge指定)。Node.jsで実行
const { chromium } = require('playwright');

(async () =&gt; {
  const browser = await chromium.launch({ channel: 'msedge', headless: true });
  const ctx = await browser.newContext({
    viewport: { width: 1280, height: 800 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge',
  });
  const page = await ctx.newPage();
  await page.goto('https://portal.example.go.jp/login', { waitUntil: 'domcontentloaded' });

  // 環境変数で資格情報を渡す(ハードコード禁止)
  await page.fill('#userid', process.env.PAYROLL_USER || '');
  await page.fill('#password', process.env.PAYROLL_PASS || '');
  await page.click('button[type=submit]');

  // 一覧画面へ
  await page.waitForURL('**/payslip');
  await page.waitForLoadState('networkidle');

  // Ajaxの実体も保存(将来の直取り用)
  page.on('response', async (res) =&gt; {
    try {
      const req = res.request();
      if (['xhr','fetch'].includes(req.resourceType())) {
        const url = req.url();
        if (url.includes('/api/payslip')) {
          const json = await res.json();
          console.log('API例', url, JSON.stringify(json).slice(0,200));
        }
      }
    } catch {}
  });

  // DOMから抽出
  const rows = await page.$$eval('table#payslip tbody tr', trs =&gt;
    trs.map(tr =&gt; [...tr.querySelectorAll('td')].map(td =&gt; td.textContent.trim()))
  );

  // CSV保存
  const csv = rows.map(r =&gt; r.map(v =&gt; `"${v.replace(/"/g,'""')}"`).join(',')).join('\r\n');
  const fs = require('fs');
  fs.writeFileSync('payslip.csv', csv, 'utf8');

  await browser.close();
})();

Edge特有の選択肢:WebView2 と 拡張機能(MV3)

WebView2でデスクトップに組み込む

WebView2はEdgeランタイムをホストして動くため、CoreWebView2を介してスクリプト注入・双方向メッセージが可能です。企業内配布・監査要件が強い案件で有効です。

// WPFの例(.NET 6+)
using Microsoft.Web.WebView2.Core;
using System.Text.Json;

public partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
    InitAsync();
  }

  private async void InitAsync()
  {
    await webView2.EnsureCoreWebView2Async();
    webView2.CoreWebView2.WebMessageReceived += (s, e) =&gt;
    {
      var json = e.TryGetWebMessageAsString();
      File.WriteAllText("export.json", json);
    };

    // DOMロード時に実行する抽出スクリプトを登録
    await webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@"
      (function(){
        const toObjects = (table) =&gt; {
          const hs = [...table.querySelectorAll('thead th')].map(th =&gt; th.textContent.trim());
          return [...table.querySelectorAll('tbody tr')].map(tr =&gt; {
            const cs = [...tr.querySelectorAll('td')].map(td =&gt; td.textContent.trim());
            if (hs.length === cs.length) return Object.fromEntries(hs.map((h,i)=&gt;[h,cs[i]]));
            return cs;
          });
        };
        new MutationObserver(() =&gt; {
          const t = document.querySelector('table#payslip');
          if (!t) return;
          const data = toObjects(t);
          window.chrome.webview.postMessage(JSON.stringify({ at: new Date().toISOString(), data }));
        }).observe(document, {childList:true, subtree:true});
      })();
    ");

    webView2.Source = new Uri("https://portal.example.go.jp/payslip");
  }
}

Edge拡張機能(Manifest V3)で分離実装

Content Scriptは「閲覧中ページのDOM取得」に専念し、保存・スケジューリング・外部送信はService Worker(背景)に任せると堅牢です。権限は最小限に。

manifest.json

{
  "manifest_version": 3,
  "name": "Portal Extractor",
  "version": "1.0.0",
  "permissions": ["storage","scripting","activeTab","downloads","alarms"],
  "host_permissions": ["https://portal.example.go.jp/*"],
  "background": { "service_worker": "background.js" },
  "action": { "default_title": "Extract data" },
  "content_scripts": [{
    "matches": ["https://portal.example.go.jp/*"],
    "js": ["content.js"],
    "run_at": "document_idle"
  }]
}

content.js(抽出のみ担当)

function tableToObjects(table) {
  const headers = [...table.querySelectorAll('thead th')].map(th =&gt; th.textContent.trim());
  return [...table.querySelectorAll('tbody tr')].map(tr =&gt; {
    const cells = [...tr.querySelectorAll('td')].map(td =&gt; td.textContent.trim());
    if (headers.length === cells.length) return Object.fromEntries(headers.map((h,i)=&gt;[h,cells[i]]));
    return cells;
  });
}
function waitForTable(id) {
  return new Promise(resolve =&gt; {
    const t = document.querySelector(`table#${id}`);
    if (t) return resolve(t);
    const mo = new MutationObserver(() =&gt; {
      const el = document.querySelector(`table#${id}`);
      if (el) { mo.disconnect(); resolve(el); }
    });
    mo.observe(document, { childList: true, subtree: true });
  });
}
(async () =&gt; {
  const table = await waitForTable('payslip');
  const data = tableToObjects(table);
  chrome.runtime.sendMessage({ type: 'EXTRACTED', payload: { at: Date.now(), data } });
})();

background.js(保存・スケジュール担当)

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create('daily', { periodInMinutes: 24 * 60 });
});

chrome.alarms.onAlarm.addListener(alarm => {
if (alarm.name === 'daily') {
// 必要なら特定タブを開いて自動抽出(ログインが必要ならユーザー操作で)
chrome.tabs.create({ url: '[https://portal.example.go.jp/payslip](https://portal.example.go.jp/payslip)' });
}
});

chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg?.type === 'EXTRACTED') {
const rows = msg.payload.data;
const headers = Array.isArray(rows[0]) ? rows[0].map((_,i)=>`Col${i+1}`) : Object.keys(rows[0] || {});
const csv = [headers, ...rows.map(r => Array.isArray(r) ? r : headers.map(h => r[h] ?? ''))]
.map(cols => cols.map(v => `"${String(v).replace(/"/g,'""')}"`).join(','))
.join('\r\n');


const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
chrome.downloads.download({ url, filename: `payslip_${new Date().toISOString().slice(0,10)}.csv`, saveAs: false });


}
}); 

セレクタ設計と堅牢化のコツ

  • 安定識別子を優先iddata-属性(例:[data-testid="row"])。
  • 構造・位置依存を避けるtbody > trのみに依存し、列順変更に備えてヘッダ名でマッピング。
  • 代替経路:テキストに基づく探索が必要ならXPathを併用(document.evaluate)。
  • 待機戦略networkidleMutationObserverで過不足ないタイミングで抽出。

パフォーマンス最適化(実践値)

施策効果実装例注意点
差分取得(ETag/304)通信量を大幅削減上述のfetchJsonWithCacheサーバがETag対応か確認
Web WorkersCSV整形など重処理を別スレッドnew Worker('csv.worker.js')ドメイン制約・バンドル構成
バルク処理の並列数制御回線・サーバ負荷の最適化Promiseプールで同時数3〜6429/503で退避(バックオフ)
requestIdleCallbackUIブロック回避スクロールと同時抽出回避フォールバック実装

セキュリティ・コンプライアンス

  • 権限確認:サイト利用規約、robots.txt、組織の情報セキュリティ規程を確認。個人情報は目的外利用禁止
  • 資格情報の管理:パスワードのハードコード厳禁。環境変数、OSの資格情報マネージャ、企業向け秘密管理を使用。
  • 最小権限:拡張機能のhost_permissionsは対象ドメインのみに限定。
  • データ保管:ローカル暗号化、アクセス制御、削除ポリシーを定義。JSON/CSVにマスキングを検討。
  • CORSとCSRF:ブラウザ内からのクロスオリジン直取りはCORSに従う。必要であれば拡張機能やサーバ側プロキシを利用。
  • 多要素認証:2FAを迂回しない。自動化はユーザー操作を前提にトリガーするか、正式なトークン取得フローを利用。
  • XSS/HTML注入:抽出時にinnerHTMLを不用意に評価しない。textContentベースで処理。
  • 監査ログ:誰がいつどの範囲を取得したか、日付と件数・ハッシュを保存。

実用コード:Ajaxを可視化して経路を特定

// 一時的に貼って使うデバッグスニペット(DevTools Console)
(function(){
  const origFetch = window.fetch;
  window.fetch = async (...args) =&gt; {
    const res = await origFetch(...args);
    try {
      const url = String(args[0]);
      if (url.includes('payslip') || url.includes('api')) {
        console.log('[FETCH]', url, res.status, res.headers.get('content-type'));
      }
    } catch {}
    return res;
  };
  const open = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function(method, url) {
    this.addEventListener('load', function(){
      try {
        if (String(url).includes('payslip') || String(url).includes('api')) {
          console.log('[XHR]', method, url, this.status, this.getResponseHeader('content-type'));
        }
      } catch {}
    });
    return open.apply(this, arguments);
  };
  console.log('Ajax監視を開始しました');
})();

実用コード:文字コード不明なCSVを安全に読む

async function fetchCsvRows(url) {
  const res = await fetch(url, { credentials: 'include' });
  if (!res.ok) throw new Error(res.status);
  const buf = await res.arrayBuffer();
  const text = decodeWithFallback(buf, res.headers.get('content-type')); // 先述関数
  return text.split(/\r?\n/).filter(Boolean).map(line =&gt; {
    // シンプルCSVパーサ(ダブルクォート対応)
    const cols = [];
    let cur = '', inQ = false;
    for (let i=0;i&lt;line.length;i++) {
      const ch = line[i];
      if (inQ) {
        if (ch === '"' &amp;&amp; line[i+1] === '"') { cur += '"'; i++; }
        else if (ch === '"') { inQ = false; }
        else { cur += ch; }
      } else {
        if (ch === '"') inQ = true;
        else if (ch === ',') { cols.push(cur); cur = ''; }
        else { cur += ch; }
      }
    }
    cols.push(cur);
    return cols;
  });
}

運用:スケジューリング・ログ・監査

  • スケジュール:ヘッドレスはWindowsタスクスケジューラで日次/週次。拡張機能はalarmsでリマインド+ユーザー操作。
  • ログ:件数、対象期間、ハッシュ(SHA-256)をJSONで保存。失敗時はエラー種別とスタックを記録。
  • 再現性:入力条件(年月・部門等)をYAML/JSONで保存し、再取得の一貫性を担保。
  • データ品質:列名の正規化、数値・日付の型付け、欠損の扱いをルール化。

トラブルシューティングFAQ

症状原因の目安対処
画面に出るのにfetchでは空SPAレンダリング後、APIは別URLNetworkでXHR/FetchのURLを特定し直叩き。無ければPlaywright等でDOM抽出。
文字化け(濁点分離や「�」が多い)Shift_JIS/EUC-JPをUTF-8で読んでいるTextDecoderで明示デコード。BOM・meta・ヘッダから推定。
403/401で取得不可認証・トークン不足、Referer/Origin制約正規フローに従い画面を先に開く、セッションを継承、ヘッダの必要最小を確認。
429(レート制限)短時間の連打指数バックオフ、並列数を3〜6に制限、差分取得を徹底。
拡張機能が保存できない権限不足またはMV3のSW停止downloads権限を追加、イベント継続条件(メッセージ・アラーム)を整備。
CORSエラークロスオリジン制限同一タブで実行(Content Script)、または許可されたプロキシ/バックエンド経由。

チェックリスト(公開前に確認)

  • 抽出先は自分または組織がアクセス権を持つか。
  • まずNetworkでJSON/CSVの直取り有無を調査したか。
  • DOM抽出はMutationObserverで安定化しているか。
  • TextDecoderで文字コードを明示し、テストケース(UTF-8/SJIS/EUC-JP)を用意したか。
  • 拡張/WebView2/ヘッドレスのいずれか、要件に合う実装形態を選んだか。
  • 資格情報の安全管理、最小権限、監査ログ、削除ポリシーを定義したか。
  • HTTP 304/ETag・並列数制御・バックオフで負荷配慮ができているか。

ミニマム実装セット(コピペで出発)

次の3点を揃えれば、ほとんどの公共ポータルで「まず動く」状態になります。

  1. Ajaxログスニペット:通信経路を把握(上記「Ajaxを可視化」)。
  2. TextDecoder関数:文字化け対策(上記decodeWithFallback)。
  3. テーブル抽出&CSV化:DOMから確実に抜き保存(上記サンプル)。

まとめ

Microsoft Edgeで公共ポータルからデータを抽出する要諦は、①まず構造化データに直アクセス②無ければDOMを丁寧にパース③動的要素はMutationObserverで待つ④文字コードを明示的に扱う⑤必要に応じて拡張機能/WebView2/ヘッドレスを選択、そして⑥セキュリティ・監査を最優先の6点です。ここまでのサンプルを組み合わせれば、Edge環境でもパフォーマンスと安全性を保ちながら、動的サイトのデータ抽出を再現性高く運用できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次