ソースコードで学ぶWebプログラミング

csv変換ツールの作り方

作業を効率化するためのプログラムをツールとして作成してみます。

今回のcsv変換とは、別のフォーマットのcsvへ変換することとします。 変換の用途としては、あるストアの商品csvデータを、別ストアの商品csvデータにしたり、 集計用のcsvデータを作成したりなどが考えられます。

学べること

・ファイルのアップロード ・ファイルのダウンロード ・csvフォーマットと処理方法

csv変換ツールの要件定義

プログラムを作成するために、まず要件定義を行います。

今回は「csv変換ツール」に必須の機能をピックアップします。

1.データ「csvファイル」を受け取る 2.データ「csvファイル」を変換する 3.データ「csvファイル」を出力する

機能としては網羅できているようです。さらに詳細を決めていきます。

・データ「csvファイル」は、手元のファイルをアップロードする ・データ「csvファイル」の文字コードは、Shift_JISとする ・データ「csvファイル」の一行目は、カラム名とする ・データ「csvファイル」のカラム数は、任意とする ・データ「csvファイル」の変換後のカラムは、元csvファイルのカラムから選択する ・データ「csvファイル」のカラム選択は、ブラウザで行う ・データ「csvファイル」の変換後のカラムは、新規カラムを追加できる ・データ「csvファイル」を選択情報を元に新しい構成にする変換を行う ・データ「csvファイル」の変換後、ダウンロードできる

ここで「変換後のカラム情報」を「選択する機能」と「選択した情報」の受け取りが必要なことが分かります。 また「変換」「出力」についても「選択する機能」用と「選択した情報」の受け取り後用に分かれそうです。

ここまでの要件をまとめてみます。機能に対して、関連する詳細を当てはめていきます。

1.データ「csvファイル」を受け取る ・データ「csvファイル」は、手元のファイルをアップロードする 2.データ「csvファイル」を変換する(選択用) ・データ「csvファイル」は、文字コードShift_JISとする ・データ「csvファイル」の一行目は、カラム名とする ・データ「csvファイル」のカラム数は、任意とする 3.データ「csvファイル」を出力する(選択用) ・データ「csvファイル」の変換後のカラムは、元csvファイルのカラムから選択する ・データ「csvファイル」のカラム選択は、ブラウザで行う ・データ「csvファイル」の変換後のカラムは、新規カラムを追加できる 4.データ「選択情報」を受け取る ・データ「csvファイル」のカラム数は、任意とする 5.データ「csvファイル」を変換する(選択後用) ・データ「csvファイル」を選択情報を元に新しい構成にする変換を行う 6.データ「csvファイル」を出力する(選択後用) ・データ「csvファイル」の変換後、ダウンロードできる

csv変換ツールの要件をコード化する

それではコーディングに移ります。

1.データ「csvファイル」を受け取る

ブラウザからファイルをアップロードするには <form method="post" enctype="multipart/form-data">タグを利用します。 PHPでは変数 $_FILES にアップロードファイルの情報が入力される事になります。

ファイルのアップロードは、自由に行われるとセキュリティ上問題があります。 例えば、プログラムをアップロードして実行されるなどの危険があるため、 必ずファイルチェックを行うようにします。

またサーバへアップロードした後の保存ファイル名もプログラム側で指定するようにします。 今回は、define() で定数として固定しています。

<?php /* csvファイル名 */ define('CSV_FILE','upload.csv'); /* エラー内容 */ $error = ''; /* ファイルがアップロードされた場合 */ if (isset($_FILES['upload']) && is_int($_FILES['upload']['error'])) { /* コーディングの省略のため変数に入力 */ $up = $_FILES['upload']; /* エラーチェック */ switch ($up['error']) { case UPLOAD_ERR_OK: break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $error = 'ファイルサイズが制限を超えています'; break; case UPLOAD_ERR_NO_FILE: $error = 'ファイルを選択して下さい'; break; default: $error = 'アップロードに失敗しました'; break; } if ($error=='') { /* アップロードファイルのチェック */ if (is_uploaded_file($up['tmp_name'])) { /* ファイルタイプをチェック */ $mine = mime_content_type($up['tmp_name']); if ($mine!='text/plain') { $error = 'csvファイルを選択して下さい'; } if ($error=='') { move_uploaded_file($up['tmp_name'],CSV_FILE); /* リロード対策 */ header('Location: '.$_SERVER['SCRIPT_NAME']); exit; } } else { $error = 'アップロードファイルが不正です'; } } } ?> <?php echo $error; ?> <form method="post" enctype="multipart/form-data"> <input type="file" name="upload"> <input type="submit" value="アップロード"> </form>

やっている事

