表題の通りなのですが、数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;
}
}
}
大量データを扱うと想定外のことが起きますね。
今回たまたま見つからなかっただけのものも全然ありそうです。