18.

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

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストール(eclipseプラグイン)
  2. クイックスタート
  3. プロジェクトの作成
  4. Spring Bootプロジェクトの作成
  5. Spring Bootプロジェクトの実行
  6. Spring BootでHello World!
  7. アノテーション一覧
  8. DB接続設定からエンティティおよびリポジトリの作成、値の取得まで(JPA編)
  9. DB接続設定や値の取得(JdbcTemplate編)
  10. ビューから値をモデルに格納しコントローラーで受け取る方法
  11. コントローラーにてモデルに値を格納してビューに渡す方法
  12. テンプレートエンジン
  13. ModelとModelAndViewの違い
  14. AOPの使用方法
  15. classpath: 内部ファイルの読み込み
  16. file: 外部ファイルの読み込み
  17. CSVファイルアップロード方法(Ajax)
  18. CSVファイルダウンロード方法(Ajax)
  19. Spring Bootプロジェクトのビルドと本番環境へのデプロイ方法(内部tomcat使用)
  20. Application.propertiesの環境依存設定の分割方法
  21. JPAにおけるEntityManagerの取得方法
  22. JPAにおけるjava.sql.Connectionの取得方法
  23. エラー一覧
  24. jarの引数を受け取る方法
  25. Spring BootでGmailからメール送信
  26. 複数のDBに接続する設定(Spring Boot & JPA編)
  27. ポート番号の変更
  28. Basic認証の実装と特定のURLに限定する方法
  29. Spring SecurityのBasic認証の無効化
  30. 独自のエラーページを定義する方法
  31. プロパティファイルの値やjar実行時の引数を取得する方法