アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < 転職してフロントエンド勉強中 あとFE聖戦1周クリアしてトラキアやりながらSQX待ち

ローカルのCSVファイルをアップロードして処理

ローカルからCSVファイルをアップロードしてその内容を処理する機能を作ることがあったのでメモ。
といってもアップロード機能自体はめちゃくちゃ簡単だった。エラーチェック系が沼。

HTML

<!-- 【※】はアップロードしたファイルを処理する部分のパス -->
<!--   例:"/csv.php" -->
<form enctype="multipart/form-data" action="【※】" method="POST">
  <input name="userfile" type="file" />
  <input type="submit" value="送信" />
</form>

これだけでファイル指定するボタンと、それを送信するボタンができます。
あとは【※】で指定したところで受け取って処理するだけ。

PHP

<?php
// エラー処理
switch ($_FILES['userfile']['error'])
{
	case UPLOAD_ERR_OK: // 正常(アップロード成功)
		// 正常
		break;
	case UPLOAD_ERR_INI_SIZE:  // サイズオーバー(PHP側の設定値)
	case UPLOAD_ERR_FORM_SIZE: // サイズオーバー(HTMLフォーム側の設定値)
		echo "サイズオーバーです。";
		exit();
		break;
	case UPLOAD_ERR_PARTIAL: // 一部のみアップロード
	case UPLOAD_ERR_NO_FILE: // アップロードされなかった
		echo "アップロード失敗です。";
		exit();
		break;
	case UPLOAD_ERR_NO_TMP_DIR: // アップロード先である一時フォルダが存在しない
	case UPLOAD_ERR_CANT_WRITE: // ディスクへの書き込みに失敗した
	case UPLOAD_ERR_EXTENSION:  // PHPの拡張モジュールがファイルのアップロードを中止した
	default:
		echo "不具合レベルのエラーです。";
		exit();
		break;
}

// ファイルオープン
$fp = fopen($_FILES['userfile']['tmp_name'], "r");

// ファイルの中身を配列に移す
$csv_list = array();
while (($csv_list[] = fgetcsv($fp)) !== false){}
fclose($fp);

//// この時点で、「$csv_list」はCSVの中身が丸ごと配列になったものなので、
//// あとはそれを好きに処理するだけ

// 終了:とりあえず取得した他の情報も表示してみる
echo $_FILES['userfile']['name'] . "の処理が完了しました。<br>";
echo "ファイルタイプ:" . $_FILES['userfile']['type'] . "<br>";
echo "サイズ:" . $_FILES['userfile']['size'] . "バイト<br>";
echo "レコード数:" . count($csv_list) . "<br>";

以上。

とりあえず冒頭にエラーコードによる分岐だけは入れてますが、
その他の例外処理はほとんど入れてないです。必要に応じて増やす必要あり。
参考:
ファイルアップロードの例外処理はこれぐらいしないと気が済まない


また、グローバル変数$_FILESの中身はこちらに記載があります。
参考:
PHP: POST メソッドによるアップロード - Manual


このコードでは、中身はさっさと配列に落とし込んでおり
処理部分はもはやファイルアップロード関係ないのでがっつり省略。
1行ずつ処理したいだけであれば別にwhileの中でやってもOK。
先に全体の件数を知りたいとか、前後のレコードの情報が必要になる場合があるとか、
データによって処理順を変えたいとか…
そういう場合はもちろんこのように先に配列化した方が明らかにやりやすい。

ちなみに処理が終了すると、アップロードされた一時ファイルはさっさと消えてくれます。
中身を処理してポイではなくサーバーに保存したい場合はコピーするなどの必要あり。

おまけ その1:アップロードされたファイルを保存

アップロードされたファイルが処理終了後に消えると困る場合。
一時ファイルを任意のアップロード用ディレクトリにコピーするだけ。

copy($_FILES['userfile']['tmp_name'], "コピー先パス"); // コピー先パスはファイル名も含む

同名のファイルが既に存在する場合は上書きされるので、衝突しないよう注意。

参考:
PHP: copy - Manual

おまけ その2:$_FILESについての注意点

既に上げた参考サイトにも記載あるので蛇足ですが…

  • $_FILES['userfile']['name'] <ローカルでのファイル名>
    • このファイル名を使用したい場合、内容のチェックが必須
      • 「(半角英数)」「.(先頭と末尾以外で)」「_」「-」以外は不可、くらいにしてよいと思う
  • $_FILES['userfile']['type'] <ファイルタイプ>
    • ブラウザ提供の情報なので、信用してはいけない
  • $_FILES['userfile']['size'] <ファイルサイズ>
    • 単位はバイト
  • $_FILES['userfile']['tmp_name'] <アップロードされた先のフルパス>
  • $_FILES['userfile']['error'] <エラーコード>
    • これもPHP側で生成してくれているのでそのまま使ってOK
      • ロジックによってはissetとかis_intとかするとよい場合も
        • 当たり前ですがアップロード処理を通らずに参照すると未定義なので

またエラーコードは以下の8種。なんで5は欠番なんだろう。

UPLOAD_ERR_OK (0)         正常(アップロード成功)
UPLOAD_ERR_INI_SIZE (1)   サイズオーバー(PHP側の設定値)
UPLOAD_ERR_FORM_SIZE (2)  サイズオーバー(HTMLフォーム側の設定値)
UPLOAD_ERR_PARTIAL (3)    一部のみアップロード
UPLOAD_ERR_NO_FILE (4)    アップロードされなかった
UPLOAD_ERR_NO_TMP_DIR (6) アップロード先である一時フォルダが存在しない
UPLOAD_ERR_CANT_WRITE (7) ディスクへの書き込みに失敗した
UPLOAD_ERR_EXTENSION (8)  PHPの拡張モジュールがファイルのアップロードを中止した

参考:
PHP: エラーメッセージの説明 - Manual

おまけ その3:文字コード

CSV(等のテキスト系ファイル)のアップロード時にありがちな問題は文字コードの違い。
中でもShift-JISになっているパターンが多く、全角文字が化けてしまいます。

とりあえず対応してみたコードがこちら。

// -----------------------------------------------------------------
//// 先述のサンプルコードのうち、以下の行について
//// 必要に応じてsjisからutf8に変換して読み込むよう書き換えてみる
// $fp = fopen($_FILES['userfile']['tmp_name'], "r");
// -----------------------------------------------------------------

$file_data = file_get_contents($_FILES['userfile']['tmp_name']);
$encoding  = mb_detect_encoding($file_data, "UTF-8, SJIS-win");
if ($encoding == "SJIS-win")
{
	// sjisからutf8に変換
	$file_data = mb_convert_encoding($file_data, 'UTF-8', 'SJIS-win');

	// 変換したデータを一時ファイルに書き込み、ポインタを先頭へ
	$fp = tmpfile();
	fwrite($fp, $file_data);
	rewind($fp);
}
elseif ($encoding == "UTF-8")
{
	$fp = fopen($_FILES['userfile']['tmp_name'], "r");
}
else
{
	echo "せめてUTF-8かSJISのどっちかでお願いします。";
	exit();
}

ポイントは以下。

まあ、CSVのアップロードは画像等と違って
リテラシーの低い不特定多数が使用するシステムに搭載することはそう多くないでしょうから、
魔法の言葉「運用でカバー」で済む場合も多々あるかとは思います。
要するにクライアント側でUTF-8に変換してからアップロードしてくれと。

参考:
[PHP]文字化けせずにCSVファイルを読み込み、配列に変換する | PHP Archive
[PHP] SJISのCSVファイルの文字化けせずに読み込む方法(SplFileObject) | マリンロード