タグ別アーカイブ: CSV

Conceptual:OnlyCSViewに、ただ何となくBootstrapを適用

ユーザが指定したCSVファイルを表形式でAjax表示するだけのWEBサービス、OnlyCSView。作成して以来、大して弄ることも無く放置していたのだが、Twitter Bootstrapの雰囲気を掴むために、インターフェースだけ変更しようと思い、手入れをした。

そもよ、Twitter Bootstrapって何?

Twitter BootstrapというのはCSSフレームワークの一つで、かのTwitterが提供する無償フレームワーク。これを使えば、簡単にカラムレイアウトが実現できたり、レスポンシブデザインに対応できたり、Twitterのサービスで使っているようなGUI部品が要素へのクラス指定だけで実現できたり…とそこそこ楽しめる。CSSコーダなど、WEBデザイン・レイアウト方面の方々に知名度はある技術だと思うけど、生粋のWEBプログラマでは、Bootstrapの存在を知らないという場合も多い。

どういう部品が使えるのか、どういう機能が付けられるかについては、こちらのデモページが大変参考になるだろう。
ちなみに、OnlyCSViewでは「表示」ボタンと、テーブルの視認性を上げる奇数行ハイライトで明示的に使っている。それぞれ、class=”btn btn-primary”という指定と、class=”table table-striped”という指定をしているだけ。なお、文字コード選択のセレクトメニューの見た目も変化しているが、こちらはclass指定をせず勝手に変更されたもの。こういったおせっかいもあるので、それが気にならない人間がBootstrapに手を出せばよい。

BootstrapのCDNはよ

こういった仕事で使うにはちょっと…というおもちゃは、すぐに影響なく外せるよう、CDNで利用したい。Bootstrapにも勿論CDNがあるので、魔法の呪文を書いておこう。

<!--Bootstrapの本体(ヴァージョンは適宜)-->
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>

jQueryが必須になる。ちなみに、レスポンシブ対応させるには、もう一つ二つファイルを読み込まないといけない。そちらはCDNが無いので、ダウンロードしてくるしかないけど。
ちなみに、このCDNを提供しているNetDNAという会社は、本来はCDNを商売にしている会社なので、急にサービスの停止などされる可能性はある。自己責任で。

OnlyCSViewの機能増えてないけど?

本当は見た目の更新だけでなく、ファイルのドラッグ&ドロップに対応させようとして、仕組みは書いていたのだけど。ただ、一般的なファイルダイアログを使ったアップロードと、ドラッグ&ドロップによるアップロードで同じGUIを使うというところに無理があったため、どうしようかと思っている。賑やかしで置いているだけで、利用者もいないだろうし別の開発を優先する予定。


Conceptual:Ajax対応CSV表示プログラムOnlyCSViewを計画する

以前、PHPの便利なfgetcsv関数を使ったCSV表示プログラムを書きました。このプログラムはソースコードを読んで判るように、フォームで受け取ったパスのファイルを一時ファイルとしてコピーし、関数に与え処理した結果をページの再読み込み時に表示という手順を踏んでいます。もちろんPHPはサーバサイドで呼ばれるので、結果の表示に最低一回の画面遷移を必要とするnon-Ajaxなプログラムです。今回はこのプログラムを新しくAjax対応させて、OnlyCSViewという名前のWEBサービスにする計画を立てました。

完成までには色々と仕様的な壁にぶち当たりました。その過程をここに順を追って記しておくことで、これからPHPプログラムをAjax対応させようとする方、ならびに自分自身が同じ轍と格闘しないようにしようと思います。

Ajax対応のキモ 〜jQuery.post命令でPHPerにも直感的に

まずは今回のajax対応に関しては、jQueryを使うということに決めました。理由としてライブラリを使わない素のJavaScriptでの開発は、非同期処理実装のためブラウザ毎に異なった命令を使わなければならないということがあり、面倒臭くスマートでないからです。面倒臭いことは時にプログラミングの醍醐味でもあったりしますが、できればメイン部分の処理か、あるいは誰もやらないような事の方に労力をかけたいと思いますよね。
Ajax部分はPHPプログラムで慣れたPOSTを使いたかったので、jQuery.post命令を使います。

