National Chengchi University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

政治大學 AQI 58 25°C PM2.5 12
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 12

檔案操作

讀寫文字檔、逐行處理、CSV 與 JSON 檔案、目錄操作與錯誤處理
Interaction實作 25 minute(s)

Learning Objectives

  • 用 file_get_contents、file_put_contents(含 FILE_APPEND)與 file() 一次讀寫整個檔案
  • 說明 fopen 的 r、w、a、x 模式差在哪裡,並用 fgets 逐行讀取大檔案
  • 處理檔案不存在的情況:看懂 Warning、先用 file_exists 檢查
  • 用 mkdir、scandir、glob、rename、copy、unlink 管理檔案,用 basename、dirname、pathinfo 拆解路徑
  • 讀寫 CSV 與 JSON 檔,並說明為什麼不能直接用使用者輸入當檔名

讓資料留下來

變數只活在程式執行的那一段時間:網頁回應送出、程式結束,變數就消失了。要讓資料下次還在,就要寫進檔案或資料庫。 小型網站常用檔案記錄這些東西:

  • 設定檔(網站名稱、每頁顯示幾筆),常用 JSON 格式
  • 紀錄檔(log),每發生一件事就在檔案最後面加一行
  • 匯入與匯出的資料,常用 CSV 格式,Excel 也能打開
  • 使用者上傳的圖片與文件

PHP 的檔案函式分成兩類:一次讀寫整個檔案(簡單,適合小檔案),以及打開檔案、一行一行處理(適合大檔案)。

在 CodeLab 裡,程式在一個虛擬的網站根目錄 /var/www/html 中執行,檔名寫相對路徑(todo.txt)就是這個目錄裡的檔案。 範例需要的檔案會先放好;程式寫出來的檔案,可以在播放器的「檔案」分頁看到執行結束時的內容。

一次讀寫整個檔案

todo.txt 已經放好三行待辦。注意第 10、12、14 行三次寫入 done.txt 的差別:有 FILE_APPEND 是加在最後面,沒有就是整個覆蓋。最後一段示範讀取不存在的檔案。

Warning 不會停止程式

讀取不存在的檔案時,PHP 印出 Warning、回傳 false,然後繼續執行下一行:

Warning: file_get_contents(memo.txt): Failed to open stream: No such file or directory in /var/www/html/index.php on line 17

如果沒有檢查回傳值,false 會被當成空字串一路往下用,最後在很遠的地方出錯,很難找到原因。兩個好習慣:

  1. 讀之前先用 file_exists($path) 檢查(或用 is_file,它還會確認不是目錄)
  2. 讀完檢查回傳值:if ($text === false) { … }

正式網站不會把 Warning 顯示給使用者看(display_errors 會關閉,只寫進紀錄檔),所以程式要自己處理失敗的情況。 把錯誤統一交給例外處理的作法,下一課(第 13 課)介紹。

函式 回傳
file_exists($p) 檔案或目錄存在就是 true
is_file($p) 是一般檔案才是 true
is_dir($p) 是目錄才是 true
filesize($p) 檔案大小(位元組)

fopen:打開檔案,一行一行處理

file_get_contents 和 file() 會把整個檔案讀進記憶體。紀錄檔可能有幾 GB,一次讀進來會用光記憶體(PHP 預設每個請求的記憶體上限是 128 MB)。 大檔案要用 fopen 打開,拿到一個檔案代碼(handle),再一次讀一行:

$fp = fopen('access.log', 'r');            // 打開,拿到檔案代碼
while (($line = fgets($fp)) !== false) {   // 讀一行;讀到檔案結尾回傳 false
    // 處理 $line(包含結尾的換行字元)
}
fclose($fp);                               // 用完關閉

同一時間記憶體裡只有一行,再大的檔案都能處理。fopen 的第二個參數是模式:

模式 用途 檔案不存在 檔案已存在 寫入位置
r 只讀 失敗(Warning,回傳 false) 從頭讀 不能寫
w 只寫 建立 清空原本的內容 從頭寫
a 附加(append) 建立 保留內容 一律寫在最後面
x 建立新檔 建立 失敗(Warning,回傳 false) 從頭寫

在模式後面加 +(r+、w+、a+、x+)表示同時可以讀和寫。最常犯的錯誤是想「加一行」卻用了 w,把原本的資料整個清掉。 x 適合「只在檔案不存在時才建立」,例如避免蓋掉別人已經上傳的同名檔案。

