SplFileObjectを使ってCSVファイルからSeederを作成する

プログラミング

他サービスのデータを使うために別DBからデータをCSVエクスポートして、そのデータをSeederとして利用したいという場面があったのでその時の実装方法をまとめます。

といってもSeederで扱う方法はおまけで、基本的にはPHPでCSVファイルを扱う方法について説明します。

Laravelなどのフレームワークでシステムを構築しているとCSVを扱う機能はよくあると思います。

しかし、PHPでファイルを読み込む方法はいくつか存在するため、どの方法が良いかわからないということがあります。

そこで今回はfile()SplFileObjectクラスで扱う方法を取り上げたいと思います。

書いてあること

・PHPでCSVファイルを読み込む方法(file()とSplFileObject)
・file()とSplFileObjectのパフォーマンスの違い

・CSVデータをSeederで利用する方法

CSVファイルからデータを取得する

今回私がやりたかったことは決まったデータをCSVで読み込むということでした。

ただほとんどの場合、第三者がCSVファイルをアップロードしたりするため、ファイルサイズが確実に決まっていないパターンがほとんどだと思います。

そのため想定よりも大きいサイズのCSVファイルを処理しなければいけない場面も出てきます。

こういったことを考慮するためにCSVファイルを読み込んで処理する際は、1行ずつ読み込んで処理していくのがベストプラクティスとされているようです。

CSVファイルをすべて読み込んでから処理してしまうとメモリ不足でエラーが発生したりしてしまうためです。

今回はfile()SplFileObjectクラスの方法を取り上げ、処理速度なども検証したいと思います。

file()

ファイル全体を読み込んで配列に格納する

https://www.php.net/manual/ja/function.file.php

特徴としてはファイル全体を読み込んで各行のデータを配列に格納する点、ファイルの指定だけでなくURLを指定してWEBページも取得できるなどがあります。

先述しましたが、CSVファイルの読み込みにおいてすべてを読み込むのはあまり良くありません。

似たメソッドとしてfile_get_contents()が存在しますが、こちらは配列に格納するのではなく、文字列としてファイルの中身を返します。

ログファイルの解析などに利用されることが多いようです。

バイナリファイルには対応していないことは気をつけるべきポイントですね。

SplFileObjectクラス

SplFileObject クラスはファイルのためのオブジェクト指向のインターフェイスを提供します。

https://www.php.net/manual/ja/class.splfileobject.php

file()とは異なりファイル全体を一気に読み込まず、1行ずつ読み込むことが可能なためcsvのファイルサイズに左右されずに処理を行うことが可能です。

基本的な使い方としては、コンストラクタにファイルパスを指定してあげるだけです。
また、オープンモードはデフォルトで「r」で読み取りになっています。
今回の例では読み取りがメインですが、書き込みを行うときもオープンモードに「w」と指定してあげることでファイルに書き込みも可能になります。

setFlagsメソッドで4つのフラグを指定することでき、指定するフラグによってファイルをどのように読み込むかを設定できます。

SplFileObject::DROP_NEW_LINE
  行末の改行を読み飛ばします。

SplFileObject::READ_AHEAD
  先読み/巻き戻しで読み出します。

SplFileObject::SKIP_EMPTY
  ファイルの空行を読み飛ばします。期待通りに動作させるには、READ_AHEAD フラグを有効にしないといけません。

SplFileObject::READ_CSV
  CSV 列として行を読み込みます。

https://www.php.net/manual/ja/class.splfileobject.php

READ_AHEADの先読み/巻き戻しで読み込むという点はあまりよくわかりませんでしたが、SKIP_EMPTYで空行を読み飛ばすために必要という認識で落ち着きました。

ファイルクローズのコマンドはないため、処理が終わった後はインスタンスにnullを代入することでインスタンスを破棄します。

CSVであればファイルの各行をforeachで1行ずつ処理していくことでメモリを無駄に使用せず処理できます。

file()とSplFileObjectのパフォーマンスの比較

使用するファイルは住所.jpの全国版を利用します。
行数は約15万行程です。

実行処理の時間とメモリの使用量を計測します。
計測はこちらの記事を元に実施しました。

まずはCSVファイルを読み取っただけの場合

  • file()
running time: 0.03351092338562 [s]
used memory: 55.20922088623 [MB]
  • SplFileObject
running time: 0.00079107284545898 [s]
used memory: 0.24273681640625 [MB]

次に15万行のデータを1行ずつ1つの配列に追加する処理を実施した場合

  • file()
running time: 0.27415800094604 [s]
used memory: 307.87184143066 [MB]
  • SplFileObject
running time: 1.0536568164825 [s]
used memory: 315.50774383545 [MB]

ファイルを読み込むだけ場合であればSplFileObjectの方がパフォーマンスが高いことがわかりますね。

ただ15万行をforeachで処理していくとSplFileObjectでも処理が重くなりfile()の方がパフォーマンスが高くなっているのでこの辺りはもう少し調査したいところです。

CSVから取得したデータをSeederで利用する

今回はSeederを例に上げていますが、読み込んだCSVファイルの扱い方はアプリケーションによって異なってくると思います。

むしろSeederで利用することは稀かなと思っています。

今回は別サービスのメールアドレスを利用するという例で実装したいと思います。

基本的なSplFileObjectを使ったCSVファイルの扱い方は一緒だと思いますでご参考までに。

class UserSeeder extends Seeder
{
    private Generator $faker;
    
    public function __construct()
    {
        $this->faker = Factory::create(config('app.faker_locale'));
    }
    
    /**
     * Run the database seeds.
     *
     * @return void
     * @throws Throwable
     */
    public function run()
    {
        DB::transaction(function () {
            $csvFile = new SplFileObject(base_path('sample.csv'));
            $csvFile->setFlags(
                SplFileObject::READ_CSV |
                SplFileObject::READ_AHEAD |
                SplFileObject::SKIP_EMPTY |
                SplFileObject::DROP_NEW_LINE
            );

            $data = [];
            foreach ($csvFile as $key => $email) {
                // ヘッダーはスキップ
                if ($key === 0) {
                    continue;
                }
                $data[] = [
                    'id' => Str::orderedUuid()->toString(),
                    'name' => $this->faker->name,
                    'email' => $email,
                ];
            }

            DB::table('users')->insert($data);
        });
    }
}

これでsample.csvから取得したメールアドレスを元にSeederの作成が完了です。

コメント

タイトルとURLをコピーしました