jQuery.post(引数1,引数2,引数3,引数4)
引数1 呼び出すurl 今回の例ではcsvtojson.phpという処理部分のファイル
引数2 呼び出し先に与えるデータ。複数の場合はラベルをつける {"label1" : data1,"label2" : data2}
PHP側では$_POST["label1"]のように取り出せる。省略可
引数3 エラーも含め値が帰ってきたら行う処理。省略可
引数4 データの種類。省略可

csvtojson.phpには何を書けば良いでしょうか。PHPのみで作ったCSV表示プログラムの中から、ユーザが指定したCSVファイルが何かを知る機能、と、配列に格納したデータを表に整形する機能、を取り除いて、この2つの機能はJavaScript側で実装するようにします。そうすることで、csvtojson.phpは入力がファイルパス(文字列)、出力が配列という単機能のプログラムにまとまります。
PHPとJavaScriptで配列の受け渡しをするには、JSONというフォーマットを使います。上に書いたjQuery.postの説明の引数2のところで既に出してしまいましたが、何の事はなくJavaScriptのオブジェクトの書式と同じです。ただしラベルは必ず文字列でなければなりません。PHPの配列をJSONにエンコード/デコードする際には、配列をjson_encode関数もしくはjson_decode関数に与えてやるだけで済みます。最後にエンコード済みのJSONフォーマットをechoして、csvtojson.phpの仕事は終わりです。

