PHPでSJISのデカイCSVデータを扱った時に困ったこと

[第39回関西PHP勉強会](https://kphpug.doorkeeper.jp/events/73318) のスライドです。

（おことわり）
スライド上では全て文字エンコーディングのことを「文字コード」と表記していますので、ご了承ください。

***

## やりたいこと

- 郵便局が提供している[郵便番号一覧データ](http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html)をDBに入れる

***

## 困ったこと

- **文字コードが「SJIS」**
    - 文字コードの変換が必要
- **データ件数が約12万件と大きい**
    - ファイル操作に工夫が必要

***

1つめの困りごと：**文字コードが「SJIS」**


【SJIS（Shift_JIS）】

- 日本語を含む文字列を表現するために用いられる文字コードの一つ
    - SJISとSJIS-winがあり、SJIS-winの方が対応文字数が多い（①②、はしご高など）
- ガラケーなどの用いられている
    - **扱いにくい...**

[参考]
[PHPの文字コードではSJISじゃなくてSJIS-win、EUC-JPじゃなくてeucJP-winを](https://sachips.byeto.jp/php/php_sjis_sjis-win.html)
***

【UTF-8】

- 表示範囲が広く、どの国の文字も文字化けしない
- 世界標準になってきている
- ほぼ全てのPC環境で読める
    - **これにしたい！！** 

[参考]
[Shift-JISとUTF-8の違い（EUC-JPも）](https://www.homepage-tukuri.com/shosinshanyuumon/entry70.html)

***

## 方法

- CSVデータを読み込む（文字列に変換）
- 文字コードを変換
- テンポラリファイルとして作成

***

CSVデータを読み込む（文字列に変換）

```php
$data = file_get_contents('database/csv/m_postalcode.csv');
```

- ファイルの内容を全て文字列に読み込む関数
    - ファイルパスを指定してデータ取得するだけでなく、URLを入れてその情報を取ることもできる

***

文字コードを変換

```php
$data = mb_convert_encoding($data, 'UTF-8', 'SJIS-win');
```

- 文字コードを変換する関数
    - 「SJIS-win」から「UTF-8」に変換
    - fromの方を指定しない場合は内部文字エンコーディングが使われる
    - **これでUTF-8になる**

***

テンポラリファイルを作成

```php
$temp = tmpfile(); //テンポラリファイルの作成

$meta = stream_get_meta_data($temp); //メタデータの取得

fwrite($temp, $data); //ファイル書き込み

rewind($temp); //ファイルポインタの位置を戻す
```

***

＜テンポラリファイルの作成＞

```php
$temp = tmpfile();
```

- テンポラリファイルを作成する
    - 書き込み可のモード（w+）でユニークな名前をもつテンポラリファイルを作成、ファイルハンドルを返す

***

＜メタデータの取得＞

```php
$meta = stream_get_meta_data($temp);
```

- ヘッダーあるいはメタデータをストリームまたはファイルポインタから取得する
- ここで取得したメタデータをデータの挿入時に使う

***

＜ファイル書き込み＞

```php
fwrite($temp, $data);
```

- バイナリセーフなファイル書き込み処理
    - バイナリセーフ：NULL `\0`、改行 `\r\n` などを正しく処理する
- テンポラリファイルに書き込む

***

＜ファイルポインタの位置を戻す＞

```php
rewind($temp);
```

- ファイルポインタの位置を元に戻す
    - ストリームの先頭に戻す
- **UTF-8に変換されたファイルのできあがり！！**

***

2つめの困りごと：**データ件数が約12万件と大きい**

- `file_get_contents()` だとメモリ不足になる可能性がある
    - 足りない＆文字列で返したい場合は `fgetcsv()` をうまいこと使う
    - **文字列にする必要がない場合はメモリも食わない `SplFileObject` クラスが便利**

***

SplFileObjectクラス

```php
$file = new SplFileObject($meta['uri'], 'rb');
```

- ファイルをオブジェクト思考っぽく扱えるクラス
    - 行単位で読み込んで操作するような処理に便利
    - rb：読み込み許可＆バイナリモード

***

- `setFlags` で空行処理などの指定が可能

```php
$file->setFlags(
    \SplFileObject::READ_CSV | //CSV列として行読み込み
    \SplFileObject::READ_AHEAD | //先読み/巻き戻し
    \SplFileObject::SKIP_EMPTY | //空行読み飛ばし
    \SplFileObject::DROP_NEW_LINE //行末の改行読み飛ばし
);
```

***

```php
$file = new SplFileObject($meta['uri'], 'rb');
$list = [];
foreach ($file as $line) {
    $list[] = [
        "jp_local_govt_code" => $line[0],
                   ......
    ];
    if (count($list)>1000) {
        Postalcode::insert($list);
        $list = [];
    }
}
if (count($list)) {
    Postalcode::insert($list);
}
```

foreachで回して処理するだけ。
**これで大きいデータがDBに入った！！**

***

最終コード

```php
$data = file_get_contents('database/csv/m_postalcode.csv');
$data = mb_convert_encoding($data, 'UTF-8', 'SJIS-win');
$temp = tmpfile();
$meta = stream_get_meta_data($temp);
fwrite($temp, $data);
rewind($temp);

$file = new SplFileObject($meta['uri'], 'rb');
$list = [];
foreach ($file as $line) {
    $list[] = [
        "jp_local_govt_code" => $line[0],
                   ......
    ];
    if (count($list)>1000) {
        Postalcode::insert($list);
        $list = [];
    }
}
if (count($list)) {
    Postalcode::insert($list);
}
```

***

## まとめ

- 文字コードが「SJIS」
    - `file_get_contents()` or `fgetcsv()` でCSV読み込み
    - `mb_convert_encoding()` でSJIS=>UTF-8に変換
    - `tmpfile()` でテンポラリファイル作成
- データ件数が約12万件と大きい
    - `SplFileObject` クラスでforeachを回して処理

文字コードは調べてみるとかなり奥深そうなので、勉強してみる価値がありそう。
CSVファイルをお客さんから渡されることはよくあるし、PHPでのファイルの扱いについてももうちょい詳しくなっておきたい。