・isset($_FILES[]) && is_int($_FILES[]['error']) でファイルが一つアップロードされたかチェック ・$_FILES[]['error'] でアップロード時のエラーを確認 ・switch(){} で各エラー毎にエラーメッセージを設定 ・is_uploaded_file($_FILES[]['tmp_name']) でアップロードされたファイルかチェック ・mime_content_type() でファイル情報を取得 ・text/plain でcsvファイルを判定 ・move_uploaded_file() でアップロード一時ファイルを移動 ・header('Location: '.$_SERVER['SCRIPT_NAME']); で同ページにリダイレクト ・inputタグのfileのname要素 upload にて $_FILES[] のキーを指定

処理内容としては、ファイルがアップロードされたかを判定、エラーを確認、ファイルタイプを確認、 アップロードされた一時ファイルを確認、一時ファイルを移動および名前付け保存となります。

アップロードが行われた場合、$_FILES['upload'] は連想配列となり、ファイル情報などが入力されています。

/* $_FILES['upload'] の状態 */ array( 'name'=>'アップロード元のファイル名', 'type'=>'ファイルのMIME型', 'size'=>'ファイルのバイトサイズ', 'tmp_name'=>'サーバ上で保存されているファイル名', 'error'=>'アップロード処理時のエラーコード' );

ファイルは一時的に別名で保存されているので、プログラム側で名前を付けて保存します。 この時、アップロード元ファイル名やファイルのMIME型は任意の文字列が入っているため、 基本的に利用しないか、注意して扱う必要があります。

今回は一つのファイルをアップロードしていますが、複数ファイルの同時アップロードも出来ます。

/* nameを変えるやり方 */ <input type="file" name="upload1"> <input type="file" name="upload2"> /* 配列で受け取るやり方 */ <input type="file" name="upload[]"> <input type="file" name="upload[]"> 配列で受け取る場合の注意点として $_FILES['upload'] の連想配列の状態が変わり、 それぞれに配列としてファイル情報が入力されています。

/* 配列で複数アップロードした場合の $_FILES['upload'] の状態 */ array( 'name'=>array( 'アップロード元のファイル名', 'アップロード元のファイル名' ), 'type'=>array( 'ファイルのMIME型', 'ファイルのMIME型' ), 'size'=>array( 'ファイルのバイトサイズ', 'ファイルのバイトサイズ' ), 'tmp_name'=>array( 'サーバ上で保存されているファイル名', 'サーバ上で保存されているファイル名' ), 'error'=>array( 'アップロード処理時のエラーコード', 'アップロード処理時のエラーコード' ) );

配列の場合は foreach(){} でそれぞれのファイル毎にエラーチェックなどの処理を行う必要があります。

続いて変換処理に移ります。

2.データ「csvファイル」を変換する(選択用)

ここでもう一度、変換の処理内容を確認すると、元csvファイルのカラムを指定して、 新しいカラム構成のcsvファイルを作成することです。

具体的な処理内容としては、下記が考えられます。

・csvファイルをPHPで処理するために、Shift_JISからUTF-8に文字コード変換する ・csvファイルから各カラム名を読み込む ・変換元のカラム名を選択できるようにする ・変換後のカラム構成を指定できるようにする

読み込みはPHPプログラムで実装できます。
カラム名の選択は、HTMLタグのselectで実装できそうです。
カラム構成の指定は、javascriptで実装する必要がありそうです。

/* 元ファイルのカラム名リスト */ $COLS = array(); /* アップロードファイルが存在する時のみ処理 */ if (file_exists(CSV_FILE)) { /* csvファイルを読み込んで、UTF-8に変換 */ $data = file_get_contents(CSV_FILE); $data = mb_convert_encoding($data, 'UTF-8', 'SJIS-win'); /* 一時ファイルに書き込んで、読込処理 */ $tmp = tmpfile(); fwrite($tmp, $data); fseek($tmp, 0); $COLS = fgetcsv($tmp); fclose($tmp); }

やっている事

・file_exists() でcsvファイルが存在する時に処理を実行 ・mb_convert_encoding() で文字列のエンコーディングを変換 ・tmpfile() でデータを読込ための一時ファイルを作成 ・fwrite() で一時ファイルに変換済データを書込 ・fseek() でファイルポインタを移動、0で先頭 ・fgetcsv() で一行分のcsvデータを読込、配列を返す

今回のプログラムでは、csvファイルは文字コードShift_JISを想定しているので、 PHPで処理できるように「SJIS-win」から「UTF-8」に変換しています。

他に押さえるべき要点として fgetcsv() でcsvの一行目のみ読み込んで、 カラム名のリスト(配列)としている点があります。これは利用者がブラウザから カラム名を選択できるようにするためです。 具体的には HTMLタグ<select></select>の選択肢として出力するために配列化しています。

次に出力処理を実装します。

3.データ「csvファイル」を出力する(選択用)

カラム名は文字列ですが、任意の文字列のため、HTML出力するためにエスケープするようにします。

/* エスケープ関数 */ function h($v){ return htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); } function eh($v){ echo h($v); }