jQuery.postの方では、引数4に”json”と指定してやる事で、帰ってきたデータをJSONと解釈してくれます。その他にとりうるオプションとして、”xml”,”html”,”script”,”jsonp”,”text”があります。もしCSVの整形表示部までcsvtojson.phpに担当させる設計にするならば、整形後のhtmlを”html”で受けるといった具合です。引数3はfunction(){…と結果が帰ってきた際の処理を匿名関数でそのまま書いてしまえば良く、さらにこの関数の引数として宣言された変数で、帰ってきたデータを受けられます。
実際にjQuery.postでcsvtojson.phpを呼んだ部分はこのようになりました。

jQuery.post("csvtojson.php", {"filepath" : filepath,"charset" : charset},function(data,status){
//処理。変数dataはcsvtojson.phpが返してきた値(JSON)。
//data[i][j]のようにアクセスして値を取り出せる。省略可。
//変数statusには成功、失敗などのステータスが入る。省略可。
},"json");

Ajax対応落とし穴 〜JavaScriptはファイルのローカルパスを知ることができない

メイン処理部分は後回しにして、csvtojson.phpに与えるローカルファイルパス取得の処理を考えます。JavaScriptではWEBページ内のフォームに入力された値を、フォームのname属性で特定して操作する事ができます。したがって、ユーザーに<input type=”file”>でローカルファイルをセットさせて、その値を取り出す。理論的にはそれで大丈夫なはずです。

//HTML部分
<input type="file" name="csvfile" accept="application/excel" />
.
.
.
//JavaScriptでセットされたパスを取得
var filepath = document.controlls.csvfile.value;

ところが、実際にはこの書き方ではパスを取得できません。試しに上のコードに続いてalert(filepath);というコードを実行すると、結果は以下のようになります。

fakepath

fakepathという単語で、実際のファイルパスがマスキングされています。良く言われる、JavaScriptではセキュリティ対策のためローカルファイルにアクセスできないという表現ですが、ファイルへのアクセスにエラーが出るという実装ではなく、そもそもローカルファイルパスを知る事ができないという仕様によるもののようです。したがって、PHPに受け渡してしまえば処理できるというものでもありません。

Ajax対応のキモ 〜HTML5のFile APIを使ってみる

そこで、ローカルファイルアクセスの問題を克服したとこちらも良く言われる、HTML5を使います。HTML4以前で作られたページでHTML5の機能を有効にするには、DOCTYPE宣言をこれまでの複雑なものから、<!DOCTYPE html>というシンプルなものに変えるだけです。詳しい説明はまた別のエントリで行いますね。
HTML5は2014年の正式勧告を目指して仕様策定中という段階であり、API導入の状況は各ブラウザによってまちまちです。今回使用するFile APIは、現時点でFirefoxとChromeが対応、Safariは部分的対応といった状況です。今回のプログラムがFirefoxおよびChromeのみ対応となってしまうのは、このSafari未対応部分の機能を使うからです。
File API対応ブラウザであれば、<input type=”file”>でユーザが選択したファイルに、name属性でなくid属性でアクセスできます。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//JavaScriptでセットされたファイルを変数に代入
var userfile = document.getElementById("csvfile").files[0];
//ファイル名取得
var name = userfile.name;
//サイズ取得
var size = userfile.size;
//タイプ取得
var type = userfile.type;
//urn取得
var urn = userfile.urn;

File APIは複数ファイルの選択にも対応しているため、セットされたファイルはインデクスで指定して取り出します。一つしかセットされていない場合はfiles[0]で指定します。
このurnというプロパティが、いかにもファイルパスだろうという安易な予想をしたのですが、実際ローカルファイルをセットして値を取得してみると空でした。大抵のFile APIの解説でも省略されているのですが、URNというのは、URLと同じくICANNによって唯一性が保証されたファイル名のことで、パスのようにユーザが主体的に値を設定せずとも存在しているものではないようです。いずれは活用するようになるかもしれない名前空間といったところでしょうか。

Ajax対応落とし穴 〜objectURLはセッションを越えられない

File APIにはローカルファイルのパスを知る2通りの方法が用意されています。どちらの方法も発想の逆転で、指定されたローカルファイルに新しくパスを与えるということでセキュリティ上の問題をクリアしています。方法の1つ、objectURLはファイルへの参照を示し、2つ目のDataURLはurl文字列にファイルの内容をそのまま展開してしまいます。もちろんobjectURLの方が圧倒的にバイト数が少ないので、Ajax通信で使うフォーマットとしてはこちらを採用したくなります。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//objectURLを作成
 
//Firefoxの場合
var filepath = window.URL.createObjectURL(document.getElementById("csvfile").files[0]);
//Chromeの場合
var filepath = window.webkitURL.createObjectURL(document.getElementById("csvfile").files[0]);
 
//jQuery.postに与える
jQuery.post("csvtojson.php", {"filepath" : filepath},function(){
//処理
},"json");

しかしながら、こうして作成したObjectURLはセッションを越えられないため、csvtojson.phpはこれを無効なパスと解釈します。残念ではあるのですが、ObjectURLにはまた別のところで活躍してもらうとしましょう。

Ajax対応のキモ 〜filereaderでData URLを作成しパス渡しを実現

Data URLというのは、先程も述べましたがファイル内容をそのまま展開してしまったURLのことで、このURLの解釈自体は大抵のブラウザが対応しています。具体的な採用例としては、Google画像検索で表示される画像がData URLです。File APIのfilereaderではアクセスしたローカルファイルをいくつかのフォーマットに展開することができるのですが、その内の一つのフォーマットがData URLなのです。filereaderの使い方を見てみましょう。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//filereaderインスタンスを作成し、メソッドでファイル読み込み
var reader = new FileReader();
//バイナリ文字列へ
reader.readAsBinaryString(document.getElementById("csvfile").files[0]);
//テキストへ(第二引数は文字コード)
reader.readAsText(document.getElementById("csvfile").files[0],"UTF-8");
//Data URLへ
reader.readAsDataURL(document.getElementById("csvfile").files[0]);
 
//読み込み終了時の処理(非同期なので他のステータスにもメソッドあり)
reader.onload = function(e){
//変数filepathに結果を代入
var filepath = e.target.result;
};

filereaderを使う場合には、FirefoxとChromeの分岐処理は必要ありません。作成されたData URLを、jQuery.postでcsvtojson.phpに送ると正常に処理されました。トラフィックには優しくない方法ですが、とりあえず既存PHPプログラムのAjax対応への糸口が見えました。

$.post("csvtojson.php", {"filepath" : filepath},function(data, textStatus){
var $table = $('<table id="ct"></table>');
for(var i in data){
var $onerow = $("<tr></tr>");
for(var j in data[i]){
$("<td></td>").text(data[i][j]).appendTo($onerow);
}
$onerow.appendTo($table);
}
$("#tablearea").append($table);
},"json");

csvtojson.phpが返してきたJSONをテーブルに整形しています。こちらもjQueryを使って幾分楽をしていますね。

ということで、とりあえず最低限のAjax対応が果たせました。デモはこちらから。
段々肉付けをして便利なサービスにしていきたいと思います。


fgetcsvで読み込んだCSVファイルをhtmlのテーブルとして出力

タイトルどおり、fgetcsv関数で読み込んだCSVファイルをテーブルにします。
PHPの動くサーバに設置してローカルのファイルを求める仕様です。サニタイジングを省略しているので公開領域には置かないでください。

csvtotable.php
 
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2>テーブル表示するcsvファイルを指定して下さい。</h2>
<form enctype="multipart/form-data" action="csvtotable.php" method="POST" />
<input type="file" name="csvfile" accept="application/excel" />
<input type="submit" value="送信" />
</form>
<br />
<?php
if(isset($_FILES["csvfile"])){
if($fp = fopen($_FILES["csvfile"]["tmp_name"],r)){
echo '<table border = "1">';
while(($row = fgetcsv($fp)) != false){
echo "<tr>";
for($i = 0; $i < count($row); $i++){
echo "<td>" . mb_convert_encoding($row[$i],"UTF8","SJIS-win") . "</td>" ;
}
echo"</tr>";
}
echo "</table>";
}
}
?>
</body>
</html>

エクセルで作成されたShift-JISのCSVファイルを読み込むという設定で、fgetcsvで読み込んだ値をいちいちmb_convert_encoding関数でShift-JISからUTF-8に変換して表示しています。この関数は引数1に変換対象の文字列、引数2に変換後エンコーディング、引数3に変換元エンコーディングを与えるのですが、変換元としてただのShift-JISを与えたケースでは、”かっこかぶ”やローマ数字などが表示できませんでした。SJIS-winと指定するのが正解なようです。
なお、このプログラムで上手く表示できない場合は前回説明したような問題が発生している可能性もあります。記事を参考にしてあたりをつけてみて下さい。


PHPでCSVファイルの読み込み/書き出しをする

表計算ソフトやデータベースなどのデータの受け渡しに使うフォーマットとして、CSVというフォーマットがあります。このフォーマットはCSV(Comma-Separated Values)の名前のとおり、複数の値をカンマで区切っただけのテキストファイルで、書式さえ理解していれば特別なソフトを使わずに作成や修正をすることができるので便利です。今回はこのフォーマットの書式の説明と、PHPで読み込んで配列に格納または出力する関数の紹介をします。

CSVの書式

CSVでは値の区切りをカンマで、データ行の区切りを改行コードで行います。たとえば表計算ソフトで以下のような表があったとします。

世界ランキング 名前 コメント
1 みかん 「次も頑張ります」
2 りんご 「妥当な結果です」
3 メロン 「スタートに失敗した。このまま腐ってしまっては仕方が無い」

この表をCSVファイルとして出力すると、次のようなテキストファイルが生成されるはずです。

世界ランキング,名前,コメント
1,みかん,「次も頑張ります」
2,りんご,「妥当な結果です」
3,メロン,「スタートに失敗した。このまま腐ってしまっては仕方が無い」

値の区切りがカンマで示され、行の区切りが改行で示されているのがわかります。値は”みかん”のように、ダブルクォーテーションで囲んでも構いません。ですが、注意すべきケースが3つ程存在します。

  1. 値がカンマを含んでいるケース
  2. 先述の通りカンマは値同士の区切りとして特別な意味を持ちますので、値自体にカンマが含まれている場合、それが区切り文字でないことを明示しなければなりません。そこで先程値を囲むのに使ってもよいと書いたダブルクォーテーションで、カンマを含んだシーケンスを挟んでそれが一つの値であると明示するのに使います。たとえば「スタートに失敗した,このまま腐ってしまっては仕方が無い」という値を、次のように囲みます。

    3,メロン,"「スタートに失敗した,このまま腐ってしまっては仕方が無い」"
    
  3. 値が改行を含んでいるケース
  4. 値が改行を含んでいる場合にも、それが列の区切りではないと明示しなければなりません。この場合もダブルクォーテーションで囲むという方法が使えます。
    「スタートに失敗した。
    このまま腐ってしまっては仕方が無い」
    という値の表記例を見てみましょう。

    3,メロン,"「スタートに失敗した。
    このまま腐ってしまっては仕方が無い」"
    
  5. 値がダブルクォーテーションを含んでいるケース
  6. 値にダブルクォーテーションが含まれている場合には、それが値の一部であって表記規則の一部でないことを明示しなければなりません。そのためにはまず値全体をダブルクォーテーションで括り、さらに値中のダブルクォーテーションは2つ重ねることによって間違って解釈されることを回避します。”スタートに失敗した。このまま腐ってしまっては仕方が無い”という値は、以下のように表記します。

    3,メロン,"""スタートに失敗した。このまま腐ってしまっては仕方が無い"""
    

    なぜダブルクォーテーションが3つ連続しているのかは、順序立てて考えると分かり易いです。大切な規則は、ダブルクォーテーションを使う場合かならず値の一番外側はダブルクォーテーションで囲まなければならないということです。

以上が基本的なCSVの書式になりますが、ソフトによって方言や独自の拡張文法も存在しています。が、国際標準として成文化されたルールは上記のものになりますので、データの広い受け渡しを想定するならばこのルールに倣っておきましょう。

PHPでのCSVファイルの取り扱い

PHPでは、CSVファイルを読み込んで二次元配列に格納する関数fgetcsvがヴァージョン4から、二次元配列をCSVのフォーマットに整形してファイルに書き込むfputcsvがヴァージョン5.1から存在しています。それぞれの書式は以下のようになります。

fgetcsv(引数1,引数2,引数3,引数4,引数5);
/*ここからコメント
引数1はfopenなどで取得したファイルポインタ。
引数2以降は省略可能。
引数2 = 行の最大長
引数3 = 値の区切り文字(デフォルトはカンマ)
引数4 = 値の囲い文字(デフォルトはダブルクォーテーション)
引数5 = エスケープ文字(デフォルトはバックスラッシュ)
*/
fputcsv(引数1,引数2,引数3,引数4);
/*
引数1 = ファイルポインタ
引数2 = 値の二次元配列
以下省略可
引数3 = 値の区切り文字(デフォルトはカンマ)
引数4 = 値の囲い文字(デフォルトはダブルクォーテーション)
*/

ただしfgetcsvの方は、値が日本語のときに上手く読み込めないという事例がネット上でも多数報告されています。どうもExcelなどのCSVの作成元が日本語文字コードにShift-JISを採用していて、PHPでユーザが設定している文字コードがEUC-JPまたはUTF-8という状況でのエラーが多いようです。さらに調べると、fgetcsv関数は文字コードの処理時に、default_charsetではなくPHPのロケール設定という利用者の地域設定を参照するようで、setlocaleという関数を使って地域と使用文字コードを明示的に設定してやらなければならないようです。

setlocale(LC_ALL, "ja_JP.UTF-8");
/*
setlocale関数の書式
setlocale(引数1,引数2);
引数1 = ロケール設定が適用される関数のカテゴリ。
とりあえずLC_ALLを指定すると適用できる全ての関数に有効になる。
引数2 = ロケール名
ja_JPまでがロケール名だが、ドットの後ろに文字コードまで指定できる。
*/

このsetlocale関数を用いて、Shift-JISのCSVファイルをUTF-8環境に読み込み表示するプログラムを個人的に実行してみました。が、結果として当方の環境ではやはり文字化けが起こってしまいました。したがって、当面の対策としては、エラーを避けるためにCSVファイルの方を整形してからプログラムに読み込ませるという方法を採用しています。具体的には、値をダブルクォーテーションで必ず囲むようにする、CSVファイルの文字コードをPHPでのエンコーディングに変換して保存し直すなどです。
CSVでのデータ受け渡しができることには計り知れないメリットがあるので、この問題が早々に解決してくれることを期待します。