ポイント
mpyw-yattemita/excel-csv-compatibility-check
上記リポジトリで互換性を確認した結果,一般的には,以下の2つのいずれかの選択肢をとるべきであることが分かりました。
- Shift_JISでCSV形式にして拡張子csvで保存
- UTF-16(LE)でTSV形式にして拡張子csvで保存
この記事では,UTF-16(LE)を取り扱うことを前提とします。
注意点
- Unicodeを用いる場合,文字セットに応じたBOMが必要である。
- UTF-8なら
0xEF
0xBB
0xBF
- UTF-16(LE) なら
0xFE
0xFF
- UTF-16(BE) なら
0xFF
0xFE
- UTF-8なら
- 「UTF-16(LE)」と「UTF-16LE」は別物なので注意。「UTF-16LE」にはBOMが付かない。
- Shift_JISのほうが文字の守備範囲が狭く,絵文字やラテン文字・ハングル文字などに対応できない。文字化けを回避したい場合はUTF-16(LE)を選択すべき。
基本
「新規作成w
」「読み出しr
」の2パターンを紹介します。
ファイルポインタリソース版
新規作成
<?php
// 挿入するデータ
$rows = [
['佐倉綾音', '保登心愛'],
['水瀬いのり', '香風智乃'],
];
// ファイルをオープン
$fp = fopen(__DIR__ . '/data.csv', 'wb');
// 書き込み時にUTF-8からUTF-16LEに変換
// (UTF-16にするとBEになってしまうため,UTF-16LEとしてオープンして後からBOMを書き込む)
stream_filter_prepend($fp, 'convert.iconv.utf-8/utf-16le');
// BOMを書き込んだ後,1行ずつファイルに書き込む
fwrite($fp, "\xEF\xBB\xBF");
foreach ($rows as $row) {
fputcsv($fp, $row, "\t");
}
読み出し
<?php
// ファイルをオープン
$fp = fopen(__DIR__ . '/data.csv', 'rb');
// 読み出し時にUTF-16(BE/LE)からUTF-8に変換する
// (自動でBOMは取り除かれる)
stream_filter_prepend($fp, 'convert.iconv.utf-16/utf-8');
// 1行ずつ取り出す
while ($row = fgetcsv($fp, 0, "\t")) {
if ($row === [null]) continue; // 空行を読み飛ばす
var_dump($row);
}
SplFileObject版
新規作成
<?php
// 挿入するデータ
$rows = [
['佐倉綾音', '保登心愛'],
['水瀬いのり', '香風智乃'],
];
// 書き込み時にUTF-8からUTF-16LEに変換するようにしてファイルオープン
// (UTF-16にするとBEになってしまうため,UTF-16LEとしてオープンして後からBOMを書き込む)
$filename = __DIR__ . '/data.csv';
$spec = "php://filter/write=convert.iconv.utf-8%2Futf-16le/resource=$filename";
$file = new \SplFileObject($spec, 'wb');
$file->setCsvControl("\t"); // デフォルトの区切り文字をタブに変更する
// BOMを書き込んだ後,1行ずつファイルに書き込む
$file->fwrite("\xEF\xBB\xBF");
foreach ($rows as $row) {
$file->fputcsv($row);
}
読み出し
<?php
// 読み出し時にUTF-16(BE/LE)からUTF-8に変換するようにしてファイルオープン
// (自動でBOMは取り除かれる)
$filename = __DIR__ . '/data.csv';
$spec = "php://filter/read=convert.iconv.utf-16%2Futf-8/resource=$filename";
$file = new \SplFileObject($spec, 'rb');
$file->setFlags(
\SplFileObject::READ_CSV | // foreachの反復処理をfgetcsvで行う
\SplFileObject::SKIP_EMPTY | // 空行を読み飛ばす (これが無いと空行を [null] として読み取ってしまう)
\SplFileObject::READ_AHEAD | // 空行を読み飛ばすためには必須
\SplFileObject::DROP_NEW_LINE // 空行を読み飛ばすためには必須
);
$file->setCsvControl("\t"); // デフォルトの区切り文字をタブに変更する
// 1行ずつ取り出す
foreach ($file as $row) {
var_dump($row);
}
応用
「新規作成or追記→読み出しa+
」の場合を紹介します。これは掲示板スクリプトなどでの利用を想定しているため,ファイルロック処理も入れています。
ファイルポインタリソース版
新規作成or追記→読み出し
<?php
// 挿入するデータ
$rows = [
['佐倉綾音', '保登心愛'],
['水瀬いのり', '香風智乃'],
];
// ファイルをオープン
$filename = __DIR__ . '/data.csv';
$fp = fopen($filename, 'a+b');
// 書き込み時にUTF-8からUTF-16LEに変換する
// (UTF-16にするとBEになってしまうため,UTF-16LEとしてオープンして後からBOMを書き込む)
stream_filter_prepend($fp, 'convert.iconv.utf-8/utf-16le', STREAM_FILTER_WRITE);
// 排他ロックと初回のみBOMの書き込みを行った後末尾に移動し,1行ずつファイルに書き込む
flock($fp, LOCK_EX);
if (filesize($filename) < 2) {
fwrite($fp, "\xEF\xBB\xBF");
clearstatcache();
}
foreach ($rows as $row) {
fputcsv($fp, $row, "\t");
}
// 先頭に戻る
rewind($fp);
// 読み出し時にUTF-16(BE/LE)からUTF-8に変換する
// (自動でBOMは取り除かれる)
stream_filter_prepend($fp, 'convert.iconv.utf-16/utf-8', STREAM_FILTER_READ);
// 共有ロックに切り替えた後,1行ずつ取り出す
flock($fp, LOCK_SH);
while ($row = fgetcsv($fp, 0, "\t")) {
if ($row === [null]) continue; // 空行を読み飛ばす
var_dump($row);
}
SplFileObject版
新規作成or追記→読み出し
<?php
// 挿入するデータ
$rows = [
['佐倉綾音', '保登心愛'],
['水瀬いのり', '香風智乃'],
];
// 書き込み時にUTF-8からUTF-16LE
// 読み出し時にUTF-16LEからUTF-8に変換するようにしてファイルオープン
// (新規作成する可能性がある場合はreadにUTF-16は指定出来ないのでUTF-16LEにする)
$filename = __DIR__ . '/data.csv';
$spec = "php://filter/read=convert.iconv.utf-16le%2Futf-8/write=convert.iconv.utf-8%2Futf-16le/resource=$filename";
$file = new \SplFileObject($spec, 'a+b');
$file->setFlags(
\SplFileObject::SKIP_EMPTY | // 空行を読み飛ばす (これが無いと空行を [null] として読み取ってしまう)
\SplFileObject::READ_AHEAD | // 空行を読み飛ばすためには必須
\SplFileObject::DROP_NEW_LINE // 空行を読み飛ばすためには必須
);
$file->setCsvControl("\t"); // デフォルトの区切り文字をタブに変更する
// 排他ロックと初回のみBOMの書き込みを行った後末尾に移動し,1行ずつファイルに書き込む
$file->flock(LOCK_EX);
if (filesize($filename) < 2) {
$file->fwrite("\xEF\xBB\xBF");
clearstatcache();
}
foreach ($rows as $row) {
$file->fputcsv($row);
}
// 共有ロックに切り替えた後2バイトを除いた先頭に戻り,1行ずつ取り出す
// イテレータは使えないので手動でfgetcsvする
$file->flock(LOCK_SH);
$file->fseek(2);
while ($row = $file->fgetcsv()) {
var_dump($row);
}
※ fgets
に対してこのwhileループの書き方は出来ないので注意
なんかイマイチですね。どうせならクラス継承しちゃいましょうか。
新規作成or追記→読み出し (SplFileObject継承)
<?php
// 書き込むデータ
$rows = [
['佐倉綾音', '保登心愛'],
['水瀬いのり', '香風智乃'],
];
// 追記と読み出しが可能なCSVファイルとしてオープン
$file = new ExcelCsvFileObject(__DIR__ . '/data.csv');
// 排他ロックのあと1行ずつファイルに書き込む
$file->flock(LOCK_EX);
foreach ($rows as $row) {
$file->fputcsv($row);
}
// 共有ロックに切り替えて1行ずつ取り出す
$file->flock(LOCK_SH);
foreach ($file as $row) {
var_dump($row);
}
/**
* Excel互換のCSVに特化したSplFileObjectのラッパー
* 常にa+bモードでオープンする
*/
class ExcelCsvFileObject extends \SplFileObject
{
private $position = 0;
private $current;
/**
* @param string $filename ファイル名
* @throws RuntimeException
*/
public function __construct($filename)
{
parent::__construct("php://filter/read=convert.iconv.utf-16le%2Futf-8/write=convert.iconv.utf-8%2Futf-16le/resource=$filename", 'a+b');
$this->setFlags(
\SplFileObject::READ_CSV |
\SplFileObject::SKIP_EMPTY |
\SplFileObject::READ_AHEAD |
\SplFileObject::DROP_NEW_LINE
);
$this->setCsvControl("\t");
if (filesize($filename) < 2) {
$this->flock(LOCK_EX);
$this->fwrite("\xEF\xBB\xBF");
$this->flock(LOCK_UN);
clearstatcache();
}
$this->rewind();
}
/**
* @return string|null
*/
public function current()
{
return $this->current;
}
/**
* @return int
*/
public function key()
{
return $this->position;
}
/**
* @return null
*/
public function next()
{
$this->current = $this->fgetcsv();
++$this->position;
}
/**
* @return null
*/
public function rewind()
{
$this->fseek(2);
$this->position = 0;
$this->current = $this->fgetcsv();
}
/**
* @return bool
*/
public function valid()
{
return $this->current !== null;
}
}