HTML出力部分は、下記のような構成になります。

<table> <tr id="th"> <th>1カラム目</th> </tr> <tr id="td"> <td class="col cur"> <select class="base" name="c[]"> <option value="">空欄</option> <?php foreach($COLS as $i=>$c): ?> <?php if($c=='') continue; ?> <option value="<?php echo $i; ?>"><?php eh($c); ?></option> <?php endforeach; ?> </select> </td> </tr> </table> <input id="add" type="button" value="カラム追加"> <input id="join" type="button" value="カラム結合"> <input id="del" type="button" value="カラム削除">

やっている事

・カラム名リスト $COLS から カラム名を optionタグとして出力 ・カラム名リスト内の位置 $i をキーとして選択できるように ・カラム名を eh() でエスケープして出力 ・変換後のカラム名は複数選択できるように select の name を c[] に ・<option value="">空欄</option> で元csvにないカラムを作成できるように

javascriptでの処理を前提としているため、タグ構成やID、クラス名の割り当てにすべて意味があります。 今回は、PHPの学習用記事のため詳細は省きますが、要点のみピックアップします。

javascript用にやっている事

・<tr id="th"> でカラム番号の追加位置を指定できるように ・<th>1カラム目</th> のみ初期表示して、2カラム目以降は自由に追加できるように ・<tr id="td"> でカラム内容の追加位置を指定できるように ・<td class="col cur"> で操作対象カラムを指定できるように ・class="base" で元カラム選択肢を取得できるように ・「カラム追加」「カラム削除」ボタンで、変換後のカラム構成を編集できるように ・「カラム結合」ボタンで、変換後のカラム内に、複数の元カラムを入れられるように

4.データ「選択情報」を受け取る

アップロード後の画面

これまでのコーディングで、変換後のcsv構成をブラウザから受け取れるようになっています。 $_POST['c'] は配列となり、新しいcsv構成でのカラム順に並んでいます。

/* $_POST['c'] 内に配列で存在 */ array( array('元csvのカラム名のリスト内番号'), array('元csvのカラム名のリスト内番号'), ・・・ );

5.データ「csvファイル」を変換する(選択後用)

csv構成を操作している画面

カラム内容の指定としてselectの値が空欄の場合、新規カラム(未入力)とします。 また元カラムを結合して新規カラムを作成する処理も行います。

/* 新しい構成のcsvを作成 */ $fp = fopen(CSV_FILE,'r'); while($cs = fgetcsv($fp)){ /* 元カラム指定を取得 */ foreach($_POST['c'] as $i=>$cno){ /* 元カラムを利用するか、新規カラム(未入力)か */ $c = $cno=='' ? '': $cs[ $cno ]; /* カラム結合 */ if (isset($_POST['a'.$i]) && is_array($_POST['a'.$i])) { foreach($_POST['a'.$i] as $a){ $c .= $a=='' ? '': $cs[ $a ]; } } $csv .= '"'.str_replace('"','""',$c).'",'; } $csv .= "rn"; } fclose($fp);

やっている事

・foreach(){} で $_POST内の新カラム情報を処理 ・$i は新csvのカラム番号、$cno は元csvのカラム名のリスト内番号 ・条件文 ? A : B; で新規カラム か 元カラムを利用するかを判定 ・$_POST['a'.$i] でカラム結合情報を受取(javascript側の実装) ・str_replace() で「"」をエスケープ

6.データ「csvファイル」を出力する(選択後用)

最後はダウンロード処理です。変換済のcsvデータをブラウザでダウンロードが行われるように出力する処理となります。

/* csvダウンロード */ header('Content-Disposition: attachment; filename="new'.date('YmdHi').'.csv"'); header('Content-Type: application/octet-stream'); header('Content-Length: '.strlen($csv)); echo $csv; exit;

やっている事

・header() でHTTPヘッダ情報を出力 ・Content-Disposition: attachment; filename="" でダウンロードファイル名を指定 ・date() でファイル名に日時を利用 ・Content-Type: application/octet-stream でダウンロード処理を指示 ・Content-Length: でダウンロードのファイルサイズを指定 ・strlen() でcsvファイルのサイズを取得

以上で完成となります。

3.プログラム完成後のカスタマイズ

ソースファイルはこちらより入手(購入)可能です。

今回は学習用として作成しているため、必要最小限の機能となっていますが、 下記のようなカスタマイズで、高機能化を目指せます。

カスタマイズ例

・アップロードの保存ファイル名を利用者毎に変えて、複数人が同時利用可能にする ・変換後のcsv構成を保存、読込できるようにして、すぐに呼び出せるようにする ・複数のcsvファルをアップロードして、結合できるようにする ・javascriptでカラムをドロップ移動できるようにする ・変換後のカラム名を入力できるようにする ・変換後のカラム値を指定の書式で自動入力されるようにする