(2021.9.4 記)(2021.10.28 追記)
社内システムをWEB(LAMP構成)で作っているのですが、その中で実装したい機能をコーンディングしている際にハマったところや、そもそも便利だと思った機能についてメモ的に書いて残していくシリーズです。
今回は環境依存文字に対応したCSVダウンロード機能です。
データベースにあるデータをCSVデータとして抜いて、別システムなどで使用すること多いですよね。
CSV出力の方法、環境依存文字の対応の方法はそれぞれ多様なやり方があるのですが、ここではわかりやすくシンプルな方法(またの名を力技)を伝えていきます。
(社内の仕様変更もあり、以前お伝えした方法では実装が不可能になりましたので、新たに別の方法で実装した例を追記しました。おそらくこちらの方が汎用性は高いかなと。)
0.構築環境
・さくらVPN(メモリ:1GB、CPU:2コア、SSD:100GB)
※有料契約
※ハードがどのような環境でも構築手順は大きく変わりません
・LinuxOS:Ubuntu18.04
※最新は20.04ですが、何故かSSL化がうまくいかなかったので、
個人的に実績がある18.04を選定しました
・Apache:2.4
・MySQL:5.7
・PHP:7.2
今回はLinux(Ubuntu18.04)で行うため、LAMP構成の構築が前提条件となります。
LAMP構成の構築は下記過去記事を参照ください。
(Ubuntu20.04でも基本的にインストール方法は変わりません。)
手法1.セッション+PHPの機能でCSV出力
ただ決められたデータをCSV出力するだけなら、こちらの方が実装は楽です。
ⅰ.データベース→CSVデータ作成
データベースからデータを引っ張ってきて、それをCSV形式のデータに加工する手順です。
※データベース操作ロジックの記述側、DB接続は省略
以下のテーブルを例とします
ID | NAME | KANA |
1001 | 田中 | タナカ |
1002 | 鈴木 | スズキ |
1003 | 佐藤 | サトウ |
①ヘッダ部の作成(以下、例)
$csvstr = ''; $csvstr += '"ID", "名前", "フリガナ"'."\n";
見ての通り、無理やりCSVデータを作る感じで、ヘッダの最後に改行コード(\n)を入れています。
ヘッダ名はテーブルのヘッダ名を持ってきてもいいですが、そのままだと使用者がわからないと思うので分かり易い日本語名で出力したいという仕様です。
②データ部の作成(以下、例)
while($row = $prepare->fetch()){ <画面表示ロジック> $csvstr += '"'.$row['ID'].'", "'.$row['NAME'].'", "'.$row['KANA'].'"'."\n"; }
コツとしては、画面表示ロジックと一緒に、欲しいデータを「$row」で検索結果をキャッチさせるということ。
「while」で対象行分回して「$csvstr」のCSVデータにどんどん連結させていく感じです。
データベースから引っ張ってくるだけでなく、この時点で付随させたいデータも混ぜることができます。
(カラム数が多いと大変ですけどね・・・)
配列をCSV化する関数等ありそちらの方がスマートですが、力業は色々小細工ができる点、好きです。
例えばこの時点で変数の中身を見ると次のようなデータになっています。
“ID”, “名前”, “フリガナ”(改行コード)
“1001”, “田中”, “タナカ”(改行コード)
“1002”, “鈴木”, “スズキ”(改行コード)
“1003”, “佐藤”, “サトウ”(改行コード)
③CSVデータを出力するたの小細工
$_SESSION['csv'] = $csvstr;
セッションに作成したCSVデータを格納します。
次項のCSVダウンロード機能の際に、このセッションに格納したデータを引っ張ってくる流れです。
セッションであればクラス(ファイル)が変わっても共通して参照できるデータスペースなので、スマートじゃないですが困ったらここですね・・・。
ⅱ.CSVダウンロード機能(環境依存文字対応)
作成したCSVデータをダウンロード(出力)する機能の作成手順です。
今回はリンク付きボタンを作成し、リンク先にCSVダウンロード機能を記述する流れを取ります。
①ブラウザにダウンロードロジックのリンクを作成(一例)
<button onclick="location.href='./csvdl.php'">CSV出力</button>

