LoginSignup
2
2

More than 3 years have passed since last update.

【PHP】CSVの特定列を取得する

Last updated at Posted at 2020-04-29

目的

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のページが存在しなかったため、参照させていただいたページのみ記載しております。

CSV

正規表現

正規表現確認
  • Rubular
    • サンプルデータを読み込ませて正規表現のマッチを確認できます
  • Regexper
    • 正規表現を視覚化して、漏れがないかを確認できます

処理作成で時間のかかったところ

"で挟まれているデータを1つとしてみなす部分が個人的には難しいポイントでした。
今回の処理だけでなく、データのクリーニングに利用できる技術だと思いますので、理解を深められるよう精進します。

終わりに

自分なりにクラスを作って処理を作りましたが、まだまだ甘い部分も多いと思います。

クラスを作った後にfget_csvという関数の存在を知りました。
最初からこれを利用していれば、クラスを作る必要もなかったですね……。

もっとこうした方が良い、このやり方おかしいんじゃない?等、ツッコミをいただけると嬉しいです!

2
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2