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のファイルを作成してダウンロードさせる問題がかなり大変そうだったので、
Javascriptで新しいform elementを作成してinputタグの
valueに元のフォームデータを
JSON化したものを入れて、新しく作ったフォームを
form.submit()
させるパワープレイで解決しました。
解決方法までジャンプ👇
フォームのinput数が多いのが問題なので、JavaScripeでデータをまとめて1つの文字列データにして送ってしまえばmax_input_vars
にかかることはありません。
管理ツールにはjQueryが使われていたので、そのままjQueryを使いました。
フォームデータをJSON形式にまとめる
jQueryにはフォームデータをまとめる.serialize()
メソッドがありますが、$form.serialize()
でシリアライズすると
const $form = $('#js-form');
const params = $form.serialize();
console.log( params );
URLエンコードされ&
区切りなので、戻すのが少し面倒そうです。
jQueryには.serializeArray()
メソッドがあり、こちらはserialize()
でエンコード・&
区切りにされる前の配列の状態を取ることが出来ます。
const $form = $('#js-form');
const params = $form.serializeArray();
console.log( params );
{name: inputタグのname: value: inputタグのvalue}
というオブジェクトの配列になります。これをJSON.stringify()
するとJSON形式のデータに変換することができます。
JS側でdata[][]
の連想配列に戻してからJSON化しても良いのですが、文字列の"data[n][n]"から連想配列に戻すのはパワーがかかりそうだったので、データの処理はAPI (PHP)側の任せることにしました。
フォームのデータを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 {
return true;
}
});
<?php
if( isset($_POST)
&& isset($_POST['form_data'])
&& !empty($_POST['form_data']) ) {
$data = [];
$is_excel = false;
$postData = json_decode( $_POST['form_data'], true );
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 ) {
navigator.msSaveBlob(file, fileName);
} else {
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対応ってのが、単に使ってる人がエクセルだとUTF-8が文字化けする(文字コードの概念を知らない)からって理由だけなのでShift_JISめ!! というかExcelめ!! という(# ゚Д゚) MSへのヘイトが高まる...
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を元に戻すことに…
API (PHP)
<?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側で文字コードまで弄って頑張ってファイル生成をするのは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 = [];
$is_excel = false;
$postData = json_decode( $_POST['form_data'], true );
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 {
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が古いだけとかって可能性もありそうですが...
[参考]
Ajaxでファイルダウンロード
文字コード関係
Spreadsheetはイイゾ...