函式 作用
fgets($fp) 讀一行(含換行字元),沒有了回傳 false
fwrite($fp, $s) 寫入字串,回傳寫入的位元組數
feof($fp) 是否已經讀到檔案結尾
fclose($fp) 關閉檔案

逐行找出錯誤紀錄

access.log 有 5 行紀錄,程式只挑出含有 ERROR 的行。看變數區的 $line:fgets 讀到的字串結尾有換行,第 6 行用 rtrim 去掉。$fp 顯示成 resource:這是 PHP 表示「打開的檔案」的特殊型別。

想一想:讀寫模式

1.

下面的程式會印出什麼?

<?php
file_put_contents('a.txt', "one\n");
file_put_contents('a.txt', "two\n");
file_put_contents('a.txt', "three\n", FILE_APPEND);
echo implode(',', file('a.txt', FILE_IGNORE_NEW_LINES));
2.

notes.txt 的內容是兩行 ab、cd(每行結尾都有換行)。下面的程式會印出什麼?

<?php
$lines = file('notes.txt');
echo strlen($lines[0]), ' ', count($lines);
3.

你想在 log.txt 的最後面加一行紀錄,而且不能清掉原本的內容。fopen('log.txt', ?) 該用哪個模式?

第 1 次作答

目錄與路徑

函式 作用
mkdir($dir) 建立目錄;已經存在時出現 Warning、回傳 false
mkdir($dir, 0777, true) 第三個參數 true:一次建立多層目錄(backup/2026/09)
scandir($dir) 列出目錄裡的所有項目(含 . 與 ..),依名稱排序
glob('uploads/*.txt') 用萬用字元找檔案,回傳路徑陣列
copy($from, $to) 複製檔案
rename($from, $to) 改名或搬移
unlink($file) 刪除檔案(刪目錄用 rmdir,目錄要先清空)

拆解路徑的函式只處理字串,不會檢查檔案是否存在:

函式 '/var/www/html/uploads/report.final.pdf'
basename($p) report.final.pdf
dirname($p) /var/www/html/uploads
pathinfo($p)['extension'] pdf(最後一個點之後)
pathinfo($p)['filename'] report.final

__DIR__ 是「目前這支程式所在的目錄」。程式被別的檔案 include(第 15 課)時,相對路徑是以執行的那支程式為準, 所以實務上常寫 __DIR__ . '/data/config.json',不論從哪裡被呼叫都指向同一個檔案。

管理上傳目錄

這個範例幾乎沒有變數,重點在輸出:第 8、9 行的 scandir、glob 列出目錄當下的內容,可以看出 copy、rename 的效果。程式結束時的檔案內容在播放器的「檔案」分頁(被刪除或改名的舊檔案顯示成空白)。

CSV 檔

CSV(comma-separated values)是最常見的表格交換格式:一行一筆資料,欄位用逗號分隔,第一行通常是欄位名稱。

name,dept,note
Amy,資工,"喜歡 PHP"
小明,企管,"早餐, 午餐"

麻煩在於欄位內容本身也可能有逗號或引號。不要自己用 explode(',', $line) 拆,交給 PHP 的兩個函式處理:

  • fputcsv($fp, $陣列):把一個陣列寫成一行。欄位含有逗號、空白、雙引號或換行時,會用雙引號包起來;內容裡的雙引號寫成兩個("")
  • fgetcsv($fp):讀一行並拆成陣列,會正確處理引號;讀到結尾回傳 false

讀進來的值一律是字串,要計算時再轉成數字。常見的寫法是先讀第一行當欄位名稱,再用 array_combine($header, $row) 把每一行變成關聯陣列。

用 Excel 打開 PHP 寫出的 UTF-8 CSV,中文可能變成亂碼(Excel 預設用系統的編碼讀檔)。這時可以在檔案開頭先寫入 "\xEF\xBB\xBF"(UTF-8 的 BOM),讓 Excel 認出編碼。

寫入與讀取 CSV

看第 14 行印出的 CSV:「喜歡 PHP」有空白、「早餐, 午餐」有逗號、他說 "hi" 有雙引號,三者都被加上雙引號。讀回來的時候,fgetcsv 把它們還原成原本的字串。

