topic: file-io (read / write / CSV / paths / upload概念) / ch02

ch02 — ファイルを読む

学習目標

  • file_get_contents()ファイル全体 を 1 つの文字列として読める
  • fopen + fgets + fclose の組み合わせで 1 行ずつ 読める
  • __DIR__ . '/tests/data/foo.txt' のように スクリプトからの相対パス を組める
  • 読み込んだテキストの 末尾改行 に気をつけて出力できる

所要時間

スライド = 5 分 / ドリル = 約 20 分

ドリル

# 内容
01-read-all/ tests/data/hello.txt の中身をそのまま出力
02-read-line-by-line/ fgets で 1 行ずつ読み、先頭に 行番号 を付けて出力

採点

php scripts/grade.php topics/14-file-io/ch02-read-file/drill/01-read-all/
php scripts/grade.php topics/14-file-io/ch02-read-file/drill/02-read-line-by-line/

確認のコツ

  • 読み込み元は drill 内固定パス (__DIR__ . '/tests/data/...')。リポジトリにコミットされているデータを読み取る形
  • このチャプターでは 書き込みは一切しない (ch03 から)

解説スライド

教材スライド(Marp)を1枚ずつ表示します。矢印キー(←→)・スワイプ・ナビボタンで移動できます。

  1. <!-- LLM_CONTEXT: Lesson 14 / Chapter 2 目的: テキストファイルを読む 2 系統 (file_get_contents / fopen+fgets+fclose) を手に馴染ませる 扱わない: 書き込み (ch03) / CSV (ch04) / パス分解 (ch05) / アップロード (ch06) 読み上げ時間目安: 5 分 -->

    ファイルを読む

    Lesson 14 / Chapter 2

  2. 2 つの読み方を持っておく

    関数 どんな時
    file_get_contents($path) 小さい / 中身を文字列でまとめて欲しい とき
    fopen + fgets + fclose 大きい / 1 行ずつ処理したい とき
    <?php
    // パターン A: 一発で全部
    $text = file_get_contents(__DIR__ . '/tests/data/hello.txt');
    
    // パターン B: 1 行ずつ
    $fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
    while (($line = fgets($fp)) !== false) {
        echo $line;   // $line には末尾の \n も含まれる
    }
    fclose($fp);
    ▶ 3v4l で実行

    「設定読み込みは A」「ログ走査は B」 くらいの肌感で使い分けると良い。

  3. file_get_contents の挙動

    $path = __DIR__ . '/tests/data/hello.txt';
    $text = file_get_contents($path);
    
    var_dump($text);
    // string(36) "こんにちは
    // ファイル読み込みのテスト
    // "
    ▶ 3v4l で実行
    • 成功すると ファイル全体を文字列で返す
    • 失敗すると false を返す (パス間違い・権限なしなど)
    • そのまま echo すれば中身がそのまま出る (末尾改行も含む)

    「末尾に \n が含まれてる前提」 で出力を組み立てる。echo $text . "\n" のように追加で改行を足すと 空行が増える 罠あり。

  4. fopen / fgets / fclose の作法

    <?php
    $fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
    if ($fp === false) {
        fwrite(STDERR, "open failed\n");
        exit(1);
    }
    
    while (($line = fgets($fp)) !== false) {
        // $line には末尾の改行 (\n / \r\n) が含まれる
        echo rtrim($line) . "\n";
    }
    
    fclose($fp);
    ▶ 3v4l で実行
    ステップ 役割
    fopen($path, 'r') ファイルを 読み込みモード で開く
    fgets($fp) 1 行読む (末尾改行付き)。EOF で false
    fclose($fp) ファイルを 閉じる (OS のリソースを返す)

    fclose を忘れても PHP 終了時に解放されるが、「開いたら閉じる」 を癖にする。

  5. なぜ fgets のループは while (($line = fgets($fp)) !== false) なのか

    // ❌ よくある罠: feof で書くと最後の行を 1 個多く読む
    while (!feof($fp)) {
        $line = fgets($fp);   // EOF 直前で fgets が false を返すが、ループは 1 回余分に回る
        echo $line;
    }
    
    // ✅ 安全: fgets の戻り値を直接判定
    while (($line = fgets($fp)) !== false) {
        echo $line;
    }
    ▶ 3v4l で実行
    • feof直前の読み取りで EOF を踏んだ後 に true になる → 「false が来てから初めて気づく」
    • 戻り値で判定すれば EOF を踏んだ瞬間に抜ける

    → 「fgets の戻り値で判定」を defalut 形として覚える。

  6. パスは __DIR__ から組む

    // ❌ NG: cwd (現在ディレクトリ) に依存する
    $text = file_get_contents('tests/data/hello.txt');
    
    // ✅ OK: スクリプト自身のディレクトリ基準
    $text = file_get_contents(__DIR__ . '/tests/data/hello.txt');
    ▶ 3v4l で実行
    • 採点ランナー (php scripts/grade.php drill/01-read-all/) は リポジトリルートから起動 される
    • cwd 相対パスだと、起動位置がズレた瞬間 false (= パス間違い) で落ちる
    • __DIR__ を基準 にすれば、どこから呼ばれてもファイルを見つけられる

    → L14 では 「パスは必ず __DIR__ から組む」 が鉄則。

  7. このチャプターでできるようになること

    file_get_contents で 1 発でテキストを読める ✅ fopen / fgets / fclose で 1 行ずつ読める ✅ fgets ループは 戻り値判定 (!== false) で書ける ✅ パスは __DIR__ 基準 で組める ✅ 読み込んだ文字列の 末尾改行 を意識して echo できる

    → ドリルへ

