PHPで大量の行数のCSVファイルを分割する際にはまったポイント

表題の通りなのですが、数MBもの CSVファイルをインポートすることになり、その過程の分割スクリプトではまりました。

そもそも分割せずにデフォルトの nginx 環境に まず 1MB 以上のものファイルをアップロードすると、 以下の 413 エラーが出てしまいます。

413 Request Entity Too Large

単純に、サーバー全体でアップロードする容量を大きくしたい場合は、nginx の conf をいじるなどを検討する必要があると思うのですが、常にそうしたいわけでなく今回だけと言うことだったので、数MBのCSVファイルを1MB 未満になるようにいくつかのCSVファイルに分割したいと考えました。

安易な気持ちでスクリプトを書いていたんですが、実際に分割されたデータを見ると変なところで次の行に入ってしまうことがありました。

要は1万行のファイルを3000行ごとに分割する場合

分割ファイル1… 1 ~ 3000 行
分割ファイル2… 3001 ~ 6000 行
分割ファイル3… 6001 ~ 9000 行
分割ファイル4… 9001 ~ 10000 行

が入ると想定してたんですが、変なところで次の行に入っているので、分割ファイル4が 1037行になるみたいな形で1000行を超えてました。

以下のケースがあると発生しました。

  • データの中に改行やカンマなどの記号があった場合
  • 分割時に文字コードを変換した場合


文字コードの方はあまり条件がわかってないですが…変換の際に記号と相性が悪かったりするのかなと思っています。

初めは SplFileObject の fgets() メソッドを使っていたのですが、使わなければ「データの中にカンマがあった場合」以外は解決しました。
setFlags() で うまいこと回避できたかもしれませんが…。

カンマがあった場合については、別の文字(今回は半角スペース)に置き換えました。これでいいかはユースケースに依りますが、今回はこれでよかったのでそうしています。

ということで長々しゃべってしまいましたが、コードです

$filePath = '分割前のファイルのパス';
$rowsPerFile = 3000; // 3000行ごとに分割する
$outputDir = '分割したファイルを置く場所のパス';
$hasHeader = true; // 分割前のファイルがヘッダーを持っているか

$splitter = new FileSplitter();
$splitter->split($filePath, $rowsPerFile, $outputDir, $hasHeader);

class FileSplitter {
    private ?array $rows;
    private int $fileCount;
    private ?string $header;

    public function split($filePath, $rowsPerFile, $outputDir, bool $hasHeader) {
        $this->fileCount = 0;
        $this->rows = null;

        $file = new \SplFileObject($filePath);
        $file->setFlags(\SplFileObject::READ_CSV);
        $rowCount = 0;

        // fgets ではなく $fileを foreach で回している
        foreach ($file as $key => $data) {
            if (!empty($data)) {
                if ($hasHeader && $key === 0) {
                    $this->header  = implode(',', $data);
                    continue;
                }

                if($rowCount % $rowsPerFile === 0) {
                    $this->writeToFile($this->generateOutputFilePath($outputDir, $file));
                }

                // カンマがあったら半角スペースに変換しているので注意
                foreach ($data as $key2 => $datum) {
                    $data[$key2] = str_replace(',', ' ', $datum);
                }

                $this->rows[] = implode(',', $data);
                $rowCount++;
            }
        }

        $this->writeToFile($this->generateOutputFilePath($outputDir, $file));
        $file = null;
    }

    private function generateOutputFilePath($outputDir, \SplFileObject $file) {
        $baseName = $file->getBasename('.'.$file->getExtension());
        $ext = $file->getExtension();
        $num = $this->fileCount + 1;
        return $outputDir.$baseName.'_'.$num.'.'.$ext;
    }

    private function writeToFile($filePath) {
        if($this->rows) {
            $file = new SplFileObject($filePath, 'w');
            
            if (!empty($this->header)) {
                $header_array = explode(',', $this->header);
                $file->fputcsv($header_array);
            }

            foreach ($this->rows as $line) {
                $contents = explode(',', $line);
                $file->fputcsv($contents);
            }
            $this->fileCount++;
            $this->rows = null;
        }
    }
}

大量データを扱うと想定外のことが起きますね。
今回たまたま見つからなかっただけのものも全然ありそうです。

AndroidManifest にそんな権限を記載していないのにしていることになって Google Play Console からその権限を利用するなら 申告してと言われた

表題のまんまです。
結論を言うとアプリのメインと言うかデフォルトのマニフェストファイル内では宣言していなくても利用しているライブラリのマニフェストで使っている可能性があります。

今回 AD_ID が引っかかってしまい実際に宣言されていたのが以下の通りでした。
Google Play Console の App Bundle の詳細より確認が出来ます。

似たような人は結構いて、今回の場合は firebase ががっつり使っていましたね…。

[GA4] データ収集

android studio で ライブラリが必要とする権限とかすぐにわかったりしないのかしら…。

PHPStorm デバッグの小技

弊社では、デフォルトではPHPStormというIDEを使っています。

で、下記に書くことは他のIDEでもできるかもしれませんが、今回検証やサンプルに使うのはPHPStormなので、タイトルが
「PHPStorm デバッグの小技」
となってますが、ほかのIDEでも同様の機能があるものはあると思います。

この記事は、普通のステップイン、ステップオーバーなどの基本のデバッグはもうできているというプログラミング中級者さん向けでお話していきます。

では早速。

①ブレークポイントの機能をうまく使う

1.ある例外が起こった時だけブレークするようにする

今回、この記事を書こうと思ったきっかけなんですが、こんな便利な機能があるのを私は知りませんでした!!

ある例外が起こった時に、エラーログで例外の起こるソースコードの場所はわかったとしても、パラメーターが多い場合など、どういう条件で例外が起こるのかがわからなかったりすると思います。

そんな時、例外が起こるまでステップオーバーをポチポチ押したりしていませんか?
この機能を使うと、例外が起きたときだけブレークしてくれます

次のサンプルスクリプトを使います。

<?php

$array = ['りんご','メロン','バナナ'];

foreach ($array as $key => $value){

    if($key === 2){
        throw new LogicException('バナナはダメ');
    }
    print $value;
}

やり方です。

①ヘッダーメニューのRun→View Break Points をクリック

②下記図のような画面になるので、左上のプラスボタンをクリックし、PHP Exception Breakpointsをクリック

ブレークポイント一覧の画面 この画面で思っているより多くのことができる!!

③Exceptionの名前を入力  ここでは、LogicExceptionを入力します。

上記③までやってデバッグをしてみてください。下記のExceptionで止まります。

デバッグウィンドウは、次のようになっています。

便利!

2. 特定の条件の時だけブレークするようにする

ブレークポイントを設定してデバッグする際、例えば100回ループが回るとして、99回目で問題が起こるとして、98回ポチポチステップオーバーを回していませんか??

99回目に手っ取り早くワープする方法があります。ちなみに、ほかの方法でもできますが、それは後述します。

やり方です。

①ブレークポイントを右クリックします。

こいつ…!右クリックできるぞ…っ!

② Conditionとあるところに条件を入れます。ここでは、サンプルとして$keyが1の時だけブレークするようにします。

③ ?がついたブレークポイントのアイコンができました!

デバッグを実行してみてください。
$key が 0 の時をすっ飛ばして、$keyが1の時だけブレークしてくれます!

3. 何度も不要なブレークポイントでブレークするのを避ける

プログラムの流れを追いたい時、最初の1回だけブレークすればいい場合がありますよね。しかし、最初の一回だけでいいのに繰り返すループ処理があって、何度もブレークする時、
「ああ、面倒だな」
とつぶやきながら、ポチポチステップオーバーやResumeをしていませんか??
1回ブレークしたら、後はブレークしないようにすることができます。

①普通にブレークポイントを設定する

②例えば、最初のブレークだけで、後はもう必要ないという場合は、デバッグウィンドウのMute Breakpointsという串団子みたいなアイコンをクリックします。

♪団子、団子、団子、団子~

③Resumeをクリックすると、もうブレークポイントでブレークしません。

ブレークポイントも黒くなります

ただし、このやり方だと、ほかにもブレークポイントがあるときに一気にブレークしなくなります。
一部のブレークポイントだけ、1回ブレークされればよい、という場合を次に紹介します。

4. 1回だけブレークされればよいブレークポイントを作る

3のやり方で、1回ブレークしたら後はミュートという方法がありますが、一気にミュートされると困る、ほかのブレークポイントでは止まってほしい、という場合に使える方法です。

①普通にブレークポイントを設定する。

②Run → View Break Points とクリックすると、下記のようなペイン(表示領域のこと)が表示されます。①のブレークポイントをクリックして
Remove once hit
というチェックボックスにチェックをつけます。

そうすると、上記は例えば7行目と10行目にブレークポイントを設定していますが、7行目だけ Remove once hit にチェックします。

すると7行目では最初の1回にブレークしてその後ブレークポイントがなくなりますが、10行目では2回ブレークします。

4. 特定の値があった場合のみ、consoleに表示する

ブレークポイントを通った時に、変数が特定の値になる条件を知りたい場合があると思います。

そんな時にこれが使えそうです。先程から何度か登場している、ブレークポイントペイン。皆様、もうこいつと仲良しですね?

実はLogという機能があり、さらにはそれに式を入れることができます。
$valueがメロンになるときがいつなのか、記録したい時があったとします。

$value ==’メロン’と入れておく

すると、デバッグコンソールに次のように表示されます。

2回目のループだけ、1と表示されているのがわかるでしょうか?


$value がメロンの時だけ1が表示される

2. 特定の条件の時だけブレークするようにする と似ていますが、こっちは例えばループをざっと回して、どういうパターンの時に値が〇〇なのか、などを俯瞰で見たい時などに使えると思います。

②変数の中身を書きかえる

ハァハァ… ブレークポイントだけで、結構もりだくさんでしたね💦

これはメジャーなのでご存じの方も多いと思いますが、紹介しておきます。

2の②でも書きましたが、ブレークポイントを設定してデバッグする際、例えば100回ループが回るとして、99回目で問題が起こるとして、98回ポチポチステップオーバーを回していませんか??

ブレークポイントに特定の条件をつけることも可能ですが、デバッグ中に変数の値を書き換えることで、このような無駄な作業を省くことができます。

サンプルでいいますと、例えば7行目にブレークポイントを設定してデバッグします。

たとえば、このループのデバッグが面倒なので、一気にLogicExceptionを起こす$keyを2にしたいと思った場合、デバッグコンソールで次のようにします。

①編集したい変数を右クリックして、Set Value をクリック

②好きな値を入れます。ここでは、2を入れちゃいます。

③するとすぐ評価されて、LogicExceptionを投げるところに行きます。

これってめちゃくちゃ応用が効いて、いろんなことに使えるんですよね。
普通に関数を開発中に、「この値飛んで来たらどういう処理にしよー?」とか思っている時に、実際にその値を関数に投げるのが呼び出しが深くて面倒な時があります。

そんな時にも使えるし、例えば、Cookieの設定だとか、サーバー変数の設定だとか、特定の環境を再現しないといけないデバッグだとかに、いちいち本物の環境を設定するわけにいかない場合だとかにも使えます。

③Watchを使う

あんましデバッグ時にWatchって使いませんよね(笑)。実は私もです。

ただ、使った方がいいケースがあると思うので、ご紹介しておきます。

今までのサンプルだとあまりに簡単すぎるので、少しだけ複雑なサンプルを作ります。

<?php

class Fruit{

    public string $name;
    public bool $with_milk; // 牛乳とまぜてよいか
    private int $calorie;

    public function __construct(string $name, bool $with_milk, int $calorie){

        $this->name = $name;
        $this->with_milk = $with_milk;
        $this->calorie = $calorie;

    }

    function mixWithMilk():bool{

        if($this->with_milk){

            $this->name = $this->name.'みるく';
            $this->calorie = $this->calorie + 100;
            return true;

        }else{

            return false;

        }

    }

    /**
     * 一日の消費カロリーの何分の1かを表示
     * @return float|int
     */
    function getCalorieOfOneDay(){

        return $this->calorie/3000;

    }

}

