by shigemk2

当面は技術的なことしか書かない

WebDBPress Vol.58 PHP転ばぬ先の杖 その4

PHP のファイル入出力について

PHPのストリーム機能について
PHP のストリームを一言で説明すると、
ローカルファイル、リモートファイル、圧縮ファイルといった外部データに対する
I/O を透過的に扱うためのしくみ。

unixにおけるgrepやcatの機能を、phpでも利用する事が出来る。

ストリームラッパーとはファイルパスや URI を受 け取ってストリームを返すための枠組み(cat,bzcatなど)

ストリームコンテキストはストリームに対してオプションを指定するための枠組み(wget)

ストリームフィルタとは、ストリームを受け取り、 内容を加工してストリームを返すための枠組み
パイプを利用して内容を加工して標準出力を行う。

ストリーム操作

fopen

gzファイルを解凍して開くプログラム。

<?php
$uri = "compress.zlib://path/to/target.gz"; 
$stream = fopen($uri, "r");
if ($stream) {
  fpassthru($stream);
  fclose($stream); 
}

popen
popen 関数を使えば別プロセス を起動してその出力をストリーム経由で利用できる。
特定のコマンドの標準出力を PHP の読み込み専用ストリームとして開く。

<?php
$stream = popen("/path/to/binary --option", "r")
if ($stream) {
  $result = straem_get_contents($stream);
  fclose($stream);
}

fcloseでストリームを閉じる

ストリームラッパー

ローカルファイルに対しては file:// ストリーム ラッパーが利用できる。

<?php
$uri = "file:///tmp/foo.txt";
$stream = fopen($uri, "w"); 
if ($stream) {
  fwrite($stream, "bar");
  fclose($stream); 
}

相対パスを使いたいときは、 '__FILE__'を使う

gzipやbzip2に対応するストリームラッパーについては、

<?php
$uri="compress.bzip2:///tmp/foo/bar.bz2";
// bz2形式で書き込みオープン
$wstream = fopen($uri, "w");
if ($wstream) {
  fwrite($wstream, "bzip2!\n");
  fclose($wstream);
  // 同じファイルを読み込みオープン
  $rstream = fopen($uri, "r");
  if ($rstream) {
    fpassthru($rstream); // bzip2! fclose($rstream);
  }
}

なお、 php.iniにallow_url_fopen という項目があり、
この値が0の場合、これまで説明してきたHTTPやHTTPS といったネットワーク通信を行う
ストリームラッパーが利用出来なくなるので注意。

ストリームラッパー

ストリームラッパーに渡せるパラメータは URI だけではない。
ストリームコンテキストを使えば、URI に含められないような
補助的な情報をストリームラッパーに受け渡すことができる

HTTP ストリームラッパーで Referer リクエストヘッダを設定するコード

<?php
$url = 'http://example.com/';
$opts = array('http' => array('header' => "Referer: $url")); // ココ
$context = stream_context_create($opts);
$stream = fopen($url, "r", false, $context);
if ($stream) {
  fpassthru($stream);
  fclose($stream);
 }

ストリームフィルタ

ス トリームの内容をストリームのままで書き換える機能がPHPにはあったりする。

ファ イルの内容をBase64デコードして別ファイルに書き 込むコード

<?php
$src = fopen('data-base64.txt', 'r'); if (!$src) {
  die("Could not open src file\n");
}
$dst = fopen('data.txt', 'w'); if (!$dst){
  fclose($src);
  die("Could not open dst file\n");
}
stream_filter_append($src, 'convert.base64-decode');
stream_copy_to_stream($src, $dst);
fclose($src);
fclose($dst);

カスタムフィルタ

stream_ filter_register 関数でフィルタを登録することで
カスタムフィルタを作成する事が出来る

mbstring 関数を利用した文字エンコーディング変換ストリームフィルタ Stream_Filter_ Mbstringの例

<?php
require_once 'Stream/Filter/Mbstring.php';
$ret = stream_filter_register('convert.mbstring.*',
'Stream_Filter_Mbstring');
$stream = fopen('sjis-win.txt', 'r'); 
if (!$stream) {
  die("Could not open file\n"); 
}
//mb_convert_string($str,'UTF-8','SJIS-win')と同等の処理 
stream_filter_append($stream,
  'convert.mbstring.encoding.SJIS-win:UTF-8'); 
fpassthru($stream);
fclose($stream);

トラブル

fgetcsv関数の問題
fgetcsv 関数はストリームを CSV として 1 行分読み進み、
読んだCSV1行に対応する全フィールドを配列として返す関数なのだが、
Shift_JISCSVファイルを正しく扱えない問題が存在する。

データをutf-8にすれば問題なくなる。

そう、こんなふうに。

<?php
$data = file_get_contents("example.csv");
$data = mb_convert_encoding($data, "UTF-8", "SJIS-win"); // 読み書き両方行うモード
$fp = fopen('php://temp, 'w+');
fwrite($fp, $data);
// 先ほど書き込んだデータを読み込むため先頭に移動 
rewind($fp);
setlocale(LC_ALL, 'ja_JP.UTF-8');
while ($values = fgetcsv($fp)) {
  print_r($values);
}
fclose($fp);

テンポラリファイルの重複

tempnamを利用すればよい

<?php
$filename = tempnam('/tmp','inputdata_');
// たとえば「/tmp/inputdata_P407cO」のような値を返す。
// 対応するファイルはサイズ0バイトで作成される。
$fp = fopen($filename, 'w');
// 以下、通常通りの処理

ユーザの入力値をそのままファイル 名の一部に利用してしまう

値のチェック漏れなどにより想定外のファイルにアクセスされてしまう

対策:basenameを使う

<?php
$id = basename($_GET['id']);
$fp = fopen('/data/'.$id.'-profile.txt', 'r'); 
if ($fp) {
  fpassthru($fp); fclose($fp);
}