この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:4
ページ更新者:T
更新日時:2026-06-11 07:07:02

タイトル: 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

関連記事