PHP製の管理ツールで、CSVを生成してDLするシステムを作っていました。
データ量が多いとエラーになるということで調べたら、PHPのmax_input_vars
にかかって居るのが原因でした。
そして、使っているサーバーがhetemlでmax_input_vars
の上限を変えることが出来なかったので管理ツールということもありパワープレイで対策してみた記録。
要件
form
<form id="js-form"> <div> <input type="text" name="data[0][0]" value="データA-1"> <input type="text" name="data[0][1]" value="データB-1"> <input type="text" name="data[0][2]" value="データC-1"> <input type="text" name="data[0][3]" value="データD-1"> </div> <div> <input type="text" name="data[1][0]" value="データA-2"> <input type="text" name="data[1][1]" value="データB-2"> <input type="text" name="data[1][2]" value="データC-2"> <input type="text" name="data[1][3]" value="データD-2"> </div> <div> <input type="text" name="data[2][0]" value="データA-3"> <input type="text" name="data[2][1]" value="データB-3"> <input type="text" name="data[3][2]" value="データC-3"> <input type="text" name="data[3][3]" value="データD-3"> </div> <label><input type="checkbox" name="is_sjis" checked="checked" value="1"> Shift JIS</label><br> <input type="submit" value="CSV Download"> </form>
- フォームデータは
name="data[0][0]"
のようなCSVの行列の連想配列形式 - フォームをデータからCSVを作ってDLできる
- CSVのエンコードをShift-JIS / UTF-8 選択できる (エクセルで文字化けすると言われたので
SJIS
対応)
結論
以下長いので、先に結論からか言えば、max_input_vars
を回避してフォームが送れれば済む話なのと、JavaScriptでShift_JISのファイルを作成してダウンロードさせる問題がかなり大変そうだったので、
form.submit()
させるパワープレイで解決しました。
方針 JavaScriptでフォームデータを1つの文字列にしてAjaxで送ってCSVをDL
フォームのinput数が多いのが問題なので、JavaScripeでデータをまとめて1つの文字列データにして送ってしまえばmax_input_vars
にかかることはありません。
管理ツールにはjQueryが使われていたので、そのままjQueryを使いました。
フォームデータをJSON形式にまとめる
jQueryにはフォームデータをまとめる.serialize()
メソッドがありますが、$form.serialize()
でシリアライズすると
const $form = $('#js-form'); const params = $form.serialize(); console.log( params ); // data%5B0%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-1&data%5B0%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-1&data%5B0%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-1&data%5B0%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-1&data%5B1%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-2&data%5B1%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-2&data%5B1%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-2&data%5B1%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-2&data%5B2%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-3&data%5B2%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-3&data%5B3%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-3&data%5B3%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-3&is_sjis=1
URLエンコードされ&
区切りなので、戻すのが少し面倒そうです。
jQueryには.serializeArray()
メソッドがあり、こちらはserialize()
でエンコード・&
区切りにされる前の配列の状態を取ることが出来ます。
const $form = $('#js-form'); const params = $form.serializeArray(); console.log( params ); // [0: {name: "data[0][0]", value: "データA-1"}, ..., 12: {name: "is_sjis", value: "1"}]
{name: inputタグのname: value: inputタグのvalue}
というオブジェクトの配列になります。これをJSON.stringify()
するとJSON形式のデータに変換することができます。
JS側でdata[][]
の連想配列に戻してからJSON化しても良いのですが、文字列の"data[n][n]"から連想配列に戻すのはパワーがかかりそうだったので、データの処理はAPI (PHP)側の任せることにしました。
JSON化したデータをAjaxで送り、API側でCSVを作成して書き出す
フォームのデータをJSON化してAjaxで送る処理
const maxInputVars = 1000; const $form = $("#js-form"); function postJSONData(jsonData) { $.ajax({ url: "api_url", type: "post", crossDomain: false, dataType: "text", data: {"form_data": jsonData} }) .done(function(res) { console.log(res); }) .fail(function(jqXHR, status, err) { console.log(status, err); }); } $form.on('submit', function() { const postNum = $('input').length; if( (postNum + 50) >= maxPostVars ) { const params = JSON.stringify( $form.serializeArray() ); postJSONData( params ); return false; } else { // max_input_vars にかからそうな場合は通常にformを送信 return true; } });
CSVを作成して返すAPI (PHP)
<?php if( isset($_POST) && isset($_POST['form_data']) && !empty($_POST['form_data']) ) { $data = []; // Shift_JIS化するかどうか // チェックボックスはチェックされないとそもそもデータが送られないのでデフォルト値を設定しておく $is_excel = false; // JSONを配列に戻す $postData = json_decode( $_POST['form_data'], true ); // data[n][n] 文字列から連想配列に戻す foreach( $postData as $row ) { $key = $row['name']; $val = $row['value']; if( $key == 'is_sjis' ) { $is_excel = (bool) intval($val); } else { preg_match('/^data\[(\d+)\]\[(\d+)\]$/', $key, $matches); if( count($matches) == 3 ) { $dRow = intval($matches[1]); $dCol = intval($matches[2]); if( !isset( $data[ $dRow ] ) || !is_array( $data[ $dRow ] ) ) { $data[ $dRow ] = []; } $data[ $dRow ][$dCol] = $val; } } } if( count(data) > 0 ) { $fileName = "MY_DATA-" . date("Y-m-d") . ".csv"; create_scvfile_by_array($data, $fileName, $is_excel); } } function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) { $csv = ''; foreach($data as $row) { if( is_array($row) ) { $csv .= join(',', $row); } else { $csv .= $row; } $csv .= "\n"; } $charset = 'UTF-8'; if( $is_excel ) { $csv = mb_convert_encoding($csv, 'SJIS', 'UTF-8'); $charset = 'sjis-win'; } header("Content-Type: application/octet-stream; charset={$charset}"); header("Content-Disposition: attachment; filename={$fileName}"); echo $csv; exit(); } exit();
AjaxでPOSTした場合そのままファイルダウンロードはできないっぽい
上記で試してみた所、CSVファイルの生成自体は上手くいっているのですが、
Content-Disposition: attachment
でCSVファイルをechoしてもダウンロードにはならずJS側に取得されるようです。
Ajaxで返されたデータをファイルにしてダウンロードさせる
Ajaxで返されたデータをファイルにしてDLさせる方法を模索します。
検索しているとだいたい2つのパターンに分かれるようでした。
- サーバーサイドで実ファイルを作成して、ファイルのパスを返しAjaxのdone内で
window.location.href=file_path
としてDLさせる - Ajaxに返されたデータをBlob オブジェクト でファイル化してDLさせる
サーバーサイドに実ファイルを作成するのは、ファイル削除したり管理が面倒そうだったので、Blobでファイル化する方法を採用することにしました。
API側はCSVデータとファイル名をJSONで返すように変更します。
API (PHP)
<?php // 略 function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) { // 略 header("Content-Type: text/javascript; charset={$charset}"); echo json_encode([ "csv" => $csv, "filename" => $fileName, "charset" => $charset, ]); exit(); }
JavaScript側はAjaxでデータが返されたあとの処理を追加します。
JS
function postJSONData(jsonData) { $.ajax({ url: "api_url", type: "post", dataType: "json", data: {"form_data": jsonData} }) .done(function(res) { console.log(res); const fileName = res.filename; const file = new Blob([res.csv], { type: `application/octet-stream; charset=${res.charset}` }); if( window.navigator.msSaveOrOpenBlob ) { // IE navigator.msSaveBlob(file, fileName); } else { // window.location.href = URL.createObjectURL(file); // => DLできるけどファイル名の付け方が面倒そうだった。 // ※ Blobの type: "text/csv;charset=utf-8" にすれば .csv 拡張子がついたファイルがDLされた const dlBtn = document.createElement('a') dlBtn.download = fileName; dlBtn.href = URL.createObjectURL(file); dlBtn.click(); } }) .fail(function(jqXHR, status, err) { console.log(status, err); }); }
HTML5を使えばaタグのdownload属性でファイル名を指定してDLさせることができます。(aタグ生成させる方法なら、IEで切り分ける必要無さそうな気もしますが...)
参考 HTML5 ファイルをダウンロードさせるリンクを作りたい。 - かもメモ
これでAjaxで受け取ったCSVファイルがダウンロードできるようになりました!
と、ここで完結すればよかったのですが...
データの中身が原因なのか不明な部分はありますが、Shift_JISに変換したCSVデータの入ったJSONが、Ajaxに戻ってきた時にパースエラーでエラーに流れてしまいました。(headerでcharsetも指定しているのですが...)
Shift_JIS 問題と戦う。
Shift_JIS対応ってのが、単に使ってる人がエクセルだとUTF-8が文字化けする(文字コードの概念を知らない)からって理由だけなのでShift_JISめ!! というかExcelめ!! という(# ゚Д゚) MSへのヘイトが高まる...
Ajaxで返されるデータの文字コードを指定してみる
AjaxのbeforeSend
内でXMLHttpRequest.overrideMimeType
で文字コードを指定すれば文字化けが解消できるそうです。(ES2015なら直接XMLHttpRequest.overrideMimeTypeで良いんだろうけど)
Shift_JISに変更するチェックが有るときだけMineTypeを上書きするようにします。
JS
function postJSONData(jsonData) { $.ajax({ url: "api_url", type: "post", dataType: "json", beforeSend: function(xhr) { const is_sjis = $('input[name="is_sjis"]').prop('checked'); if( is_sjis ) { xhr.overrideMimeType("text/javascript; charset=shift_jis"); } }, data: {"form_data": jsonData} }) .done(function(res) { /* 略 */}) .fail(function(jqXHR, status, err) { /* 略 */}) }
こちらで試してみたのですが、Shif_JISに変換したCSVを組み込んだJSONだと相変わらずパースエラーになってしまいました。
APIからはCSVファイルだけを返すようにする
ファイル名をAPI側から渡したいという理由だけでJSONにしていたので、CSVのデータだけ返すようにしてしまえばShif_JISでもエラーにならないのでは?と思いAPIを元に戻すことに…
<?php // 略 function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) { // 略 header("Content-Type: text/csv; charset={$charset}"); echo $csv; exit(); }
JavaScript側はJSONからファイル名や文字コードを取得していた部分を修正します
JS
function postJSONData(jsonData) { const const is_sjis = $('input[name="is_sjis"]').prop('checked'); const charset = is_sjis? 'shift_jis' : 'utf-8'; $.ajax({ url: "api_url", type: "post", dataType: "text", beforeSend: function(xhr) { if( is_sjis ) { xhr.overrideMimeType("text/plan; charset=shift_jis"); } }, data: {"form_data": jsonData} }) .done(function(res) { const fileName = "fileName.csv"; const file = new Blob([res], { type: `application/octet-stream; charset=${charset}` }); const dlBtn = document.createElement('a') dlBtn.download = fileName; dlBtn.href = URL.createObjectURL(file); dlBtn.click(); }) .fail(function(jqXHR, status, err) { /* 略 */}) }
こんかいはShift_JISに変換したデータでもAjaxでエラーにならずファイルがダウンロードされました!
... しかし ...
ダウロードされたファイルの中身がShif_JISになってない!! UTF-8なCSVでしたw
原因は定かではない部分があるのですが、HTML・JS側の文字コードがUTF-8なので、Ajaxに渡されたファイルがShif_JISでもUTF-8に解釈され直しているのではないかという推察です。
JavaScriptで文字コードを変換する。
字面にしただけで地雷そうな雰囲気がしているのですが、調べてみると先人がおられました。
とても大変そう…
ということで別の方法を模索することにしました。
解決 一方ロシアは鉛筆を使った作戦
JavaScript側で文字コードまで弄って頑張ってファイル生成をするのはtoo muchなように感じました。
Ajaxで受け取ったデータをファイルにしてDLさせる部分でaタグを生成してa.click()
で動作させられるなら、JavaScriptでで、元のフォームデータをシリアライズ化したvalueを持ったinputタグをもった別のformを作ってform.submit()
してしまえば、Ajaxすら不要で、JavaScript側でAjaxに渡されたデータからファイル作ってDLする機能すら不要になるのではないか?と閃いたのでした。
もともとサーバーの都合でmax_input_vars
にかかってしまうと言うのがメインの問題だったのでJavaScriptで生成したformがそのまま送信できるのであれば、フロント側で頑張らなくても問題は解決する訳です!!
一方ロシアは鉛筆を使ったみたいなソリューション...
API側は当初のCSVを吐き出す状態に戻します
API (PHP)
<?php if( isset($_POST) && isset($_POST['form_data']) && !empty($_POST['form_data']) ) { $data = []; // Shift_JIS化するかどうか // チェックボックスはチェックされないとそもそもデータが送られないのでデフォルト値を設定しておく $is_excel = false; // JSONを配列に戻す $postData = json_decode( $_POST['form_data'], true ); // data[n][n] 文字列から連想配列に戻す foreach( $postData as $row ) { $key = $row['name']; $val = $row['value']; if( $key == 'is_sjis' ) { $is_excel = (bool) intval($val); } else { preg_match('/^data\[(\d+)\]\[(\d+)\]$/', $key, $matches); if( count($matches) == 3 ) { $dRow = intval($matches[1]); $dCol = intval($matches[2]); if( !isset( $data[ $dRow ] ) || !is_array( $data[ $dRow ] ) ) { $data[ $dRow ] = []; } $data[ $dRow ][$dCol] = $val; } } } if( count(data) > 0 ) { $fileName = "MY_DATA-" . date("Y-m-d") . ".csv"; create_scvfile_by_array($data, $fileName, $is_excel); } } function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) { $csv = ''; foreach($data as $row) { if( is_array($row) ) { $csv .= join(',', $row); } else { $csv .= $row; } $csv .= "\n"; } $charset = 'UTF-8'; if( $is_excel ) { $csv = mb_convert_encoding($csv, 'SJIS', 'UTF-8'); $charset = 'sjis-win'; } header("Content-Type: application/octet-stream; charset={$charset}"); header("Content-Disposition: attachment; filename={$fileName}"); echo $csv; exit(); } exit();
JS側はinputの数がmax_input_vars
を超えそうなら新しいフォームを作成して、APIにform.submit()
するように変更します。
const maxInputVars = 1000; const $form = $("#js-form"); function createNewFormAndPost(data) { cont $form = $('<form></form>') .attr('action', "api_url") .attr('method', 'post') .attr('enctype', 'multipart/form-data'); cont $input = $('<input type="text" name="form_data">').val(data); $form.append($input).appendTo("body"); $form.submit().remove(); } $form.on('submit', function() { const postNum = $('input').length; if( (postNum + 50) >= maxPostVars ) { const params = JSON.stringify( $form.serializeArray() ); createNewFormAndPost( params ); return false; } else { // max_input_vars にかからそうな場合は通常にformを送信 return true; } });
これで試した所…
JavaScriptで生成したフォームをそのままJSでform.submit
で送ることができ、Shift_JISのCSVもそのままダウンロードですることができました!!
JSでフォーム生成してsubmit
するのその内セキュリティの問題とかで規制されそうな気がしなくもないですが、とりあえず要件を満たすことが出来たので解決としました。
まとめ
すごい長い大作記事になったけど、ホント中身がありません。
文字コード問題はホント面倒なんで、Shift_JISとかEUC-JPとか滅んでUTFに統一されて欲しいって感想しか持てませんでした。JavaScriptのBlobオブジェクトでファイルが生成できるとか、頑張れば文字コードにも対応できるとか、JSで生成したaタグをクリックさせたり、form.submit()
問題なく出来てしまうとか、色々と学びは有りました。
一つだけグローバル企業なMSがなぜ日本語版(?)のEXCELにShift_JISを採用しているのかはとっても謎いです。(最近のはUTF-8で、このシステム使ってる人のEXCELが古いだけとかって可能性もありそうですが...
[参考]
- max_input_vars対策 - Qiita
- jQuery.ajax() | jQuery API Documentation
- .serialize() | jQuery API Documentation
- .serializeArray() | jQuery API Documentation
- JSON.stringify() | MDN
- application/octet-stream
Ajaxでファイルダウンロード
文字コード関係
- jQueryでShiftJISエンコードされたCSVを処理する - Qiita
- Ajaxの文字化け対策 - knowledge base
- jQuery.ajax で overrideMimeType する方法 - hogehoge @teramako
- HTML と JavaScript が shift_jis の場合の JSON の文字コードについて - Qiita
- javascriptで文字コード変換 - Qiita
- javascript で作成したCSVファイルをエクセルで表示可能にする - Qiita
文系女子と学ぶ!Googleスプレッドシート2018: データ入力・四則演算・関数の基礎・フィルター・グラフ・ピボットテーブルetc...斎藤亜美ちゃんと学ぶはじめてのスプレッドシートの教科書
- 作者: 武田雅人,久原英之
- 出版社/メーカー: Tekuru Inc
- 発売日: 2018/06/17
- メディア: Kindle版
- この商品を含むブログを見る
はじめてのGoogleスプレッドシートの教科書2018: データ入力・四則演算・関数の基礎・フィルター・グラフ・ピボットテーブルetc...大学生・新卒のためのはじめてのスプレッドシートの教科書
- 作者: 武田雅人,阿南大輝,久原英之
- 出版社/メーカー: 株式会社Tekuru
- 発売日: 2018/05/12
- メディア: Kindle版
- この商品を含むブログを見る
Spreadsheetはイイゾ...