ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP+Excel通用课表查询系统:从零搭建到避坑实战

PHP+Excel通用课表查询系统:从零搭建到避坑实战 简介这是一套基于PHP与Excel文件构建的通用课表查询系统源码面向Web开发初学者、计算机专业毕业设计学生及需要课程管理功能的学校或培训机构。系统以PHP处理请求与数据库交互借助Excel文件实现课程数据的导入导出并配有HTML、CSS、JavaScript搭建的前端界面涵盖后端编程、数据库操作、文件处理与页面设计等完整环节。压缩包共19个文件包含8个xls表格、5个php脚本、2个html页面及css、js、htm等整体约68KB其中xls用于存放课程表与财务报表等示例数据php负责核心逻辑与数据库连接前端文件承担界面展示与交互。资源已积累242人学习下载读者可从中获取入口文件与公共函数模块的目录组织方式、Excel读写与数据库存储的实现思路以及一份可参考的毕业设计项目结构适合作为课程设计、案例教学与PHP实战练习的参考素材。1. 一个 zip 包丢过来phpexcel 通用课表查询系统到底在解决什么教务老师把一份 Excel 课表发到群里学生问「周三下午第三节在哪间教室」老师要翻半天表格换一个班级、换一个学期又得重新发一份文件。这种场景下phpexcel 通用课表查询系统的价值就出来了把 Excel 当数据源用 PHP 做一个网页查询入口学生输入班级、周次、星期几直接返回课程名、教师、教室。它不依赖数据库不需要复杂部署一个 zip 包解压到支持 PHP 的虚拟主机或本地环境就能跑。这套方案适合三类人一是学校信息老师手头只有 Excel 课表不想折腾 MySQL二是 PHP 初学者想找一个「读取本地文件 输出网页」的完整小项目练手三是需要快速交付内部查询工具的后端开发用 Excel 做配置表PHP 做接口层。核心逻辑就三步PHP 读取 Excel 文件、按查询条件过滤行、把结果渲染成 HTML 表格或 JSON。难点不在代码量而在 Excel 格式兼容、中文编码、查询性能边界和部署时的路径问题。下面按「先跑通最小查询再处理格式和性能最后避坑」的顺序拆开讲。2. 把 Excel 当数据库用PHP 读取课表文件的三种落地方式2.1 为什么多数课表查询系统最终都绕不开 PhpSpreadsheetPHP 读取 Excel 的扩展经历过几个阶段。早期用 PHPExcel后来官方停止维护社区迁到 PhpSpreadsheet。现在常见做法是 Composer 安装phpoffice/phpspreadsheet它支持 xls、xlsx、ods、csv能读单元格格式、合并单元格、公式计算结果。对于课表这种「一行代表一条课程记录」的二维表用 PhpSpreadsheet 的toArray()方法一次性转成数组再按列名映射字段是最稳的路径。选型理由很直接课表文件通常由教务系统导出格式不统一有的用合并单元格表示「上午」「下午」有的把星期几写成「周一」「星期一」「Mon」。PhpSpreadsheet 能拿到合并单元格的起始行和跨度也能读取单元格的原始值方便做归一化。如果只用fgetcsv读 CSV遇到 xlsx 就得先让用户另存为 CSV体验差一截。另一个常见做法是用spout库它内存占用低适合几万行的大课表但课表一般几百行PhpSpreadsheet 足够。安装命令如下注意 PHP 版本要求 7.4 以上PHP 8.x 兼容性已经稳定composer require phpoffice/phpspreadsheet如果虚拟主机没有 Composer也可以下载 PhpSpreadsheet 的 release 包手动引入autoload.php。但手动引入容易漏依赖比如zip扩展、xml扩展、gd扩展。常见报错Class ZipArchive not found就是 PHP 没开 zip 扩展需要在php.ini里去掉;extensionzip前面的分号。2.2 最小查询脚本从 Excel 到 JSON 接口的完整代码下面这段代码假设课表文件schedule.xlsx放在同目录第一行是表头班级、星期、节次、课程、教师、教室。查询参数通过 GET 传入class、day、section。?php // query.php require vendor/autoload.php; use PhpOffice\PhpSpreadsheet\IOFactory; header(Content-Type: application/json; charsetutf-8); // 1. 接收查询参数做基础过滤 $class isset($_GET[class]) ? trim($_GET[class]) : ; $day isset($_GET[day]) ? trim($_GET[day]) : ; $section isset($_GET[section]) ? trim($_GET[section]) : ; if ($class ) { echo json_encode([code 400, msg 缺少班级参数], JSON_UNESCAPED_UNICODE); exit; } // 2. 加载 Excel 文件 $file __DIR__ . /schedule.xlsx; if (!file_exists($file)) { echo json_encode([code 404, msg 课表文件不存在], JSON_UNESCAPED_UNICODE); exit; } $spreadsheet IOFactory::load($file); $sheetData $spreadsheet-getActiveSheet()-toArray(null, true, true, true); // 3. 第一行作为表头建立列名到列字母的映射 $header []; foreach ($sheetData[1] as $col $name) { $header[trim($name)] $col; } // 4. 逐行过滤匹配查询条件 $result []; foreach ($sheetData as $rowIndex $row) { if ($rowIndex 1) continue; // 跳过表头 $rowClass isset($row[$header[班级]]) ? trim($row[$header[班级]]) : ; $rowDay isset($row[$header[星期]]) ? trim($row[$header[星期]]) : ; $rowSection isset($row[$header[节次]]) ? trim($row[$header[节次]]) : ; if ($rowClass ! $class) continue; if ($day ! $rowDay ! $day) continue; if ($section ! $rowSection ! $section) continue; $result[] [ 星期 $rowDay, 节次 $rowSection, 课程 isset($row[$header[课程]]) ? trim($row[$header[课程]]) : , 教师 isset($row[$header[教师]]) ? trim($row[$header[教师]]) : , 教室 isset($row[$header[教室]]) ? trim($row[$header[教室]]) : , ]; } // 5. 输出 JSON echo json_encode([ code 0, msg ok, data $result, ], JSON_UNESCAPED_UNICODE);逻辑说明IOFactory::load()会自动识别文件格式xlsx 和 xls 都能处理。toArray(null, true, true, true)四个参数分别表示返回 null 而不是空字符串、计算公式、格式化日期、使用列字母作为键。第四点很关键用列字母做键后表头映射才能对上。过滤时先按班级精确匹配星期和节次作为可选条件这样前端可以只传班级查全部也可以传班级加星期查某一天。参数说明class必填对应 Excel 里的班级列day和section选填值要和 Excel 里保持一致比如 Excel 写「周一」传「星期一」就查不到。如果课表里星期几写法不统一需要在读取后做一次归一化比如把「周一」「星期一」「Mon」都转成「1」。2.3 前端查询页一个 HTML 表单加 fetch 请求就够后端接口有了前端不需要框架。一个index.html放在同目录用fetch调query.php把返回的 JSON 渲染成表格。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title课表查询/title /head body h2课表查询/h2 label班级input idclass typetext placeholder如计科2101/label label星期input idday typetext placeholder如周一/label label节次input idsection typetext placeholder如3-4节/label button onclickquery()查询/button table border1 idresult stylemargin-top:16px; thead trth星期/thth节次/thth课程/thth教师/thth教室/th/tr /thead tbody/tbody /table script async function query() { const cls document.getElementById(class).value.trim(); const day document.getElementById(day).value.trim(); const sec document.getElementById(section).value.trim(); if (!cls) { alert(请输入班级); return; } const url query.php?class${encodeURIComponent(cls)}day${encodeURIComponent(day)}section${encodeURIComponent(sec)}; const res await fetch(url); const json await res.json(); const tbody document.querySelector(#result tbody); tbody.innerHTML ; if (json.code ! 0) { alert(json.msg); return; } json.data.forEach(item { const tr document.createElement(tr); tr.innerHTML td${item.星期}/tdtd${item.节次}/tdtd${item.课程}/tdtd${item.教师}/tdtd${item.教室}/td; tbody.appendChild(tr); }); } /script /body /html这段前端代码没有依赖复制就能用。encodeURIComponent处理中文参数避免 URL 里出现乱码。如果查询结果为空表格保持空行不会报错。常见做法是在tbody里加一行「暂无数据」但这不是必须的。提示如果前端页面和 PHP 接口不在同一个域名下会遇到跨域问题。常见做法是在query.php顶部加header(Access-Control-Allow-Origin: *);但生产环境建议指定具体域名不要长期用星号。3. 课表格式千奇百怪合并单元格、中文编码和日期字段的处理3.1 合并单元格拆解把「上午」跨行还原成每一节的星期教务系统导出的课表经常长这样第一列是「上午」下面合并了三行分别对应第 1、2、3 节。用toArray()读出来合并区域的非首行会是 null。如果不处理查询「周三第 2 节」时星期列可能是空的。处理思路是先读取合并单元格信息建立一个「行号 → 合并值」的映射再在遍历时填充。PhpSpreadsheet 提供getMergeCells()方法返回类似A2:A4的区间字符串。$sheet $spreadsheet-getActiveSheet(); $mergeCells $sheet-getMergeCells(); $fillMap []; // 行号 值 foreach ($mergeCells as $range) { // 解析 A2:A4 这种区间 list($start, $end) explode(:, $range); $startRow (int) preg_replace(/[A-Z]/, , $start); $endRow (int) preg_replace(/[A-Z]/, , $end); $colLetter preg_replace(/[0-9]/, , $start); $value $sheet-getCell($start)-getValue(); for ($r $startRow; $r $endRow; $r) { $fillMap[$r][$colLetter] $value; } }然后在遍历$sheetData时如果某个单元格为空就从$fillMap里取对应值。注意toArray()返回的键是列字母所以填充时也要按列字母匹配。这一步做完合并单元格的课表就能像普通表格一样查询。参数说明getMergeCells()返回的是当前工作表所有合并区域如果课表有多个 sheet需要对每个 sheet 分别处理。常见坑是合并区域跨列比如「上午」合并了 A 列和 B 列解析时只取起始列字母填充时也只填起始列其他列仍然为空。更稳的做法是解析区间时同时记录列范围但课表场景下跨列合并较少按起始列处理通常够用。3.2 中文编码与 BOM为什么查询结果返回乱码Excel 文件本身是二进制格式PhpSpreadsheet 读出来是 UTF-8 字符串一般不会乱码。乱码通常出在两个地方一是 CSV 文件带 BOM 头PHP 读第一列时会把 BOM 当成内容的一部分二是 PHP 文件本身保存成了 GBK 编码而输出声明是 UTF-8。CSV 的 BOM 处理很简单读取后去掉前三个字节$content file_get_contents(schedule.csv); $content str_replace(\xEF\xBB\xBF, , $content);如果是 xlsx不需要这一步。但要注意 PHP 文件保存时用 UTF-8 无 BOM否则header(Content-Type: application/json; charsetutf-8)之前如果有输出会导致 header 失效。常见报错Cannot modify header information - headers already sent就是文件里有 BOM 或空行。另一个编码坑是班级名里的特殊字符比如「计科2101中外合作」括号是全角还是半角前端传参和后端匹配必须一致。常见做法是在查询前统一做一次str_replace把全角括号转半角或者用mb_convert_kana处理日文假名但中文场景下手动替换更可控。3.3 节次字段的归一化把「3-4节」拆成可比较的值课表里的节次写法很多「3-4节」「第3-4节」「3、4节」「3,4」。如果用户输入「3-4节」能查到输入「3」就查不到体验不好。归一化思路是提取数字存成起始节和结束节两个字段。function parseSection($raw) { // 提取所有数字 preg_match_all(/\d/, $raw, $matches); $nums $matches[0]; if (count($nums) 2) { return [(int)$nums[0], (int)$nums[1]]; } if (count($nums) 1) { return [(int)$nums[0], (int)$nums[0]]; } return [0, 0]; }查询时如果用户传了节次也做同样解析然后判断用户节次是否落在课程的节次区间内。这样「3」能匹配「3-4节」「4」也能匹配。参数说明preg_match_all提取所有连续数字3-4会得到[3,4]3、4也是[3,4]第3节得到[3]。如果课表里有「单周3-4节」数字提取仍然有效但单双周逻辑需要额外字段不在基础查询范围内。注意归一化后的节次只用于查询匹配展示时仍然用原始字符串避免用户看到「3-4」不知道是第几节。4. 查询性能与部署几百行课表怎么查都不该卡4.1 每次请求都读 Excel 的代价与缓存方案上面的最小脚本每次查询都IOFactory::load()一次 Excel。几百行的 xlsx 文件加载一次大约 50 到 200 毫秒并发上来后 CPU 会吃紧。常见优化是加一层缓存第一次读取后把数组序列化存到文件或 APCu后续请求直接读缓存。$cacheFile __DIR__ . /cache/schedule.php; $cacheTtl 3600; // 1小时 if (file_exists($cacheFile) (time() - filemtime($cacheFile) $cacheTtl)) { $sheetData include $cacheFile; } else { $spreadsheet IOFactory::load($file); $sheetData $spreadsheet-getActiveSheet()-toArray(null, true, true, true); // 写入缓存用 var_export 生成可 include 的 PHP 文件 $cacheContent ?php\nreturn . var_export($sheetData, true) . ;\n; file_put_contents($cacheFile, $cacheContent, LOCK_EX); }逻辑说明缓存文件用var_export生成比serialize可读性好也避免反序列化漏洞。filemtime判断过期LOCK_EX防止并发写入冲突。参数说明$cacheTtl根据课表更新频率设置一天更新一次就设 86400频繁更新就设 300。如果服务器有 APCu可以用apcu_store和apcu_fetch速度更快但虚拟主机不一定支持。4.2 部署到虚拟主机路径、权限和 PHP 版本检查zip 包解压后目录结构通常是query.php、index.html、vendor/、schedule.xlsx。上传到虚拟主机的public_html或wwwroot后需要确认三件事第一PHP 版本。PhpSpreadsheet 要求 PHP 7.4 以上PHP 8.0 到 8.3 都兼容。在query.php里加phpinfo()可以看版本但生产环境不要留。常见做法是写一个check.php输出PHP_VERSION和已加载扩展确认zip、xml、gd、mbstring都在。第二目录权限。缓存目录cache/需要可写Linux 下chmod 755或775Windows 下给IIS_IUSRS写权限。如果缓存写入失败file_put_contents返回 false查询会退化成每次读 Excel不会报错但变慢。第三文件路径。__DIR__在虚拟主机上通常没问题但如果用了符号链接或子目录部署schedule.xlsx的相对路径可能不对。常见做法是把课表文件路径写成配置项放在config.php里方便修改。// config.php return [ excel_path __DIR__ . /data/schedule.xlsx, cache_path __DIR__ . /cache/schedule.php, cache_ttl 3600, ];然后在query.php里$config require config.php;用$config[excel_path]替代硬编码路径。这样换课表文件不用改代码。4.3 用 SQLite 做中间层什么时候该放弃直接读 Excel如果课表数据超过几千行或者需要支持模糊查询、多条件组合、分页直接读 Excel 会越来越吃力。常见做法是把 Excel 导入 SQLite用 SQL 查询。SQLite 是单文件数据库不需要额外服务PHP 默认支持。导入脚本$db new PDO(sqlite: . __DIR__ . /schedule.db); $db-exec(CREATE TABLE IF NOT EXISTS schedule ( id INTEGER PRIMARY KEY AUTOINCREMENT, class TEXT, day TEXT, section TEXT, course TEXT, teacher TEXT, room TEXT )); $db-exec(DELETE FROM schedule); // 清空旧数据 $stmt $db-prepare(INSERT INTO schedule (class, day, section, course, teacher, room) VALUES (?,?,?,?,?,?)); foreach ($sheetData as $rowIndex $row) { if ($rowIndex 1) continue; $stmt-execute([ trim($row[$header[班级]]), trim($row[$header[星期]]), trim($row[$header[节次]]), trim($row[$header[课程]]), trim($row[$header[教师]]), trim($row[$header[教室]]), ]); }查询时用SELECT * FROM schedule WHERE class ? AND day ?性能比遍历数组好一个量级。参数说明SQLite 文件放在__DIR__下注意不要放在 Web 可访问目录或者用.htaccess禁止访问.db文件。导入脚本可以做成定时任务每天凌晨跑一次保持和 Excel 同步。提示SQLite 的并发写入能力弱但课表查询是读多写少完全够用。如果多个班级同时查询读操作不会互相阻塞。5. 避坑与排查课表查询系统上线后最容易翻车的 5 个点5.1 现象查询返回空数组但 Excel 里明明有数据原因通常是表头映射失败。toArray()返回的第一行如果包含合并单元格或空列$header数组里可能没有「班级」这个键导致$header[班级]报 undefined index后续匹配全部跳过。解决方法是先打印$header看键名确认 Excel 第一行没有多余空格或换行。常见做法是用trim()处理表头并且加一层判断如果!isset($header[班级])直接返回错误提示「课表格式不正确缺少班级列」。5.2 现象中文班级名查询不到英文和数字正常原因是 URL 编码或文件编码不一致。前端用encodeURIComponent编码后PHP 的$_GET会自动解码一般没问题。但如果 PHP 文件保存为 GBK而 Excel 内容是 UTF-8字符串比较会失败。解决方法是统一用 UTF-8PHP 文件无 BOM 保存header声明 UTF-8Excel 另存为 xlsx 而不是 CSV。如果必须用 CSV读取后做mb_convert_encoding($value, UTF-8, GBK)。5.3 现象PhpSpreadsheet 报错「ZipArchive not found」原因是 PHP 没有启用 zip 扩展。xlsx 本质是 zip 压缩包没有 zip 扩展就无法解压。解决方法是在php.ini里找到;extensionzip去掉分号重启 PHP 或 Web 服务。虚拟主机如果没有权限改php.ini可以在控制面板里切换 PHP 版本或者联系空间商开启。另一个类似报错是XMLReader not found需要开xml扩展。5.4 现象缓存文件不更新课表改了查询结果还是旧的原因是缓存过期时间没到或者缓存文件权限不对导致写入失败但旧缓存还在。解决方法是先检查cache/目录权限再确认filemtime返回的时间。调试时可以临时把$cacheTtl设为 0强制每次重新生成。常见做法是在后台加一个「刷新缓存」按钮删除缓存文件或者用touch更新 Excel 文件的修改时间触发缓存失效。5.5 现象并发查询时页面卡死或返回 500原因是每次请求都加载 PhpSpreadsheet内存占用高并发上来后 PHP 进程耗尽。解决方法是启用缓存把 Excel 转成数组或 SQLite避免重复解析。如果虚拟主机限制了memory_limit可以在query.php顶部加ini_set(memory_limit, 256M);但更根本的是减少单次请求的内存开销。另一个原因是缓存文件写入时没有加锁多个请求同时写导致文件损坏用LOCK_EX可以避免。6. 从能查到好用给课表查询加一个「周次」维度和导出功能基础查询跑通后最值得加的功能是周次。大学课表不是每周都一样有的课单周上有的双周上有的只上前 8 周。Excel 里通常有一列「周次」写法可能是「1-8」「1,3,5」「单周」。处理方式和节次类似提取数字区间查询时判断当前周是否在区间内。function parseWeeks($raw) { $raw trim($raw); if ($raw 单周) return [type odd]; if ($raw 双周) return [type even]; preg_match_all(/\d/, $raw, $m); $nums array_map(intval, $m[0]); if (count($nums) 2) return [type range, start $nums[0], end $nums[1]]; if (count($nums) 1) return [type range, start $nums[0], end $nums[0]]; return [type all]; } function matchWeek($parsed, $currentWeek) { switch ($parsed[type]) { case all: return true; case odd: return $currentWeek % 2 1; case even: return $currentWeek % 2 0; case range: return $currentWeek $parsed[start] $currentWeek $parsed[end]; } return false; }查询接口加一个week参数前端加一个周次输入框。这样学生查「第 5 周周三第 3 节」就能精确匹配。参数说明parseWeeks把「1-8」解析成范围「单周」「双周」解析成类型其他情况默认全部周次。matchWeek根据当前周判断是否匹配。注意周次从 1 开始不是 0。另一个实用功能是导出查询结果为 Excel。PhpSpreadsheet 既能读也能写把$result数组写成 xlsx 下载use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); $sheet-fromArray([星期, 节次, 课程, 教师, 教室], null, A1); $sheet-fromArray($result, null, A2); header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment; filenamequery_result.xlsx); $writer new Xlsx($spreadsheet); $writer-save(php://output); exit;这段代码放在export.php里前端加一个「导出」按钮指向它。注意fromArray的第二个参数是 null 表示不设置额外参数第三个参数是起始单元格。导出前要确保没有其他输出否则文件会损坏。我自己的习惯是每次改完课表 Excel先手动访问一次query.php?class测试班级确认返回正常再让教务老师更新文件。缓存时间设短一点比如 300 秒避免改了课表学生查不到。这套系统不复杂但细节多尤其是合并单元格和中文编码翻车一次就要查半天。希望帮到你。本文还有配套的精品资源点击获取
返回列表