質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.52%

【PHP】SplFileObject +URL読み込み

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,813

tajix_japan

score 80

先日、こちらで、ttt.txt に記載したタブ区切りの指定したデータを拾ってくるサンプルソースを作っていただきました。

ttt.txt の中身 「 abc def ghi jkl mno 」

test.php?p=2&e=3 とすると、◇マークと行番号を付与した上で、2行目から3つのデータを拾ってきて、

◇2 def
◇3 ghi
◇4 jkl

と表示します。

<?php

use SplFileObject as File;

// パラメータ受け取り
$p = max(filter_input(INPUT_GET, 'p') - 1, 0); // 0以上を保証
$e = filter_input(INPUT_GET, 'e');

// 実験データ
$filename = ("ttt.txt");

// SplFileObjectを使ったほうがコードがきれいになります
$file = new File($filename, 'rb');
$file->setFlags(File::READ_CSV | File::SKIP_EMPTY | File::READ_AHEAD | File::DROP_NEW_LINE); // CSVモード
$file->setCsvControl("\t"); // セパレータをタブ文字に

// イテレータを配列に変換して切り取り
$records = array_slice(iterator_to_array($file), $p, $e, true);

?>
<!DOCTYPE html>

<meta charset="UTF-8">
<title>Example</title>

<style>
ul {
    list-style: none;
    margin-left: 0;
    padding-left: 1.2em;
    text-indent: -1.2em;
}
li:before {
    content: "◇";
    display: block;
    float: left;
    width: 1.2em;
}
</style>

<ul>
<?php foreach ($records as $i => $record): ?>
  <li><?=($i+1)."\t".implode("\t", array_map('htmlspecialchars', $record))?></li>
<?php endforeach; ?>
</ul>

今回の変更点

$filename = ("ttt.txt");

の部分について

$filename = ("http://example.com/ttt.txt");

とURLで拾ってきたいというのが質問の趣旨です。

【当方でテストしてみたこと】
まずは、php.ini の「Fopen wrappers」が「allow_url_fopen = On」になっていることを確認しました。

その上でttt.txt に記載したタブ区切りの内容( abc def ghi jkl mno ) をURLから拾って全て書き出すPHPを作りました。

<?php
$inputs = new NoRewindIterator(new SplFileObject("http://example.com/ttt.txt"))
foreach($inputs as $line) {
  print $line;
}
?>

上記のPHPは、http://example.com/ttt.txt のタブ区切り内容を全て拾うことができます。

上記の2つを合体させれば問題ないと考え、下記のように作成してみました。

<?php

$inputs = new NoRewindIterator(new SplFileObject("http://example.com/ttt.txt"));

use SplFileObject as File;

// パラメータ受け取り
$p = max(filter_input(INPUT_GET, 'p') - 1, 0); // 0以上を保証
$e = filter_input(INPUT_GET, 'e');

// 実験データ
$filename = $inputs;

// SplFileObjectを使ったほうがコードがきれいになります
$file = new File($filename, 'rb');
$file->setFlags(File::READ_CSV | File::SKIP_EMPTY | File::READ_AHEAD | File::DROP_NEW_LINE); // CSVモード
$file->setCsvControl("\t"); // セパレータをタブ文字に

// イテレータを配列に変換して切り取り
$records = array_slice(iterator_to_array($file), $p, $e, true);

?>
<!DOCTYPE html>

<meta charset="UTF-8">
<title>Example</title>

<style>
ul {
    list-style: none;
    margin-left: 0;
    padding-left: 1.2em;
    text-indent: -1.2em;
}
li:before {
    content: "◇";
    display: block;
    float: left;
    width: 1.2em;
}
</style>

<ul>
<?php foreach ($records as $i => $record): ?>
  <li><?=($i+1)."\t".implode("\t", array_map('htmlspecialchars', $record))?></li>
<?php endforeach; ?>
</ul>

結果
下記のようなエラーが出ます。

Fatal error: Uncaught exception 'RuntimeException' with message 'SplFileObject::construct() expects parameter 1 to be a valid path, object given' in C:\xampp\htdocs\test.php:15 Stack trace: #0 C:\xampp\htdocs\test.php(15): SplFileObject->construct(Object(NoRewindIterator), 'rb') #1 {main} thrown in C:\xampp\htdocs\test.php on line 15

上記でエラーとなったいる場所は

