Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Created March 23, 2026 14:17
Show Gist options
  • Select an option

  • Save kobitoDevelopment/ed32d587eedaf67108b717da5c992d76 to your computer and use it in GitHub Desktop.

Select an option

Save kobitoDevelopment/ed32d587eedaf67108b717da5c992d76 to your computer and use it in GitHub Desktop.
<a id="download-csv" download="data.csv">CSVダウンロード</a>
/**
* @file JSONデータをCSVに変換してダウンロードする機能を提供するスクリプト
*
* 処理の流れ:
* 1. DOMContentLoaded発火後、initApp()がconvertJsonToCsv(SAMPLE_DATA)を呼び出してCSV変換を開始する
* 2. convertJsonToCsv()内部でextractHeaders()を呼び出し、SAMPLE_DATAの全オブジェクトを走査して全キーを抽出する
* 3. 抽出したキー配列をヘッダー行として、各キーにescapeCsvField()を適用してカンマ区切り文字列にする
* 4. convertToCsvRows()を呼び出し、各オブジェクトの値をヘッダーの列順序に従って取り出す
* 5. convertToCsvRows()内部で各フィールドにescapeCsvField()を適用し、ダブルクォート・カンマ・改行を含む値をRFC 4180準拠でエスケープする
* 6. ヘッダー行とデータ行をCRLF(\r\n)で結合し、CSV文字列として返却する
* 7. downloadCsv()がCSV文字列の先頭にBOM(0xEF 0xBB 0xBF)を付与し、MIMEタイプ"text/csv"のBlobを生成する。このBlobがブラウザ上で.csvファイルとして扱えるオブジェクトとなる
* 8. URL.createObjectURL()でBlobのURLを生成し、HTMLに配置済みのaタグ(#download-csv)のhrefに設定する
* 9. ユーザーがaタグをクリックすると、ブラウザのデフォルト動作(download属性)によりCSVファイルがダウンロードされる
*/
/** @type {ReadonlyArray<{id: number, name: string, email: string, age: number, department: string}>} */
const SAMPLE_DATA = [
{ id: 1, name: "田中太郎", email: "tanaka@example.com", age: 30, department: "開発部" },
{ id: 2, name: "佐藤花子", email: "sato@example.com", age: 25, department: "営業部" },
{ id: 3, name: "鈴木一郎", email: "suzuki@example.com", age: 35, department: "人事部" },
{ id: 4, name: '山田"次郎"', email: "yamada@example.com", age: 28, department: "開発部" },
{ id: 5, name: "高橋美咲", email: "takahashi@example.com", age: 32, department: "営業部" },
];
/**
* CSVのヘッダー行を生成するために、オブジェクト配列から全キーを抽出する。
* オブジェクトごとにキーの有無が異なる場合でも、全行を網羅したヘッダーを得るために全オブジェクトを走査する。
* @param {Array<Record<string, unknown>>} objects - オブジェクト配列
* @returns {string[]} キー名の配列
*/
const extractHeaders = (objects) => {
const headerMap = {};
objects.forEach((obj) => {
Object.keys(obj).forEach((key) => {
headerMap[key] = true;
});
});
return Object.keys(headerMap);
};
/**
* CSV出力時にフィールド値が区切り文字やクォートを含むとパースが壊れるため、
* RFC 4180に準拠したエスケープ処理を適用する。
* @param {unknown} field - エスケープ対象の値
* @returns {string} エスケープ済みの文字列
*/
const escapeCsvField = (field) => {
const str = field == null ? "" : String(field);
if (str.includes('"') || str.includes(",") || str.includes("\n") || str.includes("\r")) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
/**
* ヘッダーの列順序に従って各オブジェクトの値をCSV行文字列に変換する。
* ヘッダーと列順序を一致させることで、CSVとしての整合性を保証する。
* @param {Array<Record<string, unknown>>} objects - オブジェクト配列
* @param {string[]} headers - ヘッダー(キー名)の配列
* @returns {string[]} CSV行の配列(ヘッダー行を含まない)
*/
const convertToCsvRows = (objects, headers) =>
objects.map((obj) => headers.map((header) => escapeCsvField(obj[header])).join(","));
/**
* ヘッダー抽出・行変換・エスケープの各処理を統合し、
* オブジェクト配列からダウンロード可能なCSV文字列を生成する。
* @param {Array<Record<string, unknown>>} data - 変換対象のオブジェクト配列
* @returns {string} CSV文字列
*/
const convertJsonToCsv = (data) => {
const headers = extractHeaders(data);
const headerRow = headers.map(escapeCsvField).join(",");
const bodyRows = convertToCsvRows(data, headers);
return [headerRow, ...bodyRows].join("\r\n");
};
/**
* ブラウザにはCSV文字列を直接保存するAPIがないため、
* BOM付きUTF-8のBlobを生成し、aタグのhrefに設定してダウンロードを実行する。
* BOMを付与するのはExcelで開いた際の文字化けを防ぐため。
* @param {HTMLAnchorElement} anchor - ダウンロード用のaタグ要素
* @param {string} csvString - ダウンロード対象のCSV文字列
* @returns {void}
*/
const downloadCsv = (anchor, csvString) => {
const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
const blob = new Blob([bom, csvString], { type: "text/csv;charset=utf-8" });
// BlobURLはページ遷移・タブ閉じ時にGCされるため、非SPAでは手動revokeは不要。
// SPAの場合はページ遷移前にURL.revokeObjectURL()で解放すること。
anchor.href = URL.createObjectURL(blob);
};
/**
* DOMContentLoaded後にダウンロードボタンへイベントリスナーを登録する。
* DOM構築完了後に実行することで、要素の取得失敗を防ぐ。
* @returns {void}
*/
const initApp = () => {
const downloadLink = document.getElementById("download-csv");
const csv = convertJsonToCsv(SAMPLE_DATA);
downloadCsv(downloadLink, csv);
};
document.addEventListener("DOMContentLoaded", initApp);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment