ダウンロードすんならfile_get_contentsよりfreadの方が良さげ?

この記事はだいぶ前に書かれたものなので情報が古いかもしれません

もうずーっと前の話になりますけど、PHPで画像のダウンロードボタンを作ったことがありまして、そん時のことを記事に書いたことがあります。

これがそう。phpで画像ダウンロードボタンを作ってみる

header()とreadfile()を使ってボタンを押した時に画像がダウンロードされるっていうものなんですが、この方法を使えば別に画像じゃなくても、CSVとか動画なんかもダウンロードできるようになります。

しかし、最近になって気づいたことが一つ。

PHPでダウンロード処理を実装するとなったらこの方法さえ知ってりゃオールオッケー! ってなれば話は早かったんですけど、どうもそうは問屋が卸さなかったっぽい。



別サーバーにファイルがあるときは?

例えば今、あかつきのお宿に「http://file.norm-nois.com」っていうURLでアクセスできるファイルサーバーがあったとして、ウェブサーバー側にある「http://norm-nois.com/download.php」というURLにアクセスするとダウンロード処理ができるとする。両者は別々のサーバーね。

そこから「sample.mp4」っていう動画を落としたい。

//http://norm-nois.com/download.php
$path = http://file.norm-nois.com/sample.mp4

header('Content-Type: application/octet-stream');
header('Content-Length: '.filesize($fpath));
header('Content-disposition: attachment; filename="sample.mp4"');
readfile($fpath);

最初は、こんな感じでシンプルに実装できんのかなと思ったんですが、これだと、100MBくらいあるsample.mp4が、5KBくらいダウンロードしたところで処理が終了してしまう。調べてみたらreadfile()使ってできるよって言ってる人もいたから、僕のやり方が何か間違っていただけなのかもしれないけど、とにかく僕の場合は上手くいかなかった。

そんなわけで、まあ基本的なやり方は変わらないんですが、今回はreadfile()の変わりにfread()を使って解決してみました。



fread()でちょっとずつダウンロード

この「ちょっとずつ」っていうのがたぶん、今回のミソ。英語で言うとmiso。ドイツ語で言ってもmiso。スワヒリ語やタガログ語で言ってもmiso。

ヒップホップ調で言うとM・I・S・O。西城秀樹っぽく言うとM・I・S・O。アイドルの熱狂的なファンみたいな感じで言うとM・I・S・O。字だと違いが全然分からねー。

ヒップホップ調ってのは、ラップっぽくってことね。今回使うのfread♪ readfileはNGよ♪ 細かく分けて落としきろう♪ M・I・S・O(エムアイシーオー)それがミソ♪

西城秀樹っぽくってのは、あれよ。YMCA的なリズムでMISOって言うの。

アイドルの熱狂的なファンみたいな感じってのは、ほら、あれ。ファンの人って、L・O・V・E(エルオーブイイー)!とか言ってるじゃん? あんな感じ。



じゃ、そういうことで、とりあえずダウンロードしてみましょうか。

$url = http://file.norm-nois.com/sample.mp4
$header = get_headers($url, 1);

header('Content-Type: application/octet-stream');
header('Content-Length: '.$header['Content-Length']);
header('Content-disposition: attachment; filename="sample.mp4"');

$fp = fopen($url, 'rb');
while(!feof($fp)) {
    $buf = fread($fp, 1048576);
    echo $buf;
    ob_flush();
    flush();
}
fclose($fp);

はい、こんな感じになりました。やっていることは、fread()でファイルを1MBずつ読み込んで出力している感じです。

get_headers()っていうのは、ヘッダ情報を取得する関数です。HTTPステータスコードとか、Content-Typeとか取得できます。第二引数を1にすると、配列のキーが数字じゃなくて「Content-Type」とか「Content-Length」とかになります。詳細はこっち

今回の本題であるfreadとはあまり関係ない部分なんですけど、Content-Lengthにセットするためのファイルサイズを取得したかったんで、今回使ってみました。

whileの条件にfeof()っていう関数を使ってますけど、これはファイルのポインタが終端に達したかどうかを判断する関数です。例えば100MBの動画をfread()で1MB読み込んだ場合、まだ残りは99MBありますよね。その場合、ポインタはまだ終端に達してないことになるので、feof()でfalseが返ってきます。100MB全部読み込むとtrueが返ってくるので、そこでループを抜けると、そういう条件式です。

ob_flush()とかflush()っていうのは、出力バッファをフラッシュするとマニュアルには書いてあるんですが、えーとこれはどういうことかって言うと……………………

ま、まあとりあえずこれ使っとけば大丈夫ってことです(^ω^;)

ob_flush

flush



file_get_contentsを使わない理由

もう一つ。

似たようなやり方になりますけど、fread()を使わずにfile_get_contetns()を使ってもファイルのダウンロードをすることはできます。

$url = http://file.norm-nois.com/sample.mp4
$header = get_headers($url, 1);

header('Content-Type: application/octet-stream');
header('Content-Length: '.$header['Content-Length']);
header('Content-disposition: attachment; filename="sample.mp4"');

echo file_get_contents($url);

書き方としてはreadfile()のときとほぼ変わらないですね。

ただ、そんなに重くないファイルだったら本当にfread()とほとんど挙動が変わらないんですけど、100MBとか1GBとかあるファイルをダウンロードしようとすると、結構差が出てくるようです。

というのも、fread()と違って、file_get_contents()の方は、一度ファイルの内容を変数に格納するっていう動きをしているらしいんですね。だから100MBの情報を一度変数に格納して、それからechoしていれば出力するみたいな感じなので、実際僕もやってみたんですけど、重たい動画をダウンロードしようとすると、ダウンロードが始まるまでに結構時間がかかる。freadの方は、ダウンロード自体はすぐに始まる。

そんなわけで、今回はfread()を使ってみました。

必ずしもfread()の方が処理が早いってわけでもないみたいなんですけどね。ファイルの中身を文字列に格納したいだけならば、 file_get_contents() を使うほうが上記の例よりも効率的って、マニュアルにも書いてあったし。






今回はダウンロード目的でやってみましたが、このfread()のやり方を上手く使うと、擬似的に動画のストリーミング再生ができたりするっていう情報もちらほらと見かけたから、機会があればやってみたいですね。

っていうか、実際一度やってはみたんですけど、動画再生用に使っているプレイヤーとの兼ね合いの問題なのか、上手くできなかったんで、その辺はもうちょっと研究してみます。






追記(2015/01/30)

上記の書き方だと、fopen()でファイルを開けなかったときなんかに無限ループに陥る可能性があります。

回避策も含めて、よかったらこっちの記事もどうぞ。

feofはワーナーなエンターテイメント 〜無限ループという名のラビリンス〜

アピールジョブズ 2016年08月17日 12:25:00
面白い内容ですね。
file_get_contentsの遅い理由に納得しました。
少し触る程度なら、簡単に書けるし応用力あるのでいいですけどね
まっち~(管理人) 2016年08月18日 00:29:53
ありがとうございます。
そうですね。どっちを使うかはその時の状況とか好みで良いかなーと思います。
もしかしたら何か関連しているかも?