タイトル: CSVファイルダウンロード方法(Ajax)
SEOタイトル: Ajax で CSV ファイルをダウンロードする方法
| この記事の要点 |
|---|
クライアント側(JavaScript / fetch)
// モダンな書き方 (fetch + Blob)
async function downloadCsv() {
const response = await fetch("/api/users/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filters: { status: "active" } })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Blob として取得
const blob = await response.blob();
// ダウンロードリンクを作成して自動クリック
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "users.csv"; // ファイル名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url); // メモリ解放
}
jQuery 版
// jQuery.ajax を使う場合
$.ajax({
url: "/api/users/export",
method: "GET",
xhrFields: { responseType: "blob" }, // ← Blob として受け取る
success: function(blob, status, xhr) {
// Content-Disposition からファイル名取得
let filename = "download.csv";
const cd = xhr.getResponseHeader("Content-Disposition");
const match = cd && cd.match(/filename="?(.+?)"?$/);
if (match) filename = decodeURIComponent(match[1]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
},
error: function(xhr) {
alert("ダウンロード失敗: " + xhr.statusText);
}
});
サーバー側(Spring Boot)
@RestController
@RequestMapping("/api/users")
public class UserExportController {
private final UserService userService;
@GetMapping("/export")
public void exportCsv(HttpServletResponse response) throws IOException {
response.setContentType("text/csv; charset=UTF-8");
response.setHeader("Content-Disposition",
"attachment; filename=\"users.csv\"");
// BOM 付き UTF-8 (Excel 文字化け対策)
response.getOutputStream().write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
try (PrintWriter writer = response.getWriter()) {
// ヘッダ行
writer.println("ID,氏名,メール,作成日");
// データ行 (ストリーミングで大量データ対応)
userService.streamAll().forEach(user -> {
writer.println(String.format("%d,%s,%s,%s",
user.getId(),
csvEscape(user.getName()),
csvEscape(user.getEmail()),
user.getCreatedAt()
));
});
}
}
// CSV エスケープ (カンマ・引用符・改行を含む値)
private String csvEscape(String value) {
if (value == null) return "";
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}
}
サーバー側(PHP)
query("SELECT id, name, email, created_at FROM users");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($out, $row);
}
fclose($out);
exit;
サーバー側(Laravel)
public function export(): StreamedResponse
{
$headers = [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="users.csv"',
];
return response()->stream(function () {
echo "\xEF\xBB\xBF"; // BOM
$out = fopen('php://output', 'w');
fputcsv($out, ['ID', '氏名', 'メール']);
User::chunk(1000, function ($users) use ($out) {
foreach ($users as $user) {
fputcsv($out, [$user->id, $user->name, $user->email]);
}
});
fclose($out);
}, 200, $headers);
}
サーバー側(Node.js / Express)
app.get("/api/users/export", async (req, res) => {
res.setHeader("Content-Type", "text/csv; charset=UTF-8");
res.setHeader("Content-Disposition", 'attachment; filename="users.csv"');
res.write("\uFEFF"); // BOM
res.write("ID,氏名,メール\n");
// ストリーミングで送る
const stream = db.users.find().stream();
stream.on("data", user => {
res.write(`${user.id},${csvEscape(user.name)},${user.email}\n`);
});
stream.on("end", () => res.end());
});
function csvEscape(s) {
if (s == null) return "";
if (/[,"\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
return s;
}
Excel 文字化け対策
Excel は BOM 付き UTF-8(バイトオーダーマーク EF BB BF)でないと、CSV を Shift_JIS と誤認して文字化けします:
# BOM の付与方法
# Java
out.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
# PHP
echo "\xEF\xBB\xBF";
# Node.js
res.write("\uFEFF");
# Python
csv_writer.writerow(["..."])
# ファイル open 時に encoding="utf-8-sig" を指定
CSV エスケープのルール
- 値に カンマ
,が含まれる → ダブルクォートで囲む - 値に 改行
\nが含まれる → ダブルクォートで囲む - 値に ダブルクォート
"が含まれる → ダブルクォート 2 個に置換、全体をダブルクォートで囲む
// 例
"Alice, Bob" → "Alice, Bob" # カンマ → 囲む
He said "Hi" → "He said ""Hi""" # " を "" にエスケープ
"Line1
Line2" → "Line1\nLine2" # 改行 → 囲む
大容量データのストリーミング
10 万行以上の CSV を全部メモリに溜めるとサーバが OOM します。ストリーミングで逐次送信:
- JPA:
@Query(... )+Stream+@Transactional(readOnly = true) - JdbcTemplate:
query(... , RowCallbackHandler) - Laravel:
chunk()やcursor() - Node:
cursor()/readable stream