PHPでCSVファイル読み込み
SplFileObjectを使ってCSVファイルを読み込んでみる。
SplFileObjectとは
PHP の Standard PHP Library(SPL) に含まれるクラスで、ファイル操作のためのインターフェイスを提供するもの。
値内に改行も対応しているので、他のCSV関係の関数よりも扱いやすい。
CSVファイルの読み込みにはこれを使うのが良い。
使用環境
・ Amazon Linux 2
・ PHP 8.2.9 ※default_charset = UTF-8 環境
※以下はUTF-8環境を前提に書いているが、そうでない場合は話が変わるので注意。
基本サンプル
行毎に読み込んで、var_dump() 出力するだけのサンプル。
<?php
// インスタンス生成
$fp = new SplFileObject( './sample.csv' );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// エスケープのデフォルトが「\」なので設定解除
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $line ) {
var_dump( $line );
}
PHP対応している文字コードはUTF-8(BOMなし)のみ。
UTF-8(BOMあり)は一見問題ないように見えるが、BOMがデータの一部として付いてくるので少し加工してやる必要がある。
改行コードは、LF、CRLFのどちらも問題ないが、CRは動きがやや怪しい。
// エスケープのデフォルトが「\」なので設定解除
$fp->setCsvControl( escape: '' );
PHPこの部分、設定を変更しないとバックスラッシュ(半角円マーク)がエスケープ記号として扱われてしまうので注意。
設定を変えることで、バックスラッシュがただの文字として扱われる。
PHP7以前だと名前付き引数が使えないので、その場合は以下でOK。
$fp->setCsvControl( ',', '"', '' );
PHP空行対策
上記例では、途中や末尾の空行もそのまま読み込まれる。
空行を無視したい場合はスキップする処理を追加する。
<?php
// インスタンス生成
$fp = new SplFileObject( './sample.csv' );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// エスケープのデフォルトが「\」なので設定解除
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $i => $line ) {
// 必要に応じて空行スキップ
if ( is_null( $line[0] ) || $line[0] === '' ) {
continue;
}
var_dump( $line );
}
PHPis_null() では空文字が判定出来ないので空文字を追加している。
末尾の空行はnullになるが、途中行のブランクは空文字が取得されるケースがあるため。
上記では1列目がブランクかどうかだけで判定しているが、データによってはそれではダメ。
なので適宜調整が必要。
以下のように setFlags() の設定で空行を飛ばすやり方もある。
$fp->setFlags(
SplFileObject::READ_CSV |
SplFileObject::READ_AHEAD |
SplFileObject::SKIP_EMPTY |
SplFileObject::DROP_NEW_LINE
);
PHPただこれは、以下の点に注意が必要。
- UTF-8(BOMあり)を読み取る場合、先頭行が空行だと空行扱いされない(BOMがあるから)
- PHPのバージョンによって挙動が異なる(古いバージョンを使ってなければ大丈夫とは思われる)
- 空行の定義が曖昧(該当行に値がなくカンマ(区切り文字)だけがある場合、空行扱いにならない)
UTF-8(BOMあり)対策
BOM有りのファイルを読み込む場合、1行目の先頭にBOMのデータがくっついてくる。
先頭行の個別処理を入れておくことで、BOM有り無しの両方に対応出来る。
<?php
// インスタンス生成
$fp = new SplFileObject( './sample.csv' );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// エスケープのデフォルトが「\」なので設定解除
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $i => $line ) {
// 頭にあるBOMを消す
if ( $i === 0 ) {
$line[0] = preg_replace( '/^\xEF\xBB\xBF/', '', $line[0] );
}
// 必要に応じて空行スキップ
if ( is_null( $line[0] ) || $line[0] === '' ) {
continue;
}
var_dump( $line );
}
PHPこの処理は、空行スキップよりも前に入れる必要がある。
(BOMのデータがNULLや空文字判定を阻害するので先に取り除く必要がある)
Shift-JISのCSVファイルに対応
Shift-JISのCSVファイルも読み込む場合、読み込みながらエンコードするか、事前にUTF-8に変換したファイルを用意するか、のどちらかになる。
※以下例、UTF-8(BOMなし)とShift-JISに対応しているが、BOMあり対策は省略しているので注意。
まずは読み込みながらエンコードする場合のサンプル。
<?php
// 文字コードを判定して、SJISだったら変換する
function convertEncoding( null|string $v ) : null|string
{
if ( ( $code = mb_detect_encoding( $v, 'UTF-8,SJIS-win,SJIS' ) ) === 'UTF-8' ) {
return $v;
}
return mb_convert_encoding( $v, 'UTF-8', $code );
}
// インスタンス生成
$fp = new SplFileObject( './sample.csv' );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// エスケープのデフォルトが「\」なので設定解除
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $line ) {
$line = array_map( 'convertEncoding', $line );
var_dump( $line );
}
PHP値1つ1つに対して文字コードを判定し、必要に応じてUTF-8に変換している。
シンプルに実装出来るが、文字コードの判定・変換を値毎にやっているのでやや処理負荷が高い(遅い)。
次に、事前にUTF-8に変換する方法。
<?php
// 文字コードを判定して、SJISだったら変換する
function convertEncoding( null|string $v ) : null|string
{
if ( ( $code = mb_detect_encoding( $v, 'UTF-8,SJIS-win,SJIS' ) ) === 'UTF-8' ) {
return $v;
}
return mb_convert_encoding( $v, 'UTF-8', $code );
}
// 文字コードUTF-8のCSVファイルを事前に用意
$filePath = '/tmp/' . uniqid() . '.csv';
file_put_contents( $filePath, convertEncoding( file_get_contents( './sample.csv' ) ) );
// インスタンス生成
$fp = new SplFileObject( $filePath );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// 区切り文字等を設定する
// (エスケープのデフォルトが「\」なので、そこだけなしに変更している)
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $line ) {
var_dump( $line );
}
PHP文字コードの確認・変換を1回で終えており、速い。
ただ、CSVファイルの容量分メモリを使うので、ファイルが大きい場合はそちらが問題になる。
メモリも変換の回数も抑えるには、以下のような形が妥当か。
<?php
// 文字コードを判定して、SJISだったら変換する
function convertEncoding( null|string $v ) : null|string
{
if ( ( $code = mb_detect_encoding( $v, 'UTF-8,SJIS-win,SJIS' ) ) === 'UTF-8' ) {
return $v;
}
return mb_convert_encoding( $v, 'UTF-8', $code );
}
// 文字コードUTF-8のCSVファイルを事前に用意
$filePath = '/tmp/' . uniqid() . '.csv';
$fpRead = fopen( './sample.csv', 'r' );
$fpWrite = fopen( $filePath, 'w' );
while ( $line = fgets( $fpRead ) ) {
fwrite( $fpWrite, convertEncoding( $line ) );
}
fclose( $fpRead );
fclose( $fpWrite );
// インスタンス生成
$fp = new SplFileObject( $filePath );
// CSVとして読み込む事を指定
$fp->setFlags( SplFileObject::READ_CSV );
// 区切り文字等を設定する
// (エスケープのデフォルトが「\」なので、そこだけなしに変更している)
$fp->setCsvControl( escape: '' );
// 1行ずつ読み込んで処理する
foreach ( $fp as $line ) {
var_dump( $line );
}
PHP文字コードの確認・変換は行毎に行っている。
その分遅くはなるが、値毎にやるよりは格段に速い。
事前変換はSplFileObjectではなくfopen()でやっているが、こちらの方がメモリ使用量が若干少ない。
速度も誤差の範囲ではあるが、若干fopen()の方が速い印象。