目的
CSVを読み込み、カラム名で指定した値を取得したい。
環境
Windows 10 Home 64bit
XAMPP Version 7.3.11
(PHP 7.3.11 (VC15 X86 64bit thread safe) + PEAR)
対応
CSV用のクラスを作成して対応しました。
説明はコメントとして記載しているので、割愛します。
以下、ソースコードです。
CSVハンドリングクラス
2020-0501 追記
改行、ダブルクォートが含まれるデータへの対応を追加しました
/*
* CSV分割クラス
*/
class csv_handle
{
// 文字エンコード用定数
private const ENCODING = 'UTF-8';
/**
* ダブルクォートが閉じられているか判断する用の定数
* ※エンクロージャ2つで1フィールドなので、定数を用いてフィールドの途中で改行が入っても1つのデータとしてまとめるようにする
*/
private const ENCL_END = 2;
// ヘッダ
private $csv_header = array();
private $data = array();
/**
* 引数の文字列をデリミタで分割した配列を取得する
*
* @param string $line 分割対象の文字列
* @param string $delim デリミタ(区切り文字)
* @param string $encl エンクロージャ(データ領域を囲う文字)
* @return array
**/
private function get_separated_array(string $line, string $delim, string $encl)
{
// 正規表現を使うので、エスケープをしておく
$ecl = preg_quote($encl);
$dlm = preg_quote($delim);
/**
* CSV分解用に正規表現を設定
* $delim = ',' $encl = '"' の場合、以下の正規表現になる
* ""が2回続く場合はは、フィールドの切れ目としてみなす必要があるので、以下の正規表現とする
* ("[^"]*(""[^"]*)*"|[^,]*),
* -------------------------- c
* ----------------- -----
* a b
* aの説明
* a. "[^"]* -> " かつ、次が " でない任意の文字列
* b. (?:""[^"]*) -> "" かつ、次が " でない任意の文字列(" " 形式で囲まれたフィールド内のエスケープされた " を取得)
* (?:xxxx)という表現で、グループとしてキャプチャされなくなる
* c. *" -> 任意の文字で、" が見つかるまでの文字列
* -> "" となっていないかつ、
* bの説明
* a. [^,]* -> , でない任意の文字列
* cの説明
* ("[^"]*(""[^"]*)*"|[^,]*), -> ①か②に合致かつ、 , が見つかるまでの文字列
* 正規表現図 -> https://regexper.com/#%28%22%5B%5E%22%5D*%28%22%22%5B%5E%22%5D*%29*%22%7C%5B%5E%2C%5D*%29%2C
*/
$a_a = '[^'. $ecl .']*';
$a_b = '(?:' . $ecl . $ecl . '[^'. $ecl .']*)';
$a_c = '*'. $ecl;
$b_a = '[^' . $dlm. ']*';
$pattern = '/(' . $ecl . $a_a . $a_b . $a_c . '|' . $b_a . ')' . $dlm . '/';
// 正規表現で全文字をマッチさせるため、文字列の終端にデリミタを付与
$line_add_delim = $line . $delim;
// 正規表現で分解し、配列として取得
$match_result = preg_match_all($pattern, $line_add_delim, $separated);
$result = array();
/**
* 取得した配列は[0]がパターン全体のデータ、
* [1]が最初の()でマッチしたデータ、[2]が2番目の()でマッチしたデータ、、、、となっている
* 今回は1つしか()がないので、[1]を検知結果として取得する
* また、[1]には最後の$delimが含まれていないため、$delimをトリムする処理も不要
*/
foreach ($separated[1] as $value) {
// ダブルクォートが入っている場合は削除
// $replace = str_replace('"', '', $value);
$replace = preg_replace('/^' . $ecl . '(.*)' . $ecl . '$/s', '$1', $value);
$replace = str_replace($ecl . $ecl , $ecl, $replace);
$result[] = $replace;
}
return $result;
}
/**
* CSVファイルを読み込み、クラスにデータを保持する
*
* @param string $file_path 読み込みファイルパス
* @param string $delim デリミタ(区切り文字)
* @param string $encl エンクロージャ(データ領域を囲う文字)
* @param string $encode エンコード情報
**/
public function read_csv(string $file_path, string $delim, string $encl, string $encode = self::ENCODING){
// 最後の改行コードは無視する
$file = file($file_path, FILE_IGNORE_NEW_LINES);
// 内部ではUTF-8で情報を持つ
$file_enc_conv = mb_convert_encoding($file, self::ENCODING, $encode);
$target_array = array();
$target_line = "";
$delim_count = 0;
foreach($file as $line){
// 処理対象行に追加(改行コードを含む場合は、PHPのシステム環境の改行コードを明示的に付与する)
$target_line .= 0 === $delim_count ? $line : PHP_EOL . $line;
// エンクロージャの数を取得
$delim_count += substr_count($line, $encl);
// エンクロージャは2つでセットなので、2で割り切れない場合は1つのレコード情報とする
if (0 === $delim_count % self::ENCL_END){
$target_array[] = $target_line;
$target_line = "";
$delim_count = 0;
}
}
// ヘッダーの登録
$this->csv_header = $this->get_separated_array(array_shift($target_array), $delim, $encl);
// 実データの登録
foreach($target_array as $value){
$this->data[] = $this->get_separated_array($value, $delim, $encl);;
}
}
/**
* 指定したカラムのデータを取得する
*
* @param string $column_name カラム名(可変長引数)
**/
public function get_column_data(string ...$column_name){
// 取得カラムのインデックスを取得
$column_indexes = array();
foreach($column_name as $column){
$tmp = array_search($column, $this->csv_header);
// 存在しないカラムの場合は、エラーメッセージを出力
if(false === $tmp){
echo printf("no column : %s\n", $column);
continue;
}
$column_indexes[] = $tmp;
}
// 戻り値の作成
$result = array();
foreach($this->data as $index => $value){
foreach($column_indexes as $c_index){
$result[$index][$this->csv_header[$c_index]] = $value[$c_index];
}
}
return $result;
}
}
テストデータ(split_test.csv)
2020-0430 追記
※ CSVのフィールドデータ内に改行やダブルクォートが含まれないデータを前提としております
2020-0501 追記
テストデータに、改行、ダブルクォートが含まれるデータを利用するように変更しました
c1,c2,c3,c4,c5
"テスト","日
,
a""
本",語,1,2
"av","a,c",c,11,88
"v","b,d",c,991,38
"g","g,k",c,21,88
"aaa","b,l",c,5,88
使い方
// 読み込みファイルパス
$path = 'D:/tmp/split_test.csv';
// クラス初期化
$line_sep = new csv_handle();
// CSV読み込み
$line_sep->read_csv($path, ',', '"');
// c1, c2 の列のみ取得
$result = $line_sep->get_column_data('c1', 'c2');
// 結果を表示
print_r($result);
出力結果
Array
Array
(
[0] => Array
(
[c1] => テスト
[c2] => 日
,
a"
本
)
[1] => Array
(
[c1] => av
[c2] => a,c
)
[2] => Array
(
[c1] => v
[c2] => b,d
)
[3] => Array
(
[c1] => g
[c2] => g,k
)
[4] => Array
(
[c1] => aaa
[c2] => b,l
)
)
参考サイト
以下のリンクを参照させていただきました。
PHP分割処理
改行、""
の対応については、以下のページを参照し処理を流用させていただきました。
本来であれば、fgetcsv_reg
を作成されたyossyさんのページもリンクするべきですが、
該当URLのページが存在しなかったため、参照させていただいたページのみ記載しております。
-
PHP5のfgetcsvに関して
- yossyさんの
fgetcsv_reg
を改良されたソースを記載いただいてます
- yossyさんの
CSV
正規表現
正規表現確認
処理作成で時間のかかったところ
"
で挟まれているデータを1つとしてみなす部分が個人的には難しいポイントでした。
今回の処理だけでなく、データのクリーニングに利用できる技術だと思いますので、理解を深められるよう精進します。
終わりに
自分なりにクラスを作って処理を作りましたが、まだまだ甘い部分も多いと思います。
クラスを作った後にfget_csvという関数の存在を知りました。
最初からこれを利用していれば、クラスを作る必要もなかったですね……。
もっとこうした方が良い、このやり方おかしいんじゃない?等、ツッコミをいただけると嬉しいです!