第4章 4.1 $_GET/$_POST 表单处理

上章回顾:上一章我们做了一个 CSV 导入导出工具,小明终于能把 Excel 里的客户名单导进程序处理了。但有个问题——这些数据都是小明自己手动录入的。现实更常见的情况是:数据从用户那里来。用户填表单、点按钮、搜索框里打字……这些交互才是程序的入口。今天我们就来解决这个问题。


🎯 开场:为什么你需要一个"接水管"的技能?

想象一下这个场景:

你在公司做行政,公司有个老系统——三十年前的 ASP 写的,界面丑得让你想打人。但你没办法,必须用它。每天重复工作:打开网页 → 手动输入员工信息 → 点提交 → 等页面刷新 → 再来一条……

你跟老板说想优化,老板说:"行啊,你不是学计算机的吗?写个脚本自动录!"

你一看那系统,输入框长得像这样:

<form method="GET" action="saveEmployee.php">
姓名:<input type="text" name="username">
部门:<input type="text" \n\n![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/37db9e6be44abff.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/94a2b5c2e206f78.png)\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」等你来探索。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。