PHP
DB
CSV

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

第39回関西PHP勉強会 のスライドです。

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


やりたいこと


困ったこと

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

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

【SJIS(Shift_JIS)】

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

[参考]
PHPの文字コードではSJISじゃなくてSJIS-win、EUC-JPじゃなくてeucJP-winを


【UTF-8】

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

[参考]
Shift-JISとUTF-8の違い(EUC-JPも)


方法

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

CSVデータを読み込む(文字列に変換)

$data = file_get_contents('database/csv/m_postalcode.csv');
  • ファイルの内容を全て文字列に読み込む関数
    • ファイルパスを指定してデータ取得するだけでなく、URLを入れてその情報を取ることもできる

文字コードを変換

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

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

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

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

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

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

<テンポラリファイルの作成>

$temp = tmpfile();
  • テンポラリファイルを作成する
    • 書き込み可のモード(w+)でユニークな名前をもつテンポラリファイルを作成、ファイルハンドルを返す

<メタデータの取得>

$meta = stream_get_meta_data($temp);
  • ヘッダーあるいはメタデータをストリームまたはファイルポインタから取得する
  • ここで取得したメタデータをデータの挿入時に使う

<ファイル書き込み>

fwrite($temp, $data);
  • バイナリセーフなファイル書き込み処理
    • バイナリセーフ:NULL \0、改行 \r\n などを正しく処理する
  • テンポラリファイルに書き込む

<ファイルポインタの位置を戻す>

rewind($temp);
  • ファイルポインタの位置を元に戻す
    • ストリームの先頭に戻す
  • UTF-8に変換されたファイルのできあがり!!

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

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

SplFileObjectクラス

$file = new SplFileObject($meta['uri'], 'rb');
  • ファイルをオブジェクト思考っぽく扱えるクラス
    • 行単位で読み込んで操作するような処理に便利
    • rb:読み込み許可&バイナリモード

  • setFlags で空行処理などの指定が可能
$file->setFlags(
    \SplFileObject::READ_CSV | //CSV列として行読み込み
    \SplFileObject::READ_AHEAD | //先読み/巻き戻し
    \SplFileObject::SKIP_EMPTY | //空行読み飛ばし
    \SplFileObject::DROP_NEW_LINE //行末の改行読み飛ばし
);

$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に入った!!


最終コード

$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でのファイルの扱いについてももうちょい詳しくなっておきたい。