JSON 檔

JSON 可以存巢狀的資料(陣列裡有陣列),很適合當設定檔,也是網頁 API 最常用的格式。

寫法 作用
json_encode($a) 陣列 → JSON 字串(中文預設寫成 \u4e2d 這種跳脫碼)
json_encode($a, JSON_PRETTY_PRINT \| JSON_UNESCAPED_UNICODE) 加上縮排與換行,中文直接寫出來,方便人閱讀
json_decode($s, true) JSON 字串 → 關聯陣列
json_decode($s) JSON 字串 → 物件,用 $obj->theme 讀取
json_last_error_msg() 上一次 JSON 轉換的錯誤訊息

兩個常數用 | 連起來,表示同時開啟兩個選項。json_decode 遇到格式錯誤的 JSON 回傳 null,不會出現 Warning,要自己檢查。 (PHP 8.3 另外加入 json_validate($s),只檢查字串是不是合法的 JSON。)

存檔的固定模式:讀出 → 解碼成陣列 → 修改 → 編碼 → 寫回。

儲存與讀回設定

比較第 5 行和第 9 行印出的 JSON:前者中文變成跳脫碼、全部擠在一行;加上兩個選項之後有縮排、中文直接顯示。第 11 行的第二個參數 true 讓 json_decode 回傳關聯陣列。

不要直接用使用者輸入當檔名

假設網址 view.php?file=a.txt 會顯示 docs/a.txt 的內容:

// 危險:使用者可以讀到網站上的任何檔案
echo file_get_contents('docs/' . $_GET['file']);

如果有人送出 ?file=../config/db.php,路徑就變成 docs/../config/db.php,.. 代表上一層目錄,結果讀到了資料庫密碼。 這叫做路徑穿越(path traversal)。兩道防線:

$name = basename($_GET['file'] ?? '');            // 只留下最後一段,去掉所有目錄
$allowed = ['a.txt', 'b.txt', 'intro.txt'];        // 白名單:只允許這幾個檔名
if (!in_array($name, $allowed, true)) {
    exit('找不到這個檔案');
}
echo file_get_contents(__DIR__ . '/docs/' . $name);

上傳檔案時也一樣:不要沿用使用者給的檔名,由程式產生新的檔名,並限制副檔名。安全性的完整說明在第 18 課。

想一想:CSV、JSON 與路徑

1.

下面的程式會印出什麼?

<?php
$fp = fopen('t.csv', 'w');
fputcsv($fp, ['Amy', 'a,b', 'c']);
fclose($fp);
echo file_get_contents('t.csv');
2.

下面的程式執行的結果是什麼?

<?php
$data = json_decode('{"name":"Amy"}');
echo $data['name'];
3.

下面的程式會印出什麼?

<?php
echo basename('../../config/db.php');
第 1 次作答

動手做:從 CSV 產生成績報表

scores.csv 已經放好,欄位是 name,mid,final。請讀取每一位學生,算出兩次分數的平均,寫成新的 CSV 檔 report.csv:第一行是 name,avg,之後每人一行。最後把 report.csv 的內容印出來。

你的輸出
預期輸出

重點回顧

  • 小檔案:file_get_contents 讀成字串、file() 讀成一行一個元素的陣列(FILE_IGNORE_NEW_LINES 去掉換行)、file_put_contents 寫入(預設覆蓋,FILE_APPEND 附加)
  • 大檔案:fopen → fgets 逐行讀(結尾回傳 false)/fwrite 寫 → fclose;同一時間記憶體裡只有一行
  • 模式:r 只讀、w 清空後寫、a 附加在最後、x 只建立新檔(已存在就失敗)
  • 檔案不存在時是 Warning + 回傳 false,程式不會停;用 file_exists/is_file 先檢查,或用 === false 檢查回傳值
  • 目錄:mkdir(第三個參數 true 建立多層)、scandir、glob、copy、rename、unlink;路徑:basename、dirname、pathinfo、__DIR__
  • CSV 用 fputcsv/fgetcsv,不要自己用逗號拆;讀進來的值都是字串
  • JSON:json_encode($a, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)、json_decode($s, true) 拿到陣列;格式錯誤回傳 null
  • 不要直接用使用者輸入當檔名:basename + 白名單(第 18 課)
這一課完成了!
Next lesson:例外處理
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。