
简介一份基于PHP与Excel的通用课表查询系统源码包面向PHP初学者和需要快速搭建课表管理功能的开发者。项目通过PHP后台读取Excel课表数据实现课表浏览、条件查询与结果展示完整覆盖从数据解析到前端交互的开发链路适合用于课程设计或功能模块二次开发。压缩包共19个文件以8个Excel数据表为核心数据源另有5个PHP脚本、3个HTML页面配合JavaScript、CSS和说明文档整体仅78KB。代码结构紧凑便于逐文件阅读和学习已有110人学习浏览。源码包含index.php入口、数据库连接文件、课表查询处理逻辑、Excel操作相关代码等模块。通过阅读可掌握PHP文件I/O、PHPExcel/phpspreadsheet类库调用、MySQL数据库查询、前端表格渲染与搜索筛选以及用户输入过滤、session会话管理、文件上传校验等安全实践。说明文档与Excel示例数据一并附带方便直接运行体验是一个很好的PHP综合练习项目稍加扩展即可演变为多角色课表管理系统。1. 一个PHP课表查询系统为什么值得拆开看很多中小学校、培训机构到现在还是用Excel排课教务把课表排进一个xls文件发到群里就算完事。学生要查课得打开Excel、找sheet、用筛选来回切手机上经常错位。我拿到这份phpexcel通用课表查询系统源码时第一反应是它解决了真正存在的痛点不改动Excel排课习惯在PHP后端直接读取xls内容提供按班级、教师、节次查询的网页入口。包里包含index.php入口、inc目录、code.php验证码、excel.php解析、pubs.php公共函数、conn.php数据库连接以及一份《_KB2X_ShiYongShuoMing.html》使用说明。适合两类人一是想快速给单位搭一个轻量课表查询页面的开发者二是想学习PHP文件操作、Excel解析和安全过滤的初学者。下面我会按“目录结构→数据解析→查询实现→老项目改造”的顺序拆开讲每个环节都给可直接复制的代码。2. 先看懂这个系统的骨架目录、连接与Excel读取方案2.1 解压zip后src里的每个文件负责什么这类老项目的文件命名其实很有规律并不是随便放的。解压后先不要急着双击index.php而是把目录结构列出来对照职责看一遍。下面是我根据压缩包内的文件列表整理出来的典型分工表文件/目录职责依赖关系index.php系统入口接收查询参数、渲染结果页面引入pubs.php、conn.phpcode.php生成验证码图片用于防止机器刷查询依赖 GD 库、Sessionexcel.php读取Excel文件把单元格内容解析成数组依赖 PHPExcel 或 PhpSpreadsheetpubs.php公共函数库字符串过滤、分页函数、时间转换、数组处理被其他页面 includeconn.php数据库连接及公共查询方法引入后作为全局$conninc/页头页脚模板、CSS样式、JS脚本被 index.php includedesc.txt部署说明记录服务器要求、数据库初始化的简要信息无_KB2X_ShiYongShuoMing.html使用说明文档通常包含管理员账号和操作流程无从这份职责划分可以看出系统是典型的“入口-公共函数-业务脚本”三层结构。index.php负责所有对外请求excel.php是数据源解析层pubs.php提供公共能力。实际改造的时候建议先打开desc.txt和_KB2X_ShiYongShuoMing.html里面往往写着数据库名和初始账号如果没有再逐个文件找配置项不要一上来就改业务逻辑。2.2 PHP读Excel的两种路线PHPExcel已是历史PhpSpreadsheet是现在这个zip包的年代感很强里面大概率用的是PHPExcel。PHPExcel在2019年之后就不再维护了官方把项目移交给了PhpSpreadsheet。两者的API高度相似但命名空间和部分方法名不同。如果你现在用PHP 8.1直接跑老代码会报Class PHPExcel not found这是正常的因为老类库根本就没适配现代PHP。对比项PHPExcel老PhpSpreadsheet新包名/命名空间PHPExcelPhpOffice\PhpSpreadsheet安装方式手动 require 或老版本 composercomposer require phpoffice/phpspreadsheetPHP版本要求PHP 5.2 ~ 7.2 较稳定PHP 7.4推荐8.x读取 .xls支持支持但需额外扩展读取 .xlsx支持支持内存占用较高容易超标提供setReadDataOnly、setReadFilter等优化手段社区维护已停止持续更新如果只是想看懂这套源码里excel.php的逻辑我建议先按PHPExcel的思路去读因为老项目里的变量名基本都是$objPHPExcel、$sheet-getCell()这类。理解了读取流程再按第5章的对照表迁移到PhpSpreadsheet成本很低。2.3 conn.php怎么改成PDO连接老项目里conn.php常写成mysql扩展或mysqli过程式比如mysql_connect($host,$user,$pass)。这种写法在PHP 7.0以上直接会被移除所以第一步就是把它改成PDO。不要小看这一步很多查询代码依赖$conn这个对象改成PDO后后续所有SQL调用都需要用预处理语句等于顺带解决了SQL注入问题。?php // conn.php - 改造后的PDO连接 $host 127.0.0.1; $db schedule; // 数据库名需要先用phpmyadmin创建 $user root; $pass 123456; $charset utf8mb4; $dsn mysql:host$host;dbname$db;charset$charset; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, // 抛异常方便调试 PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, // 默认返回关联数组 PDO::ATTR_EMULATE_PREPARES false, // 使用真正的预处理防SQL注入更彻底 ]; try { $conn new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { exit(数据库连接失败 . $e-getMessage()); } ?这里注意ATTR_EMULATE_PREPARES设置成false后MySQL会使用原生预处理LIKE查询的占位符写法略有不同后面第3章会讲到。改成PDO后原来所有mysql_query($sql)的地方都要替换成$conn-query($sql)或$conn-prepare($sql)-execute()。2.4 本地跑起来的第一步在改代码之前先确认环境能跑起来。推荐用PHP 7.4作为老项目的过渡版本因为它的兼容性比PHP 8.x好又能支持PhpSpreadsheet 1.x。如果你用的是PHP 8.x建议直接跳到第5章先做迁移。# 在源码目录下启动PHP内置开发服务器 php -S 127.0.0.1:8080 -t .然后浏览器访问http://127.0.0.1:8080/index.php。如果页面空白先看命令行有没有报Parse error如果提示缺少vendor/autoload.php说明项目依赖没有安装需要先执行composer install。这一步能排除掉90%的环境问题剩下的多半是数据库配置或扩展没开比如GD库、mbstring。3. 课表数据是怎么从Excel走进数据库的3.1 先识别排课Excel的“约定格式”排课Excel看起来五花八门但绝大多数遵循同一个约定第一行是标题第二行是星期几周一、周二…第一列是节次第1节、第2节…交叉单元格里写的是课程信息。真正的数据从第3行、第2列开始。我在解析前会先打印前3行数组确认行列偏移量而不是盲目写死。周一周二周三第1节大学英语李明三教201高等数学王强二教105程序设计赵丽机房A第2节大学英语李明三教201高等数学王强二教105程序设计赵丽机房A第3节体育陈晨操场线性代数孙浩一教301空单元格里的是分隔符依次表示课程名、教师名、教室。如果某个格子里没有说明格式不规范需要做容错处理比如整个单元格当作课程名教师和教室留空。这套系统里的excel.php干的就是这件事。3.2 解析单元格内容课程、教师、教室拆开下面这段代码是用PhpSpreadsheet读取Excel并转换成二维数组的核心逻辑。如果你看到的是老PHPExcel代码只需要把IOFactory的命名空间替换成PHPExcel对应的类名思路完全一样。?php require vendor/autoload.php; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xls; /** * 读取Excel文件返回二维数组 * param string $filePath 上传的xls文件路径 * return array 每行是一个索引数组每个单元格是字符串 */ function readSheetToArray($filePath) { // 老课表多是 .xls 格式用 Xls 读取器如果是 .xlsx 则换成 Xlsx() $reader new Xls(); // 只读数据不读公式和样式内存占用大幅下降 $reader-setReadDataOnly(true); $spreadsheet $reader-load($filePath); $sheet $spreadsheet-getActiveSheet(); // 第四个参数false表示不格式化单元格保留原始字符串 return $sheet-toArray(null, true, false, false); } // 解析单元格内容大学英语李明三教201 拆成数组 function parseCell($cellText) { $parts explode(, $cellText); return [ course trim($parts[0] ?? ), teacher trim($parts[1] ?? ), room trim($parts[2] ?? ), raw $cellText, ]; } ?参数说明toArray的第一个参数是null表示使用默认值第二个参数true表示保留单元格的换行符第三个参数false表示不计算公式第四个参数false表示不做格式化。这样读出来的$rows[0]就是Excel第一行的内容$rows[2][1]就是周三第1节对应的单元格。如果发现单元格值是数字格式比如日期变成了43211说明需要去掉第4个参数为true或者用getFormattedValue()逐个读取这是很多人在Excel日期解析上踩过的坑。3.3 建立schedule表把解析结果批量写库课表数据适合用一张表存储字段越简单越好。我的设计如下CREATE TABLE schedule ( id INT AUTO_INCREMENT PRIMARY KEY, week_day TINYINT NOT NULL COMMENT 1周一, 2周二, ... 7周日, period_no TINYINT NOT NULL COMMENT 第几节从1开始, course_name VARCHAR(100) NOT NULL COMMENT 课程名, teacher_name VARCHAR(50) NOT NULL DEFAULT COMMENT 教师名, room VARCHAR(50) NOT NULL DEFAULT COMMENT 教室/地点, class_name VARCHAR(100) NOT NULL DEFAULT COMMENT 班级/专业名称, UNIQUE KEY uk_week_period_class (week_day, period_no, class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引设计上(week_day, period_no, class_name)联合唯一索引可以防止重复导入时产生脏数据同时也能加快按班级节次查询的速度。解析Excel时外层循环遍历行内层循环遍历列把week_day和period_no计算出来然后拼接插入语句。?php // 把二维数组写入schedule表 function importSchedule($rows, $conn) { $sql INSERT INTO schedule (week_day, period_no, course_name, teacher_name, room, class_name) VALUES (:wd, :pn, :course, :teacher, :room, :class) ON DUPLICATE KEY UPDATE course_name VALUES(course_name), teacher_name VALUES(teacher_name), room VALUES(room); $stmt $conn-prepare($sql); // 第一行是标题第二行是星期从第3行开始是数据 // 假设第0列是节次第1~7列是周一~周日 for ($i 2; $i count($rows); $i) { $period trim($rows[$i][0] ?? ); // 跳过空行和合并单元格导致的空行 if (!preg_match(/^\d$/, $period)) continue; for ($day 1; $day 7; $day) { $cell trim($rows[$i][$day] ?? ); if ($cell || $cell 空) continue; $info parseCell($cell); $stmt-execute([ :wd $day, :pn (int)$period, :course $info[course], :teacher $info[teacher], :room $info[room], :class $info[class] ?? $GLOBALS[default_class], // 如“2024级计科1班” ]); } } return $stmt-rowCount(); } ?这里有几个容易出错的地方$rows[$i][$day]的下标必须和Excel列顺序一致如果前几列有其他内容就要调整映射关系ON DUPLICATE KEY UPDATE很好用可以避免重复导入课表时出现几百条重复记录$GLOBALS[default_class]需要提前在入口脚本里定义因为单张Excel可能只针对一个班级这个值不写在解析函数里更合理。3.4 三种常用查询按班级、按教师、按教室数据落库后查询逻辑就变成纯粹的SQL问题了。老项目里常常直接把用户输入拼进SQL这是很危险的做法。我知道很多人觉得麻烦就不写预处理但课表查询系统会被学生反复访问一旦参数可控就会被SQL注入利用。下面用PDO预处理分别写三种查询?php // 按班级查一周课表最常用 function getTimetableByClass($conn, $className) { $sql SELECT week_day, period_no, course_name, teacher_name, room FROM schedule WHERE class_name :class ORDER BY week_day, period_no; $stmt $conn-prepare($sql); $stmt-execute([:class $className]); return $stmt-fetchAll(); } // 按教师查课模糊匹配姓名 function getTimetableByTeacher($conn, $teacherName) { $sql SELECT week_day, period_no, course_name, class_name, room FROM schedule WHERE teacher_name LIKE :teacher ORDER BY week_day, period_no; $stmt $conn-prepare($sql); // 手动加上通配符注意不要在SQL里写 LIKE %:teacher%那会变成字符串 $stmt-execute([:teacher % . $teacherName . %]); return $stmt-fetchAll(); } // 按教室查适合查自习教室占用情况 function getTimetableByRoom($conn, $room) { $sql SELECT week_day, period_no, course_name, teacher_name, class_name FROM schedule WHERE room :room ORDER BY week_day, period_no; $stmt $conn-prepare($sql); $stmt-execute([:room $room]); return $stmt-fetchAll(); } ?注意LIKE :teacher这种写法占位符绑定的是包含%的完整字符串而不是在SQL里拼接。还有一点class_name的精确匹配适合班级名统一的情况如果用户输入不完整建议也改成LIKE或者在前端加上下拉选择框避免歧义。4. 让查询系统好用前端展示、验证码与输入安全4.1 index.php一个文件搞定表单和列表老项目的index.php往往就是半个单页应用顶部是查询表单下面是结果表格。我不建议拆成多个页面因为课表查询本身逻辑简单一个文件反而方便拷贝到内网服务器。下面是一个可用的模板和原系统风格保持一致?php require conn.php; require pubs.php; $result []; $keyword isset($_GET[keyword]) ? trim($_GET[keyword]) : ; $type isset($_GET[type]) ? $_GET[type] : class; if ($keyword ! ) { // 根据查询类型调用不同的函数 if ($type class) { $result getTimetableByClass($conn, $keyword); } elseif ($type teacher) { $result getTimetableByTeacher($conn, $keyword); } elseif ($type room) { $result getTimetableByRoom($conn, $keyword); } } ? !DOCTYPE html html langzh-CN head meta charsetUTF-8 title通用课表查询系统/title link relstylesheet hrefinc/style.css /head body div classcontainer h2课表查询/h2 form methodget actionindex.php select nametype option valueclass ? $type class ? selected : ?按班级/option option valueteacher ? $type teacher ? selected : ?按教师/option option valueroom ? $type room ? selected : ?按教室/option /select input typetext namekeyword value? htmlspecialchars($keyword, ENT_QUOTES, UTF-8) ? placeholder请输入关键词 required button typesubmit查询/button /form ?php if ($keyword ! ): ? table trth星期/thth节次/thth课程/thth教师/thth教室/thth班级/th/tr ?php foreach ($result as $row): ? tr td周? $row[week_day] ?/td td? $row[period_no] ?/td td? htmlspecialchars($row[course_name] ?? ) ?/td td? htmlspecialchars($row[teacher_name] ?? ) ?/td td? htmlspecialchars($row[room] ?? ) ?/td td? htmlspecialchars($row[class_name] ?? ) ?/td /tr ?php endforeach; ? ?php if (!$result): ? trtd colspan6没有找到匹配记录/td/tr ?php endif; ? /table ?php endif; ? /div /body /html这段代码把查询逻辑和展示放在同一个文件中适合小型系统。注意所有输出到HTML的变量都经过htmlspecialchars处理避免用户构造script标签形成XSS。$result里的键名必须和 SQL 的AS别名保持一致否则表格会显示空值。4.2 code.php验证码防刷公网部署时课表查询接口容易被脚本批量爬取验证码是成本最低的拦截手段。原系统有code.php作用是生成一张带随机数字的图片并把答案写入session。这里给出一个PHP GD库的实现可以直接替换原文件?php session_start(); header(Content-Type: image/png); // 生成4位随机数字 $code ; $pool 23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ; $len strlen($pool) - 1; for ($i 0; $i 4; $i) { $code . $pool[random_int(0, $len)]; } $_SESSION[captcha] strtolower($code); // 图片尺寸宽120 高40 $img imagecreatetruecolor(120, 40); $bg imagecolorallocate($img, 245, 245, 245); $textColor imagecolorallocate($img, 30, 30, 30); $lineColor imagecolorallocate($img, 180, 180, 180); imagefilledrectangle($img, 0, 0, 120, 40, $bg); // 干扰线 for ($i 0; $i 5; $i) { imageline($img, random_int(0, 120), random_int(0, 40), random_int(0, 120), random_int(0, 40), $lineColor); } // 写入字符 $fontSize 18; $x 10; for ($i 0; $i 4; $i) { imagettftext($img, $fontSize, random_int(-20, 20), $x, 28, $textColor, inc/arial.ttf, $code[$i]); $x 25; } imagepng($img); imagedestroy($img); ?使用时要确保服务器安装了GD库和FreeType扩展且inc/arial.ttf字体文件存在否则imagettftext会报错。前端表单里用一个img srccode.php onclickthis.srccode.php?tDate.now()来显示验证码点击可以刷新。后端验证时把用户输入转小写后和$_SESSION[captcha]比较验证通过后再执行查询。4.3 上传新课表Excel时的验证与参数系统允许管理员上传新的课表Excel文件。这个功能是双刃剑如果对文件类型不校验攻击者可能上传一个包含PHP代码的文件到uploads/目录然后通过URL直接访问执行。因此必须做双重校验。校验项推荐参数说明文件类型application/vnd.ms-excel或.xls、.xlsx不能只看扩展名要用finfo_file检测MIME文件大小2 * 1024 * 10242MB课表不可能太大防止内存耗尽存储路径uploads/目录且禁止执行PHP在nginx/Apache里配置该目录不解析PHP文件名随机改名如date(YmdHis)..xls避免用户路径穿越和重名覆盖下面是上传处理的核心代码?php if ($_SERVER[REQUEST_METHOD] POST isset($_FILES[timetable])) { $file $_FILES[timetable]; // 1. 检查错误码 if ($file[error] ! UPLOAD_ERR_OK) { exit(上传失败错误码 . $file[error]); } // 2. 检查大小2MB if ($file[size] 2 * 1024 * 1024) { exit(文件不能超过2MB); } // 3. 检查真实类型 $finfo finfo_open(FILEINFO_MIME_TYPE); $mime finfo_file($finfo, $file[tmp_name]); finfo_close($finfo); $allowedMimes [ application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/octet-stream ]; if (!in_array($mime, $allowedMimes)) { exit(不是合法的Excel文件); } // 4. 随机文件名并移动到uploads目录 $ext $mime application/vnd.openxmlformats-officedocument.spreadsheetml.sheet ? xlsx : xls; $newName date(YmdHis) . _ . random_int(1000, 9999) . . . $ext; if (!move_uploaded_file($file[tmp_name], uploads/ . $newName)) { exit(保存失败请检查uploads目录权限); } // 5. 调用excel.php中的读取函数导入数据库 $rows readSheetToArray(uploads/ . $newName); importSchedule($rows, $conn); echo 导入成功; } ?finfo_file能读取文件头部的真实类型比$_FILES[type]可靠得多。move_uploaded_file之后还要记得给uploads/目录设置严格权限比如在Apache中配置php_admin_flag engine off或者用Nginx的location禁止执行PHP。4.4 老项目最容易踩的PHP兼容性坑老源码到了现代PHP环境会暴露出一堆问题。我总结最常见的四个每个都有对应解法mysql_*函数被移除。这个最致命PHP 7.0起直接删除。解决办法是把所有mysql_query替换成PDO或mysqli别偷懒。Session配置变化。老代码里可能用到session_register()PHP 7.2以后这个函数被移除了直接用$_SESSION[key] value即可。each()函数被移除。老代码如果用了list($key, $value) each($array)在PHP 8.0会直接Fatal error。改成foreach ($array as $key $value)就行。默认时区报错。需要在php.ini里设置date.timezone Asia/Shanghai或者在pubs.php开头加date_default_timezone_set(Asia/Shanghai)。排查顺序建议是先开PHP报错error_reporting(E_ALL); ini_set(display_errors, 1)看到哪个文件抛错就改哪个。不要试图一次性全部改完优先改入口和数据库连接然后跑一次查询再处理Excel解析分步推进比大重写容易得多。5. 老课表系统改造PHPExcel迁移到PhpSpreadsheet的实操5.1 为什么必须迁移如果你已经决定在PHP 8.1上运行这套源码迁移到PhpSpreadsheet是唯一的路。PHPExcel最后一次发布是在2017年之后所有新特性如Excel 2019格式、更严格的类型校验都不会再支持。更重要的是PHPExcel在读取大Excel时内存占用极高一个2MB的课表就可能吃掉256MB内存在云服务器上很容易触发memory_limit报错。PhpSpreadsheet提供流式读取和只读数据模式性能好一个量级。5.2 迁移步骤与命名空间对照迁移不需要重构业务逻辑只需要改两处引入方式和类名。下面是具体的操作步骤# 1. 删除项目里的PHPExcel目录如果有 rm -rf PHPExcel/Classes/PHPExcel # 2. 用composer安装PhpSpreadsheet composer require phpoffice/phpspreadsheet:^1.29然后对照下面的表把excel.php里的旧类名逐个替换PHPExcel旧代码PhpSpreadsheet新代码require_once PHPExcel.phprequire vendor/autoload.phpPHPExcel_IOFactory::load($file)IOFactory::load($file)或IOFactory::createReaderForFile$objPHPExcel-getActiveSheet()$spreadsheet-getActiveSheet()$sheet-getCell(A1)-getValue()$sheet-getCell(A1)-getValue()方法名不变$sheet-getHighestRow()$sheet-getHighestRow()方法名不变PHPExcel_Style_AlignmentPhpOffice\PhpSpreadsheet\Style\Alignment注意toArray()的行为在新老版本里也有差异PHPExcel的toArray默认会格式化数字和日期导致原本是文本的课程名不会变但日期字段会被转成yyyy-m-d。建议统一写成$sheet-toArray(null, true, false, false)四个参数的含义分别是null格式化风格、true保留换行、false不计算公式、false不做格式转换。如果你发现单元格里的 2016-05 被读成了数字基本就是第四个参数没设成false。5.3 内存优化和日期处理两个小技巧迁移完成后针对课表这种瘦长型Excel我一般会加两个优化。第一个是读取过滤器只读取有数据的行跳过空行。PhpSpreadsheet支持setReadFilter但最简单的做法是先用getSheetNames()拿到sheet列表再用循环只读第一个sheet$reader new \PhpOffice\PhpSpreadsheet\Reader\Xls(); $reader-setReadDataOnly(true); $reader-setReadEmptyCells(false); // 跳过空单元格提高解析速度第二个是日期字段的处理。课表里如果包含考试日期、放假日期Excel存储的是序列值如 42401。不要用getValue()改用getFormattedValue()或者getCalculatedValue()取日期。如果你确实需要把整个sheet转数组后再统一处理可以在导入时判断字段名// 在importSchedule的循环里对日期列单独处理 if ($row[日] ?? false) { $date \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row[日]); $row[日] $date-format(Y-m-d); }最后提醒一点composer require装的是带phpoffice/phpspreadsheet的 vendor 目录部署到服务器时一定要把vendor/一起传上去并保证nginx中vendor/目录不能被直接访问否则别人可以下载你的库文件源码路径。本文还有配套的精品资源点击获取