1 / 7
スライドを全部一気に読む(縦表示)

<!-- LLM_CONTEXT: Lesson 14 / Chapter 2 目的: テキストファイルを読む 2 系統 (file_get_contents / fopen+fgets+fclose) を手に馴染ませる 扱わない: 書き込み (ch03) / CSV (ch04) / パス分解 (ch05) / アップロード (ch06) 読み上げ時間目安: 5 分 -->

ファイルを読む

Lesson 14 / Chapter 2


2 つの読み方を持っておく

関数 どんな時
file_get_contents($path) 小さい / 中身を文字列でまとめて欲しい とき
fopen + fgets + fclose 大きい / 1 行ずつ処理したい とき
<?php
// パターン A: 一発で全部
$text = file_get_contents(__DIR__ . '/tests/data/hello.txt');

// パターン B: 1 行ずつ
$fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
while (($line = fgets($fp)) !== false) {
    echo $line;   // $line には末尾の \n も含まれる
}
fclose($fp);
▶ 3v4l で実行

「設定読み込みは A」「ログ走査は B」 くらいの肌感で使い分けると良い。


file_get_contents の挙動

$path = __DIR__ . '/tests/data/hello.txt';
$text = file_get_contents($path);

var_dump($text);
// string(36) "こんにちは
// ファイル読み込みのテスト
// "
▶ 3v4l で実行
  • 成功すると ファイル全体を文字列で返す
  • 失敗すると false を返す (パス間違い・権限なしなど)
  • そのまま echo すれば中身がそのまま出る (末尾改行も含む)

「末尾に \n が含まれてる前提」 で出力を組み立てる。echo $text . "\n" のように追加で改行を足すと 空行が増える 罠あり。


fopen / fgets / fclose の作法

<?php
$fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
if ($fp === false) {
    fwrite(STDERR, "open failed\n");
    exit(1);
}

while (($line = fgets($fp)) !== false) {
    // $line には末尾の改行 (\n / \r\n) が含まれる
    echo rtrim($line) . "\n";
}

fclose($fp);
▶ 3v4l で実行
ステップ 役割
fopen($path, 'r') ファイルを 読み込みモード で開く
fgets($fp) 1 行読む (末尾改行付き)。EOF で false
fclose($fp) ファイルを 閉じる (OS のリソースを返す)

fclose を忘れても PHP 終了時に解放されるが、「開いたら閉じる」 を癖にする。


なぜ fgets のループは while (($line = fgets($fp)) !== false) なのか

// ❌ よくある罠: feof で書くと最後の行を 1 個多く読む
while (!feof($fp)) {
    $line = fgets($fp);   // EOF 直前で fgets が false を返すが、ループは 1 回余分に回る
    echo $line;
}

// ✅ 安全: fgets の戻り値を直接判定
while (($line = fgets($fp)) !== false) {
    echo $line;
}
▶ 3v4l で実行
  • feof直前の読み取りで EOF を踏んだ後 に true になる → 「false が来てから初めて気づく」
  • 戻り値で判定すれば EOF を踏んだ瞬間に抜ける

