はじめに
個人でシステム開発の仕事をしています。
管理画面を開発しているときに、Windowsのエクセルでも文字化けしないようなCSVインポート機能を実装しようとしたんですが、
SJIS変換してもBOMをつけてもなかなか解決できなかったので、その解決方法を備忘録として残しておこうと思います。
開発環境
- php 8.2
- laravel 10
- docker
やりたいこと
header_1 | header_2 | header_2 |
---|---|---|
body_a1 | body_a2 | body_a3 |
body_b1 | body_b2 | body_b3 |
上のようなCSVをエクスポートし、macでもwindowsでも文字化けせずにCSVを表示したい。
- windowsではexcelで開くことを想定しています。
- headerはソース内で定義し、bodyはMySQLから持ってきた値です。
試したこと
SJIS-winに変換(うまくいかなかった)
まずは、単純にUTF-8をSJISに変換しました。
mb_convert_encoding($text, 'SJIS-win', 'UTF-8');
しかし、エクスポートしたCSVを開くと、カタカナの濁音が ?
となっていまいました。
具体的には「はばぱハバパ」のように濁音と半濁音のひらがなカタカナの文字列があったときに
「はばぱハハ?パ」と表記されてしました。
BOMの追加(うまくいかなかった)
BOM付きのUTF-8にすることで、ExcelがUTF-8で開いてくれると思い、
fopenした直後くらいに以下の一文を追加しましたがうまく行かず。
fwrite($handle, pack('C*', 0xEF, 0xBB, 0xBF));
0xEF, 0xBB, 0xBF はUTF-8のBOM表す3バイトのシーケンスで、CSVがUTF-8エンコーディングであることを示しています。
正規化 + SJIS-winに変換(うまくいった)
Unicodeで正規化して、正規化されたものをSJISに変換してうまくいきました。
use Normalizer;
Normalizer::normalize($field, Normalizer::FORM_C);
return mb_convert_encoding($normalizedField, 'SJIS-win', 'auto');
Normalizer::FORM_C で異なる方法の文字エンコードを統一した方法に正準合成します。
https://www.php.net/manual/ja/class.normalizer.php
データ取得先の MySQL が utf8mb4
で utf-8
の拡張表現なのが影響していました。
そのため、正規化してSJIS変換でうまくいきました。
以下にサンプルコードを記述します。
サンプルコード
- 前提
- TestDataというモデルからbodyのデータを取得することを想定しています。
public function exportCsv(Request $request)
{
// データを取得
$query = TestData::query();
// CSVヘッダー
$headers = ['ID', '名前', 'テストデータ1', 'テストデータ2', '作成日時', '更新日時'];
$headers = array_map(function ($header) {
return mb_convert_encoding($header, 'SJIS-win', 'UTF-8');
}, $headers);
$callback = function() use ($query, $headers) {
$handle = fopen('php://output', 'w');
fputcsv($handle, $headers);
// データを一行ずつ書き込む
foreach ($query->cursor() as $row) {
$csvData = [
$row->id,
$row->user_name,
$row->test_data_1,
$row->test_data_2,
$row->created_at,
$row->updated_at,
];
// 各行のデータをShift-JISに変換して出力
$csvData = array_map(function ($field) {
$normalizedField = Normalizer::normalize($field, Normalizer::FORM_C);
return mb_convert_encoding($normalizedField, 'SJIS-win', 'auto');
}, $csvData);
fputcsv($handle, $csvData);
}
fclose($handle);
};
$fileName = 'test_data_' . date('Ymd') . '.csv';
return new StreamedResponse($callback, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => "attachment; filename=\"$fileName\"",
]);
}
最後に
同じWindowsでも使っているExcelのバージョンなどによっても文字化けする/しないがあるので、文字コードの管理は難しいですね。