$apple = new Fruit('りんご', false, 50);
$strawberry = new Fruit('いちご', true, 31);
$melon = new Fruit('メロン', false, 70);
$banana = new Fruit('バナナ', true, 65);

$fruits = [$apple, $strawberry, $melon, $banana];

foreach ($fruits as $key => $fruit){

    print $fruit->name;
    print "<br>";

    $result = $fruit->mixWithMilk();

    if($result){

        print "牛乳とまぜたら". $fruit->name. "になったよ";

    }else{

        print "牛乳とまぜたらダメー!";

    }

    print "<br>";
    print "-------------------------";
    print "<br>";

}

フルーツの配列がクラスになりました。フルーツがいくつかあって、牛乳とまぜると名称が変わり、カロリーもちょっと変わる、というプログラムです。(カロリーについては全くのデタラメです。)

実行すると、結果は下記のようになります。

ここで、このFruitというオブジェクトの
getCalorieOfOneDay()
というメソッドは実行されていないのですが、実行したらどうなるかをシミュレートできます。それがWatchです。

①57行目にブレークポイントを設定しておきます。

メロン食べたい。

②デバッグを実行します。

③デバッガーウィンドウのコンソールの一番右端の+ボタンをクリックします。

New Watchと出ると思うんですが、そこで
$fruit->getCalorieOfOneDay()
と入力します。

すると、ステップ実行により、変化する

$fruit->getCalorieOfOneDay()
の値を見ることができます。

「え?それで何が嬉しいんですか??Watchって何に役立つんですかー?!」

と言いたくなるかもしれません。

しかし、この機能も可能性は無限大。

使うケース①

例えば このプログラムで言えば、一日の総消費量に対するフルーツミックスジュースの割合、つまりgetCalorieOfOneDay()が0.03を超えたときに発生するバグがあったとします。

しかし、このプログラムのように
mixWithMilk()
みたいな関数でプロパティの値が加工されていると、プロパティの値がどこで変わっているのかトンとわからないときがあります。

その場合に使えます。 ずっと見ているのはだるい場合はWatchに式を入れることができますので、

$fruit->getCalorieOfOneDay()>0.03

というWatchを追加します。

すると、TrueとFalseだけを返してくれるので、より見やすいです。

使うケース②

例えば、人の目で見るのがつらい数字とかがありますよね。
弊社では、配送業向けのODIN リアルタイム配送システムというシステムを開発していますが、0時0分0秒を0として、経過秒数を秒で表すという数字をよく扱います。41200秒は、60×60で割ると11.44で、大体11時半ぐらいだなということがわかります。

ただ、11時半ぐらいと言えばイメージがわきやすいのですが、 41200秒 は正直??って感じですよね。

開発している際のこのわかりやすさって開発効率に大事だと思うんですよ。

なので、そのプロパティを3600で割る数字をWatchしておくと、人間が把握しやすいので、ちょっとだけ開発効率が上がります。

Watchには気楽に式を入れられますので、例えばさっきの

getCalorieOfOneDay()

が小数点で見づらいな、と思えば、×100を入れておくこともできます。

下記のようにさっきWatch式をプラスしたところの空白のスペースに

$fruit->getCalorieOfOneDay()*10


と入力すると、その結果をresult というところに表示してくれます。

ただ、「このresult消えるよ…」ということで、すぐに消えてしまうので、実行の際に見てたい場合は、Watch式を入力したほうがいいです。

以上、そのほかにもデバッガってめちゃくちゃいっぱい機能があります。

ぜひPHPStorm本家のサイトなども見てみてください。

https://pleiades.io/help/phpstorm/examining-suspended-program.html#find-execution-point

PHP コメントで配列の中身の型を定義する

大した話じゃ全然ないんですが、いっつも忘れてしまって、コードの中を漁るはめになっているので、メモっておきます。

何のためにこのコメントがあるかというと、PHPでは、配列の中の変数の型を定義することができません。

なので、コメントに書いておくと親切、ということですね。

    /* @var int[] */
    public array $numbers;