→ 「fgets の戻り値で判定」を defalut 形として覚える。


パスは __DIR__ から組む

// ❌ NG: cwd (現在ディレクトリ) に依存する
$text = file_get_contents('tests/data/hello.txt');

// ✅ OK: スクリプト自身のディレクトリ基準
$text = file_get_contents(__DIR__ . '/tests/data/hello.txt');
▶ 3v4l で実行
  • 採点ランナー (php scripts/grade.php drill/01-read-all/) は リポジトリルートから起動 される
  • cwd 相対パスだと、起動位置がズレた瞬間 false (= パス間違い) で落ちる
  • __DIR__ を基準 にすれば、どこから呼ばれてもファイルを見つけられる

→ L14 では 「パスは必ず __DIR__ から組む」 が鉄則。


このチャプターでできるようになること

file_get_contents で 1 発でテキストを読める ✅ fopen / fgets / fclose で 1 行ずつ読める ✅ fgets ループは 戻り値判定 (!== false) で書ける ✅ パスは __DIR__ 基準 で組める ✅ 読み込んだ文字列の 末尾改行 を意識して echo できる

→ ドリルへ

演習問題の詳細

この章の演習問題の内容を読めます。実際に手元で解くには教材リポジトリを clone してください。

ドリル 01 — ファイル全体を読む

問題

__DIR__ . '/tests/data/hello.txt' の中身を そのまま標準出力に出す PHP を書いてください。

データファイル (tests/data/hello.txt):

こんにちは
ファイル読み込みのテスト

期待される出力:

こんにちは
ファイル読み込みのテスト

(末尾の改行はファイルに含まれている前提。echo で追加の "\n" を足さない)

採点

php scripts/grade.php topics/14-file-io/ch02-read-file/drill/01-read-all/

ヒント

$path = __DIR__ . '/tests/data/hello.txt';
echo file_get_contents($path);
▶ 3v4l で実行
  • file_get_contentsファイル全体を文字列で返す
  • 末尾の改行も含めて返すので、echo だけで OK
  • パスは __DIR__ 基準 で組む (cwd 依存にしない)

つまづいたら

  • Warning: file_get_contents(...): Failed to open stream → パスがズレている。__DIR__ . '/tests/data/hello.txt' の綴り / / の数を見直す
  • 末尾に空行が 1 つ余計に出る → echo file_get_contents($path) . "\n"; のように追加で改行を足していないか確認

ドリル 02 — 1 行ずつ読んで行番号を付ける

問題

__DIR__ . '/tests/data/poem.txt'1 行ずつ 読み、各行の先頭に "N: " (N は 1 始まりの行番号) を付けて出力する PHP を書いてください。

データファイル (tests/data/poem.txt):

春はあけぼの
やうやう白くなりゆく山ぎは
すこしあかりて
紫だちたる雲のほそくたなびきたる

期待される出力:

1: 春はあけぼの
2: やうやう白くなりゆく山ぎは
3: すこしあかりて
4: 紫だちたる雲のほそくたなびきたる

採点

php scripts/grade.php topics/14-file-io/ch02-read-file/drill/02-read-line-by-line/

ヒント

$fp = fopen(__DIR__ . '/tests/data/poem.txt', 'r');
$n = 1;
while (($line = fgets($fp)) !== false) {
    echo $n . ': ' . rtrim($line, "\r\n") . "\n";
    $n++;
}
fclose($fp);
▶ 3v4l で実行
  • fopen($path, 'r')読み込みモード で開く
  • fgets($fp)1 行返す (EOF で false)
  • fgets の戻り値には 末尾改行 が含まれるので rtrim($line, "\r\n") で剥がし、自分で "\n" を付け直す
  • 開いたら必ず fclose

つまづいたら

  • 最後に空行が 1 つ余分に出る → fgets の戻り値末尾の改行を剥がさず、さらに "\n" を付け足している
  • 1 行ズレる / 行が 1 個多い → while (!feof($fp)) で書くと末尾で 1 回余分に回ることがある。while (($line = fgets($fp)) !== false) の形で書く

演習問題(2問)

  1. ドリル 01 — ファイル全体を読む

    README.md starter.php answer.php

  2. ドリル 02 — 1 行ずつ読んで行番号を付ける

    README.md starter.php answer.php

サイト内で問題文・雛形・解答例を確認できます。実際に手元で解くには教材リポジトリを clone してください。