$file = new File($filename, 'rb');

の部分です。

どこが間違っているのでしょうか?

よろしくお願いいたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

NoRewindIterator なんてあったんですね,勉強になりました。不可逆なストリームを扱う際には必須ですね。

あと調べていたらLimitIteratorなるものもあるようです!一度全部配列に変換してから切り取るよりも,部分的にイテレータの段階で読み捨てを行ったほうがメモリ効率が圧倒的にいいので是非こちらで。

<?php

use SplFileObject as File;

// パラメータ受け取り
$p = max(filter_input(INPUT_GET, 'p') - 1, 0); // 0以上を保証
$e = filter_input(INPUT_GET, 'e');

// SplFileObjectでURLをオープンする
$file = new File('http://example.com/ttt.txt', 'rb');
$file->setFlags(File::READ_CSV | File::DROP_NEW_LINE); // CSVモード (但し空行読み飛ばしモードは使えない)
$file->setCsvControl("\t"); // セパレータをタブ文字に

// Rewindされないようにラップする
$it = new NoRewindIterator($file);
// 空行を読み飛ばすようにラップする
$it = new CallbackFilterIterator($it, function ($row) { return $row !== [null]; });
// $pオフセットから$e件に制限するようにラップする
$it = new LimitIterator($it, $p, $e);

// 配列に変換
$records = iterator_to_array($it);

?>
<!DOCTYPE html>

<meta charset="UTF-8">
<title>Example</title>

<style>
ul {
    list-style: none;
    margin-left: 0;
    padding-left: 1.2em;
    text-indent: -1.2em;
}
li:before {
    content: "◇";
    display: block;
    float: left;
    width: 1.2em;
}
</style>

<ul>
<?php foreach ($records as $i => $record): ?>
  <li><?=($i+1)."\t".implode("\t", array_map('htmlspecialchars', $record))?></li>
<?php endforeach; ?>
</ul>

蛇足ですが,

  • ob_startを使って出力HTMLのバッファリングを行っている
  • エラーや例外が発生したときには正常系で使うHTMLのバッファを破棄してエラー用のHTMLに切り替える処理を書いている

上記を満たす場合,一時的な配列を生成する必要すらありません。イテレータのままforeachで回してもらって構いません。ただし条件を満たしていないのにやってしまうと,foreachループ中に通信障害が発生したときにHTMLが中途半端な状態でぶっ壊れて出力されてしまうので注意。

一度配列を作る意味としては,「一度作ってしまえば配列は絶対に壊れることが無い」と保証できるからですね。HTTP経由でURLをオープンしているイテレータは不安定なことを意識しなければなりません。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/27 05:35

    いつもありがとうございます!!
    また、丁寧なアドバイス有難うございます。本当に勉強になります。
    早速、頂いたソースでトライ致しました。

    結果
    なぜか表示されませんでした。但しエラーも出ません。

    試しに、
    $file = new File('http://127.0.0.1:8080/ttt1.txt&#039;);
    の部分を
    $file = new File('ttt1.txt', 'rb');
    と直接叩くことも試してみましたが表示されません。
    http://127.0.0.1:8080/ttt1.txt をクリックするとちゃんとデータは表示されます。

    表示ソースを見ると、<ul></ul>となっており、PHPの配列表示部分がまるまる表示されないようです。

    アドバイスいただけましたら幸いです。

    よろしくお願い申し上げます。

    キャンセル

  • 2017/05/27 15:09 編集

    どうやら不可逆なHTTP URLのイテレータに関しては,File::READ_AHEADおよびFile::SKIP_EMPTY,要するに空行を読み飛ばすためのオプションが使えないようです。なので更にCallbackFilterIteratorをかぶせる形で対応しました。(そもそもCSV中に空行が含まれていないという保証があれば要らないのですが)

    fgetcsv系の処理は,空行が見つかると [null] というNULL1個だけ入った配列を返すので,それと比較しています。

    キャンセル

  • 2017/05/27 16:34

    有難うございます!! 出来ました。

    少しだけ弄りました。
    下記の部分だけit2にするだけで見事に動きました。
    弘法にも筆の誤りですね!

    // $pオフセットから$e件に制限するようにラップする
    $it2 = new LimitIterator($it, $p, $e);

    // 配列に変換
    $records = iterator_to_array($it2);

    本当に有難うございました。
    深く御礼申し上げます。


    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.52%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる