很多虛擬主機不提供 MySQL,但 PHP 強大的文件操作功能完全能支撐投票、留言本、計數器、簡易 CMS等應用。本文以一個完整的「新版頁面調查」為例,講解文件鎖併發控制、數據結構設計、百分比計算、HTML 表單處理、結果可視化全流程。
data.pol,每行 選項ID 票數<?php
// 配置
$pollFile = __DIR__ . '/data.pol';
$cookieName = 'polled_ce';
$options = [
1 => '信息量更大',
2 => '網頁更精美',
3 => '沒什麼改進',
4 => '其它',
];
$barHeight = 9; // 百分比條高度(px)
$barScale = 1; // 1% = 1px (max 100px)
// 1. 檢查 Cookie 防刷
$voted = isset($_COOKIE[$cookieName]);
if (!$voted && isset($_GET['poll']) && isset($_GET['poll_voteNr'])) {
$choice = (int)$_GET['poll_voteNr'];
if (isset($options[$choice])) {
// 2. 文件鎖寫入(併發安全)
$fp = fopen($pollFile, 'c+'); // 打開/創建,指針在開頭
if (flock($fp, LOCK_EX)) { // 獨佔鎖
$counts = [];
rewind($fp);
while (($line = fgets($fp)) !== false) {
if (preg_match('/(\d+)\s+(\d+)/', $line, $m)) {
$counts[(int)$m[1]] = (int)$m[2];
}
}
$counts[$choice] = ($counts[$choice] ?? 0) + 1;
ftruncate($fp, 0); // 清空文件
rewind($fp);
foreach ($counts as $id => $v) {
fwrite($fp, "$id $v\n");
}
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
// 3. 設置 Cookie
setcookie($cookieName, '1', time() + 86400, '/');
$voted = true; // 立即顯示結果
}
}
// 4. 讀取統計數據(共用邏輯)
function readPoll(string $file): array
{
$counts = array_fill_keys(array_keys($GLOBALS['options']), 0);
if (is_file($file)) {
$fp = fopen($file, 'r');
if (flock($fp, LOCK_SH)) { // 共享鎖讀取
while (($line = fgets($fp)) !== false) {
if (preg_match('/(\d+)\s+(\d+)/', $line, $m)) {
$counts[(int)$m[1]] = (int)$m[2];
}
}
flock($fp, LOCK_UN);
}
fclose($fp);
}
return $counts;
}
$counts = readPoll($pollFile);
$total = array_sum($counts);
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>新版頁面調查</title>
<style>
body{font-family:system-ui,sans-serif;max-width:600px;margin:2rem auto}
table{border-collapse:collapse;width:100%} td,th{border:1px solid #009933;padding:.5rem;text-align:center}
.bar{height:9px;background:#009933;display:inline-block;vertical-align:middle}
</style></head><body>
<h2>與舊版頁面相比較您覺得新版頁面:</h2>
<?php if (!$voted): ?>
<form method="get">
<?php foreach ($options as $id => $label): ?>
<label><input type="radio" name="poll_voteNr" value="= $id ?>" = $id===1?'checked':'' ?>> = htmlspecialchars($label) ?></label><br>
<?php endforeach; ?>
<input type="hidden" name="poll" value="vote">
<button type="submit">提交投票</button>
<a href="?submit=1">查看結果</a>
</form>
<?php else: ?>
<table>
<tr><th>選項</th><th>票數</th><th>百分比</th></tr>
<?php foreach ($options as $id => $label):
$v = $counts[$id] ?? 0;
$pct = $total ? round(100 * $v / $total, 2) : 0;
$barW = (int)($pct * $barScale);
?>
<tr>
<td>= htmlspecialchars($label) ?>
<td>= $v ?>
<td><div class="bar" style="width:= $barW ?>px"></div> = $pct ?>%</td>
</tr>
<?php endforeach; ?>
</table>
<p>總投票數: <strong>= $total ?></p>
<p><a href="?">返回首頁</a> | <a href="?" onclick="document.cookie='polled_ce=;max-age=0;path=/';location.reload();">清除 Cookie 重新投票</a></p>
<?php endif; ?>
</body></html>
flock():併發寫入的生命線LOCK_EX 獨佔鎖(寫入前獲取)LOCK_SH 共享鎖(讀取時獲取,允許並發讀)LOCK_UN 釋放鎖c+:文件不存在則創建,存在則打開,指針在開頭,不截斷(關鍵!)flock → 讀取舊數據 → 計算新數據 → ftruncate(0) → rewind → fwrite → fflush → flock(UN)1 12
2 5
3 3
4 1
每行 ID 空格 數量,preg_match('/(\d+)\s+(\d+)/') 解析,無需 JSON/serialize 開銷,人工可讀、可編輯。
setcookie('polled_ce', '1', time() + 86400, '/'); // 24h,全站路徑
局限:用戶可清除 Cookie、換瀏覽器、用無痕模式繼續投票。進階方案:IP 限制 + 登入用戶 ID 綁定 + 驗證碼。
<div class="bar" style="width:= $barW ?>px"></div>
.bar { height: 9px; background: #009933; display: inline-block; },無需圖片、無需 JS,輕量高效。
// 多選:表單 name="poll_voteNr[]",後端遍歷 $_GET['poll_voteNr'] 逐個加鎖寫入
// 加權:options 陣列改為 ['id'=>1, 'label'=>'...', 'weight'=>2],統計時 $counts[$id] += $weight
// 導出 CSV:
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="poll_'.date('Ymd').'.csv"');
echo "選項,票數,百分比\n";
foreach ($options as $id=>$label) {
$v = $counts[$id]??0;
$p = $total?round(100*$v/$total,2):0;
echo "\"$label\",$v,$p%\n";
}
這個 mypolls.php 雖小,却包含了Web 應用核心要素:HTTP 協議(GET/POST)、狀態管理、併發控制、數據持久化、模板渲染、用戶交互。掌握文件存儲模式,你就能在任何支援 PHP 的環境(甚至無數據庫的虛擬主機、嵌入式設備)快速構建互動功能。下一步嘗試:將數據層抽象為 PollStorageInterface,實現 FilePollStorage / MySQLPollStorage / RedisPollStorage 多驅動切換。