同じディレクトリに「csvdl.php」ファイルを作り、その中にダウンロードロジックのphp構文を記述するというやり方です。
CSV出力というボタンをクリックすると、先ほど作ったCSVデータの中身がCSVデータとしてダウンロードされます。
②CSVファイルダウンロード機能
$filename = strval(time()).'csv'; //ファイル名 //出力情報の設定 header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename={$filename}'); header('Content-Transfer-Encoding: binary'); //CSV出力用データをセッションから取得 $csv = $_SESSION['csv']; //環境依存文字対応のCSVファイル出力 echo pack('C*', 0xEF, 0xBB, 0xBF).$csv; return;
よく”UTF-8”コードに変換して出力するやり方を見ますが、CSVをExcelで開くと強制的に”SJIS“コードに変換される仕様で文字化けします・・・
どちらにせよ、コードを気にするのであれば、「pack()」関数を利用してBOM(Byte Order Mark)という符号をデータの先頭に付与すれば、文字コードの悩みは少なくなると思います。
ちなみに、データを開くと、「BOM付きUTF-8」という形式で表示されます。
pack()関数の詳細は、こちらのPHPのリファレンスマニュアルを参考にしてください。
手法2.JavaScriptでスマートに出力
今回は、対象行に☑されたデータをCSV出力する流れを紹介します。
この☑されたデータを云々ロジックは、CSV出力のみならず色々な要望に汎用的に組み合わせられると思います。
ⅰ.データベース→CSVデータ作成
データベースからデータを引っ張ってきて、それをCSV形式のデータに加工する手順です。
※データベース操作ロジックの記述側、DB接続は省略
以下のテーブルを例とします(手法1と同じ)
ID | NAME | KANA |
1001 | 田中 | タナカ |
1002 | 鈴木 | スズキ |
1003 | 佐藤 | サトウ |
①ヘッダ部の作成(以下、例)
$csvstr = '"ID", "名前", "フリガナ"'."\n"; <input id="csv0" type="hidden" value="'.$csvstr.'" />
見ての通り、無理やりCSVデータを作る感じで、ヘッダの最後に改行コード(\n)を入れています。
ヘッダ名はテーブルのヘッダ名を持ってきてもいいですが、そのままだと使用者がわからないと思うので分かり易い日本語名で出力したいという仕様です。
今回はこれを、フォームの隠し要素である<input type=”hidden” />を利用し、「画面には表示されないが裏でデータを持っている」、という状態にするというコツを使います。
②データ部の作成(以下、例)
<?php //検索件数 $res = $prepare->rowCount(); //ボタン(引数に検索件数を引き渡す) $csvstr += '<button onclick="csvdl('.$res.')">CSV出力</button>'; $i = 1; $printstr = ""; //画面に表示させる内容 $printstr += '<table>'; //テーブル //ヘッダ $printstr += '<tr><td>選択</td><td>ID</td><td>名前</td><td>カナ</td></tr>'; //明細はループ文で一緒に加工する while($row = $prepare->fetch()){ //画面表示ロジック //CSVで出したいデータ $csvstr = $row['ID'].', '.$row['NAME'].', '.$row['KANA]."\n"; //明細 //隠し要素(画面には表示されない) $printstr += '<td><input id="csv'$i'" type="hidden" value="'.$csvstr.'" /></td>'; //チェックボックス $printstr += '<td><input id="check'$i'" type="checkbox" /></td>'; //ID $printstr += '<td>'.$row['ID'].'</td>'; //名前 $printstr += '<td>'.$row['NAME'].'</td>'; //カナ $printstr += '<td>'.$row['KANA'].'</td>'; $printstr += '</tr>' $i += 1; } $printstr += '</table>' //画面出力 print($printstr); ?>
欲しいデータを「$row」で検索結果をキャッチ。
「while」で対象行分回す中に、①と同様この画面表示ロジックと合わせて、<input type=”hidden” />のフォーム隠し要素を利用し、対象行一行一行に対して、このinput-hidden を付与してあげます
正確には以下の通りです。
コツとしては、各行のフォームに対して、フォームのIDをループを利用して連番で付与してあげるということですね。
あとは手法1で述べた通りで、データベースから引っ張ってくるだけでなく、この時点で付随させたいデータも混ぜることができます。
(カラム数が多いと大変ですけどね・・・)
配列をCSV化する関数等ありそちらの方がスマートですが、力業は色々小細工ができる点、好きです。
例えばこの時点で画面表示データを見ると、次のようなデータになっています。
各行の見えないところに、input-hidden で指定した値が格納されている寸法です。

