第4章 4.1 $_GET/$_POST 表单处理
上章回顾:上一章我们做了一个 CSV 导入导出工具,小明终于能把 Excel 里的客户名单导进程序处理了。但有个问题——这些数据都是小明自己手动录入的。现实更常见的情况是:数据从用户那里来。用户填表单、点按钮、搜索框里打字……这些交互才是程序的入口。今天我们就来解决这个问题。
🎯 开场:为什么你需要一个"接水管"的技能?
想象一下这个场景:
你在公司做行政,公司有个老系统——三十年前的 ASP 写的,界面丑得让你想打人。但你没办法,必须用它。每天重复工作:打开网页 → 手动输入员工信息 → 点提交 → 等页面刷新 → 再来一条……
你跟老板说想优化,老板说:"行啊,你不是学计算机的吗?写个脚本自动录!"
你一看那系统,输入框长得像这样:
<form method="GET" action="saveEmployee.php">
姓名:<input type="text" name="username">
部门:<input type="text" \n\n\n\n\n\nname="department">
<button type="submit">保存</button>
</form>
问题是:你的 Python 脚本怎么把数据"填进去"再点"提交"?
或者换个更常见的场景——你要做个留言板,用户在前台填表单,后台 PHP 收数据存数据库。怎么收?收什么格式?用户故意输 <script>alert('hack')</script> 你怎么办?
学完这章,你能:
- 让程序自动填表单、模拟按钮点击(自动化测试/爬虫基础)
- 接收并验证用户表单数据(Web 开发核心)
- 防止用户输入恶意代码(XSS 防护)
🧱 基础:核心概念(生活化讲解)
4.1.1 先认识表单:它是数据的"投递信筒"
想象你寄信:
- GET 方法 = 明信片,信息写在 URL 上,http://xxx.com/search?keyword=苹果。路人能看到你写了啥(URL 公开),但不能写太多(URL 长度有限制)。
- POST 方法 = 封信,信息装在信封里,http://xxx.com/submit 后面看不见你填了啥。适合隐私数据(密码)和大量数据(文件上传)。
在 PHP 里,这两种方式提交的数据,分别存在两个超全局变量里:
<?php
// 看一眼这两个变量长啥样
var_dump($_GET);
var_dump($_POST);
?>
打开浏览器访问 http://localhost/test.php?name=小明,你会看到 $_GET 里有一个 name。
4.1.2 一个完整的表单接收例子
先有前端表单:
<!-- save_user.php 同目录下的 form.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h2>新用户注册</h2>
<form method="POST" action="save_user.php">
用户名:<input type="text" name="username"><br>
邮箱:<input type="email" name="email"><br>
年龄:<input type="number" name="age"><br>
<button type="submit">注册</button>
</form>
</body>
</html>
然后是 PHP 接收端:
<?php
// save_user.php
// 检查是不是 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 接收数据
$username = $_POST['username'];
$email = $_POST['email'];
$age = $_POST['age'];
// 简单验证
if (empty($username)) {
echo "用户名不能为空";
exit;
}
// 模拟保存
echo "✅ 用户 {$username}({$email})注册成功!";
} else {
echo "请通过表单提交";
}
?>
这段代码做了三件事:
1. 判断请求方法是不是 POST(安全第一步)
2. 从 $_POST 里取数据
3. 验证后输出结果
4.1.3 filter_input:数据清洗的"安检机"
为什么要用 filter_input?
用户输入的数据,别信任。永远别信任。
用户可能输入:
- 空字符串(没填)
- 超长字符串(想撑爆你数据库)
- <script>alert('xss')</script>(想黑你网站)
- 0 或 "0"(类型陷阱)
PHP 提供了 filter_input 函数来做"安检":
<?php
// 获取 POST 的 username,同时做消毒处理
$username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
// 获取邮箱,验证格式
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
// 获取整数
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT);
// 如果格式不对,filter_input 会返回 false 或 null
if ($email === false) {
echo "邮箱格式不对";
}
?>
filter_input(INPUT_POST, '字段名', 过滤器) 相当于:
- 从 $_POST['字段名'] 取值
- 用过滤器处理
- 返回处理后的值(失败返回 false/null)
常见的过滤器:
| 过滤器 | 作用 | 失败返回值 |
|--------|------|-----------|
| FILTER_SANITIZE_STRING | 去除标签、转义特殊字符 | - |
| FILTER_VALIDATE_EMAIL | 验证邮箱格式 | false |
| FILTER_VALIDATE_INT | 验证整数 | false |
| FILTER_SANITIZE_URL | 清理 URL | - |
4.1.4 GET 和 POST 的选择
什么情况用哪个?
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 搜索框 | GET | URL 能分享、收藏 |
| 登录表单 | POST | 密码不该出现在 URL 里 |
| 上传文件 | POST + enctype | 需要特殊编码 |
| 删除/修改数据 | POST | 不该能用 URL 直接触发 |
小明的选择困难:
小明要做个"根据 ID 查用户详情"的页面,URL 长这样:
/user.php?id=123
这是 GET,因为查数据不影响服务器状态。
要做"删除用户"的功能,他该用 POST,表单提交到 /user.php?action=delete&id=123。
🔥 实战:3 个递进项目
项目 1:5 分钟 - 简易搜索工具
目标:做一个搜索页面,用户输入关键词,PHP 返回匹配结果(模拟)
<?php
// search.php
$message = "";
$results = [];
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['keyword'])) {
$keyword = filter_input(INPUT_GET, 'keyword', FILTER_SANITIZE_STRING);
if ($keyword && strlen($keyword) >= 2) {
// 模拟数据库
$all_users = [
["id" => 1, "name" => "张三", "city" => "北京"],
["id" => 2, "name" => "李四", "city" => "上海"],
["id" => 3, "name" => "王五", "city" => "广州"],
["id" => 4, "name" => "赵小明", "city" => "深圳"],
];
foreach ($all_users as $user) {
// 用 mb_strpos 支持中文模糊匹配
if (mb_strpos($user['name'], $keyword) !== false) {
$results[] = $user;
}
}
if (empty($results)) {
$message = "没找到包含「{$keyword}」的用户";
}
} else {
$message = "关键词至少2个字符";
}
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h2>🔍 用户搜索</h2>
<form method="GET">
<input type="text" name="keyword" placeholder="输入姓名关键词"
value="<?= isset($_GET['keyword']) ? htmlspecialchars($_GET['keyword']) : '' ?>">
<button type="submit">搜索</button>
</form>
<?php if ($message): ?>
<p><?= $message ?></p>
<?php endif; ?>
<?php if (!empty($results)): ?>
<table border="1" cellpadding="5">
<tr><th>ID</th><th>姓名</th><th>城市</th></tr>
<?php foreach ($results as $user): ?>
<tr>
<td><?= $user['id'] ?></td>
<td><?= htmlspecialchars($user['name']) ?></td>
<td><?= htmlspecialchars($user['city']) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</body>
</html>
预期输出(搜索"明"时):
🔍 用户搜索
[输入框: 明] [搜索]
ID 姓名 城市
4 赵小明 深圳
一句话解释:用 filter_input(INPUT_GET, ...) 获取 URL 参数,在模拟数据库里做中文模糊匹配。
项目 2:15 分钟 - 用户注册表单(带完整验证)
目标:做一个注册页面,验证用户名、邮箱、密码,做 CSRF 防护
<?php
// register.php
session_start();
// 错误提示
$errors = [];
// 生成 CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
// 处理提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证 CSRF
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
$errors[] = "CSRF 验证失败,请刷新页面重试";
} else {
// 获取并过滤数据
$username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
// 验证用户名
if (empty($username)) {
$errors[] = "用户名不能为空";
} elseif (mb_strlen($username) < 3 || mb_strlen($username) > 20) {
$errors[] = "用户名3-20个字符";
}
// 验证邮箱
if ($email === false) {
$errors[] = "邮箱格式不正确";
}
// 验证密码
if (mb_strlen($password) < 6) {
$errors[] = "密码至少6位";
} elseif ($password !== $password_confirm) {
$errors[] = "两次密码不一致";
}
// 全部通过
if (empty($errors)) {
// 实际项目:存数据库、发邮件等
// 这里模拟成功
$success = "✅ 用户 {$username} 注册成功!";
}
}
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h2>📝 新用户注册</h2>
<?php if (!empty($errors)): ?>
<div style="color: red;">
<?php foreach ($errors as $e): ?>
<p>❌ <?= htmlspecialchars($e) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (isset($success)): ?>
<div style="color: green;"><?= $success ?></div>
<?php else: ?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<p>
用户名:<br>
<input type="text" name="username" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
</p>
<p>
邮箱:<br>
<input type="email" name="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
</p>
<p>
密码:<br>
<input type="password" name="password">
</p>
<p>
确认密码:<br>
<input type="password" name="password_confirm">
</p>
<button type="submit">注册</button>
</form>
<?php endif; ?>
</body>
</html>
预期输出(输入错误时):
❌ 用户名3-20个字符
❌ 邮箱格式不正确
❌ 两次密码不一致
一句话解释:用 hash_equals 对比 CSRF token、用 filter_input 验证邮箱格式、用 htmlspecialchars 防止 XSS。
项目 3:15 分钟 - 自动化数据录入工具
目标:用 PHP 写一个工具,读取 CSV 文件,模拟表单提交,自动录入数据到项目 2 的注册表单
<?php
// batch_register.php
/**
* 这个脚本模拟:从 CSV 批量导入用户数据
* 实际场景:行政系统要从 Excel 导员工信息
*
* CSV 格式:username,email,password
*/
session_start();
// 读取 CSV 文件
$csv_file = 'users_to_import.csv';
$results = [];
if (file_exists($csv_file)) {
$handle = fopen($csv_file, 'r');
// 跳过标题行
fgetcsv($handle);
while (($data = fgetcsv($handle)) !== false) {
$username = filter_var($data[0], FILTER_SANITIZE_STRING);
$email = filter_var($data[1], FILTER_SANITIZE_EMAIL);
$password = $data[2] ?? '';
// 简单验证
$valid = true;
$error_msg = '';
if (mb_strlen($username) < 3) {
$valid = false;
$error_msg = '用户名太短';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$valid = false;
$error_msg = '邮箱格式错误';
}
if (mb_strlen($password) < 6) {
$valid = false;
$error_msg = '密码太短';
}
if ($valid) {
// 模拟注册成功
$results[] = [
'username' => $username,
'email' => $email,
'status' => 'success',
'message' => '导入成功'
];
} else {
$results[] = [
'username' => $username,
'email' => $email,
'status' => 'failed',
'message' => $error_msg
];
}
}
fclose($handle);
} else {
// 如果没有 CSV,生成一个示例
echo "<p>未找到 {$csv_file},正在创建示例文件...</p>";
$sample_data = "username,email,password\n";
$sample_data .= "张三,zhangsan@example.com,pass123\n";
$sample_data .= "李四,li@example.com,pass456\n";
$sample_data .= "王五,wang@example.com,x\n"; // 这条会失败
file_put_contents($csv_file, $sample_data);
echo "<p>示例文件已创建,请再次运行此脚本</p>";
exit;
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h2>📋 批量导入结果</h2>
<table border="1" cellpadding="5">
<tr>
<th>状态</th>
<th>用户名</th>
<th>邮箱</th>
<th>结果</th>
</tr>
<?php foreach ($results as $row): ?>
<tr>
<td><?= $row['status'] === 'success' ? '✅' : '❌' ?></td>
<td><?= htmlspecialchars($row['username']) ?></td>
<td><?= htmlspecialchars($row['email']) ?></td>
<td><?= htmlspecialchars($row['message']) ?></td>
</tr>
<?php endforeach; ?>
</table>
<p>共 <?= count($results) ?> 条记录</p>
<p>成功:<?= count(array_filter($results, fn($r) => $r['status'] === 'success')) ?> 条</p>
<p>失败:<?= count(array_filter($results, fn($r) => $r['status'] === 'failed')) ?> 条</p>
</body>
</html>
先创建测试用的 users_to_import.csv:
username,email,password
小明,xiaoming@tech.com,pass123
小红,xiaohong@tech.com,pass456
老王,laowang@tech.com,secret789
预期输出:
📋 批量导入结果
状态 用户名 邮箱 结果
✅ 小明 xiaoming@tech.com 导入成功
✅ 小红 xiaohong@tech.com 导入成功
✅ 老王 laowang@tech.com 导入成功
共 3 条记录
成功:3 条
失败:0 条
一句话解释:读取 CSV 文件,对每行数据做验证,模拟批量注册流程。
💪 进阶:常见坑 + 性能小贴士
坑 1:直接用 $_POST['key'] 不做检查
<?php
// ❌ 错误:没检查 key 是否存在
$name = $_POST['username'];
echo $name; // 如果没这个 key,直接报错
// ✅ 正确:用 isset 或 ??
$name = $_POST['username'] ?? '';
$name = isset($_POST['username']) ? $_POST['username'] : '';
// ✅ 更好:用 filter_input
$name = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
?>
坑 2:不过滤就输出(XSS)
<?php
// ❌ 错误:用户输入直接输出
echo $_POST['comment']; // <script>alert('hack')</script> 会被执行
// ✅ 正确:用 htmlspecialchars
echo htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8');
?>
坑 3:GET 和 POST 混用
<?php
// ❌ 错误:同一个页面同时处理 GET 和 POST
if ($_GET['action'] === 'save') { ... }
if ($_POST['action'] === 'save') { ... }
// ✅ 正确:分开处理或明确判断请求方法
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 处理表单提交
} else {
// 显示表单
}
?>
坑 4:迷信 FILTER_SANITIZE_STRING
<?php
// ❌ 误解:以为 filter_input 就安全了
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
// 这个过滤器会去掉 <script> 但不会阻止所有 XSS
// ✅ 正确:输出时也要转义
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
?>
坑 5:CSRF token 不验证
<?php
// ❌ 错误:存了 token 但不验证
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// ... 表单里也放了 token
// 但处理时直接跳过验证
// ✅ 正确:必须验证
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
die('CSRF 验证失败');
}
?>
性能小贴士:少用正则,多用内置过滤器
<?php
// 慢
if (preg_match('/^[a-zA-Z0-9]+$/', $username)) { ... }
// 快(验证邮箱)
if (filter_var($email, FILTER_VALIDATE_EMAIL)) { ... }
// 快(验证整数)
if (filter_var($age, FILTER_VALIDATE_INT)) { ... }
?>
调试技巧:打印请求信息
<?php
// 开发时加这段,看清楚请求的全貌
echo "<pre>";
print_r($_SERVER); // 请求方法、URL 等
print_r($_POST); // POST 数据
print_r($_GET); // GET 数据
echo "</pre>";
?>
✏️ 练习题
练习 1(2 分钟):改关键字搜索
- 输入:访问 search.php?keyword=张
- 预期输出:显示张三的信息
- 提示:直接改 URL 参数即可
练习 2(3 分钟):加个判断
- 题目:在项目 1 里,如果没有输入关键词就点搜索,不显示"没找到",改为显示"请输入搜索词"
- 提示:检查 $keyword 是否为空
练习 3(5 分钟):处理新数据
- 题目:把项目 3 的 CSV 换成以下内容运行:
username,email,password
阿强,aq@example.com,123
小美,xm@example.com,abc
- 预期输出:第三条会失败
- 提示:密码 "abc" 只有 3 位
练习 4(8 分钟):串起两个项目
- 题目:修改项目 2 的注册表单,添加"来源"字段,用户选"网站注册"或"后台导入",这个字段也存入结果
- 提示:用 <select> 标签,POST 接收后用 filter_input 获取
练习 5(5 分钟):读懂报错
- 题目:用户提交表单后页面显示 Notice: Undefined index: password in ...,怎么修?
- 预期输出:不再有 Notice
- 提示:看"坑 1"的代码
作业:做一个「联系我们」表单处理系统
需求描述:
公司网站需要一个"联系我们"页面,用户填名字、邮箱、留言,提交后管理员收到邮件通知。
功能点:
1. 显示联系表单(名字、邮箱、留言内容、主题选择)
2. 提交后验证必填项、邮箱格式
3. 验证通过后显示"感谢您的留言,我们会在 24 小时内回复"
4. 有 CSRF 防护
加分项:
1. 用 filter_input 验证所有字段
2. 输入的内容在页面上正确显示(防 XSS)
3. 记录提交历史到文件(JSON 格式)
验收标准:
- 能跑起来
- 不填名字直接提交,显示错误提示
- 邮箱格式错,提示"邮箱格式不对"
- 正常提交后,刷新页面不重复提交(PRG 模式)
📚 总结
本文学了 3 件事:
1. $_GET 和 $_POST 是接收用户数据的两个"管道",GET 在 URL 里、POST 在请求体里
2. filter_input 是数据清洗的"安检机",验证格式、过滤危险字符
3. 永远不信任用户输入——输入要验证、输出要转义、提交要防 CSRF
延伸资源:
- PHP 官方文档:filter_input
- 《PHP 核心技术与最佳实践》:第 5 章"输入过滤与安全"
- OWASP XSS 防护指南
互动钩子:你在做表单的时候遇到过什么奇葩用户输入?比如有人往表单里贴过一整篇论文、或者输入过奇怪的编码……?评论区聊聊,老粉优先回复!
下章预告:表单数据"用完就丢"——页面关了,数据也没了。下一章我们要解决的是:怎么让数据"记住"用户? 浏览器怎么知道你是谁?「Cookie 与 Session」等你来探索。

评论(0)