上記のコメントの意味は、$numbers という配列には、数字を入れることになっている、という意味です。

ただし、これはただのコメントなので、なんの強制力もありません。

上記の$numbers に Stringを入れてもエラーを吐いたりはしません…。

しかし、あるとないとで読んだプログラマーさんがわかりやすいので大違いだとは思います。

ChatGPTにコードレビューの言葉を考えてもらう

※ コードレビューをしてもらうではありません。

コードレビューの言葉って気を遣いますよね。
相手は人間なので 失礼なことを言ってしまってないか心配です。
そこで今回は、 Chat GPT にコードレビューの言葉を考えてもらいましょう。
しかも、レビュイーの好きなキャラを使えば、そのキャラからレビューしてもらっているので何を言われても幸せな気持ちになるはずです。
こんな win-win な 活用方法はありません。

変換してもらう言葉は以下の4つになります。

  1. このメソッドの名前が内容と合ってないです。
  2. この条件分岐は必要ないと思います。
  3. ここで空チェックを行った方が良いと思います。
  4. このメソッドの役割が多すぎるので分割した方が良いと思います。

※ GPT-4 を利用しています。

カミーユ・ビダン の場合

  1. このメソッドの名前、完全に内容とズレてるだろ!名前を変えないと誤解を生むぞ!
  2. この条件分岐、一体何のために存在してるんだ?邪魔だから削除しろ!
  3. ここ、まさか空チェックをスルーするつもりじゃないだろうな? 不具合を招きたいのか?今すぐ修正しろ!
  4. このメソッドの役割、多過ぎるだろ。まるでジャンクだ。分割しないと後で後悔するぞ!

過激ですね…レビュイーが精神崩壊しそうです…
何回か試してたのですが、たまにベジータみたいになってましたね

シャア・アズナブル の場合

  1. このメソッドの名前、まるで戦場で迷子になったかのようだ。内容と一致させるべきだ!名前が全てを語るのだから。
  2. この条件分岐、本当に必要と思っているのか? これは無駄だ、削除すべきだ。質を追求しろ!
  3. ここでの空チェック、まさか忘れていないだろうな?過信は命取りだ。すぐに対応せよ!
  4. このメソッド、役割が多すぎる。分割することで各々が本来の力を発揮できる。手を打つべきだ!

所々、ぽい名言生み出しています。
「まるで戦場で迷子になったかのようだ」、「分割することで各々が本来の力を発揮できる」とかはかっこいいですね。

ハマーン・カーン の場合

  1. このメソッドの名前、内容と全く合致していない。恥を知れ。名前と内容が一致しないなど、どうしようもない俗物ね。直ちに名前を見直すべきだ。
  2. この条件分岐、必要ないだろうが。恥を知れ、俗物。不必要なものに固執するなど、余計な混乱を生むだけよ。
  3. ここで空チェックを怠るとは、あまりにも無責任だ。恥を知れ、俗物。これ以上問題を生む前に、速やかに対応しなさい 。
  4. このメソッドの役割、明らかに多すぎるわ。無秩序な行為を放置するな、恥を知れ。分割することで整理し、機能を明確にするべきよ。

情緒不安定さを隠せません。
それぞれ最後の言葉がないほうがハマーン様っぽいですね…
どうしようもない俗物って…w

シン・アスカ の場合

  1. 全然内容と合ってない、このメソッドの名前!名前を変えてやる!俺が、ここで!
  2. 絶対に必要ない、この条件分岐!削除してやる!それが俺の意志だ、今日、ここで!
  3. 空チェックを怠ってる、この部分!すぐにでも対処するんだ!今日、ここで!
  4. 役割が多すぎる、このメソッド!分割してやる!今日、ここで!

めちゃくちゃ怒ってますが全部やってくれるのが、シンっぽいですね。
任務とか大体文句言いながらもやってますからね。
レビュワーにとってはマイナスでしかないですが、レビュイーにとっては神様かもしれません。

※ 「もっと強気でお願いします。」「「恥を知れ、俗物。」のような雰囲気でお願いします。など何回かリクエストを出した結果です。

まとめ

使うにしてもキャラは選びましょう。