選択 | ID | 名前 | かな |
□ | 1001 | 田中 | タナカ |
□ | 1002 | 鈴木 | スズキ |
□ | 1003 | 佐藤 | サトウ |
ⅱ.CSVダウンロード機能(環境依存文字対応)
作成したCSVデータをダウンロード(出力)する機能の作成手順です。
今回はリンク付きボタンを作成し、かつ、各行に付けたチェックボックスに☑されたデータに対してCSVダウンロードする、という機能を記述します。
①ブラウザにダウンロードロジックのリンクを作成(一例)
<button onclick="csvdl(res);">CSV出力</button>

同じディレクトリに「<javascript>」の関数(ここでは、csvdl()という関数)を用意し、クリック時にその関数で全て処理させるという流れです。
CSV出力というボタンをクリックすると、先ほど作ったCSVデータのうち、チェックボックスに☑された対象データのみCSVデータとしてダウンロードされます。
ここでは「res」という引数を渡していますが、詳細は以下の通りです。
②CSVファイルダウンロード機能
<script> //CSV出力 function csvdl(res){ // コピー対象をJavaScript上で変数として定義する var copyStr = document.getElementById('csv0').value; var rowCount = res; var i = 1; //1行ずつ見ていき、チェックが入っていた行のCSVデータを連結させていく for (i; i < rowCount; i++){ if(document.getElementById('check'+i).checked){ copyStr = copyStr + document.getElementById('csv'+i).value; } } // Dateオブジェクトを作成 var date = new Date() ; // UNIXタイムスタンプを取得する (ミリ秒単位) var a = date.getTime() ; //ダウンロードするCSVファイル名を指定する var filename = "csvdate_" + a + ".csv"; //BOMを付与する(Excelでの文字化け対策) const bom = new Uint8Array([0xef, 0xbb, 0xbf]); //Blobでデータを作成する const blob = new Blob([bom, copyStr], { type: "text/csv" }); //IE10/11用(download属性が機能しないためmsSaveBlobを使用) if (window.navigator.msSaveBlob) { window.navigator.msSaveBlob(blob, filename); //その他ブラウザ } else { //BlobからオブジェクトURLを作成する const url = (window.URL || window.webkitURL).createObjectURL(blob); //ダウンロード用にリンクを作成する const download = document.createElement("a"); //リンク先に上記で生成したURLを指定する download.href = url; //download属性にファイル名を指定する download.download = filename; //作成したリンクをクリックしてダウンロードを実行する download.click(); //createObjectURLで作成したオブジェクトURLを開放する (window.URL || window.webkitURL).revokeObjectURL(url); } } </script>
このスクリプトを、</body>の手前に書いてあげます。
上記は手順1の「pack()」関数を利用してBOM(Byte Order Mark)という符号をデータの先頭に付与したやり方の、JavaScript版になります。
ちなみに、データを開くと、こちらもちゃんと「BOM付きUTF-8」という形式で表示されます。
この☑が入っていたら○○する、という要望は多々あるので、このロジックのほうが応用が利きます。
3.最後に
結局のところ、要望の実装にあたって数学パズルのようなところがあるので、そのアルゴリズムがバチっと閃いたときは超絶快感ですね!
コツはループと配列添え字の使い方です。
プログラム脳の鍛え方も紹介していますので、こちらも是非。
以上。
(不明点あればコメント頂ければ、可能な限りお答えします。)

コメント