PHP Excel导入导出实战:PhpSpreadsheet选型、内存优化与踩坑记录
最近这段时间一直在捣鼓一个内部报表系统的收尾工作核心功能就是大家经常挂在嘴边的“PHP Excel导入导出”。标题写了“亲测成功”真不是标题党——从最开始选型、搭环境到后来导出报表、导入批量数据再到中途被内存溢出和文件格式搞到头大最后把整套流程跑稳定整个过程的坑我基本都趟了一遍。这篇不写套话直接说我在实际项目里怎么选型、怎么落地、怎么排查问题给正在搞类似功能的你一个能直接“抄作业”的参考。先说下这个项目背景一个基于PHP的报表管理平台需要支持后台管理员导出订单明细同时支持导入Excel格式的客户资料和成绩数据。听起来挺常规但真做起来才发现Excel导入导出从来不是“调个库就完事”那么简单。1. 从需求到方案为什么最终选了PhpSpreadsheet1.1 项目里的真实痛点最初接手这个需求时第一反应是“这不就封装两个方法吗”。真正梳理完才发现要处理的东西比想象中多导出的表格不能只是简单把数据堆进去要有表头加粗、列宽自适应、日期格式正确显示导入的Excel来源五花八门有从老系统导出的.csv有手工录入的.xlsx还有带公式和合并单元格的报表数据量不算特别大但单个sheet上万行还是会有的如果用老一套的数组一次性读取内存很容易爆。很多时候项目不是死在功能实现上而是死在“数据一多就崩、格式一乱就错”这种细节上。所以选型阶段我就比较谨慎不能随便拿一个能跑的库就往上怼。1.2 老库与新库的取舍当时摆在面前的主要是PHPExcel和PhpSpreadsheet。PHPExcel是老牌工具网上教程一大堆但问题也很明显已经停止维护很久了对PHP 7以上版本支持一般而且处理大文件时内存占用极高。PhpSpreadsheet是PHPExcel的继承者命名空间换了底层代码重构过对PHP 7.2支持很好还支持从单元格流式读取对内存占用更友好。我最后选了PhpSpreadsheet。原因很简单兼容性更好服务器跑的是PHP 8.0PHPExcel在这种环境下经常报兼容性警告虽然能用但总感觉不踏实社区活跃遇到问题容易搜到答案官方文档也写得比较清楚内存控制优势可以用setReadDataOnly(true)配合按行读取实测对大文件更友好。1.3 环境准备与安装PhpSpreadsheet用Composer安装一条命令就搞定composer require phpoffice/phpspreadsheet这里提醒一个常被忽略的点如果服务器PHP版本比较旧比如还是5.6那Composer会直接拒绝安装。建议至少PHP 7.4以上我用的8.0跑起来很顺。另外需要确保开启了php_zip和php_xml扩展否则读取xlsx会报“Class not found”或者“ZipArchive not supported”之类的错误。安装完成后简单写个加载测试确认能正常生成一个xlsx文件就可以正式进入开发了。2. 导出Excel的实操要点与避坑记录2.1 样式与格式让报表不再“灰头土脸”很多教学demo只教你用setCellValue填数据最后导出的表确实能打开但没有任何格式表头和数据一样平铺领导一眼看过去就不太满意。实际上一个合格的导出功能至少要处理这几个点表头加粗并设置背景色关键列调整列宽避免文字被截断日期类型数据要设置成日期格式不然会显示成序列号。我用PhpSpreadsheet实现了一个简单的工具方法专门导出带样式的表格?php use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Fill; $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); // 设置表头 $headers [ID, 姓名, 金额, 下单时间]; foreach ($headers as $colIndex $header) { $column \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex 1); $sheet-setCellValue($column . 1, $header); } // 表头样式 $sheet-getStyle(A1:D1)-getFont()-setBold(true); $sheet-getStyle(A1:D1)-getFont()-setSize(12); $sheet-getStyle(A1:D1)-getAlignment()-setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet-getStyle(A1:D1)-getFill() -setFillType(Fill::FILL_SOLID) -getStartColor()-setARGB(FF4F81BD); // 列宽自适应 foreach (range(A, D) as $column) { $sheet-getColumnDimension($column)-setAutoSize(true); } // 写入导出逻辑...这里有两个容易踩的坑第一用stringFromColumnIndex把数字索引转成字母列名时索引从1开始不是0第二setAutoSize(true)对某些中文场景下感知不是特别准如果要求严格可以手动给每列宽度设置一个固定值。实际导出时还可以顺手把整个表格加上边框、自动筛选、冻结首行。这些都可以参考官方文档的Style类不复杂但能明显提升表格的“专业感”。2.2 分页写入彻底解决内存溢出最开始的版本处理一万行数据直接一个setCellValue往内存里塞结果跑到五六千行时就报“Allowed memory size exhausted”。后来查了资料发现PhpSpreadsheet默认是一次性将整个工作表对象放进内存的数据写越多内存占用越大。解决办法是开启写入缓冲。PhpSpreadsheet的Xlsx Writer自带setPreCalculateFormulas(false)同时可以用setCellValueExplicit写入减少格式计算开销。还有一个更朴素的做法把数据分批写入每写一批后调用unset()释放内存。但真正有效的方案是改变写入思路——不要一口气把所有数据组装成一个超大的二维数组而是用生成器或分批查询的方式从数据库里取一批写一批?php use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); $row 2; // 第一行是表头 $batchSize 500; $offset 0; while (true) { $data getDataFromDatabase($offset, $batchSize); // 模拟分批查询 if (empty($data)) { break; } foreach ($data as $item) { $sheet-setCellValue(A . $row, $item[id]); $sheet-setCellValue(B . $row, $item[name]); $sheet-setCellValue(C . $row, $item[amount]); $sheet-setCellValue(D . $row, $item[created_at]); $row; } $offset $batchSize; unset($data); } $writer new Xlsx($spreadsheet); $writer-save(php://output);实测用这种分页方案单sheet 2万行数据稳稳生成内存占用始终控制在128MB以内。如果数据量再大还可以考虑用真正的流式写入方式不过一般业务场景到2万行已经够用了。2.3 顺手做一版CSV导出的兜底方案有些时候Excel文件本身不是刚需客户只要能“能打开、能筛选”的数据文件就行。这种场景我建议做个CSV导出的兜底一方面实现成本极低另一方面对超大文件更友好。CSV的本质是纯文本所以生成速度比xlsx快很多内存占用也小。但CSV有两个细节第一要注意编码Excel用UTF-8容易乱码可以加上BOM头\xEF\xBB\xBF第二单元格内容里如果包含逗号、换行、引号一定要用fputcsv而不是手动拼接字符串。?php header(Content-Type: text/csv; charsetUTF-8); header(Content-Disposition: attachment; filenameexport_ . date(YmdHis) . .csv); $output fopen(php://output, w); fwrite($output, \xEF\xBB\xBF); // 增加BOM防止Excel打开乱码 fputcsv($output, [ID, 姓名, 金额, 下单时间]); $batchSize 1000; $offset 0; while (true) { $data getDataFromDatabase($offset, $batchSize); if (empty($data)) { break; } foreach ($data as $row) { fputcsv($output, [$row[id], $row[name], $row[amount], $row[created_at]]); } $offset $batchSize; unset($data); } fclose($output); exit;CSV xlsx双导出方案放在一个小工具类里统一管理调用方只需要传一个文件类型参数前端按钮分别指向两个导出接口即可非常灵活。3. 导入Excel从文件上传到数据入库的全流程3.1 上传前的边界检查导入功能的坑比导出更多。因为导出时数据是自己生产的结构可控而用户传上来的Excel你完全不知道里面会有哪些“惊喜”。所以第一步就得在文件上传时做好校验文件后缀必须是指定的.xlsx, .xls, .csv文件大小限制一般设置在10MB以内防止恶意大文件拖垮服务器文件MIME类型检查不能只看后缀某些浏览器会改后缀上传。我用Laravel框架做后端时通常会写一个FormRequest里面把规则声明好?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class ExcelImportRequest extends FormRequest { public function rules() { return [ file [ required, file, max:10240, // KB mimes:xlsx,xls,csv ] ]; } public function messages() { return [ file.required 请选择文件, file.mimes 仅支持xlsx、xls、csv格式, file.max 文件大小不能超过10MB ]; } }这里有个实际遇到过的坑mimes:xlsx,xls,csv在部分服务器上会因为MIME类型识别不一致而误杀合法文件。比如某些环境下CSV的MIME是text/plain而不是text/csv。建议加上file规则后用getClientOriginalExtension()再做一层后缀判断才更稳。3.2 读取时的格式陷阱文件上传后就要用PhpSpreadsheet读取内容了。最基础的方式是?php use PhpOffice\PhpSpreadsheet\IOFactory; $inputFile $request-file(file)-getRealPath(); $spreadsheet IOFactory::load($inputFile); $sheet $spreadsheet-getActiveSheet(); $rows $sheet-toArray();但这只是demo写法实际用起来问题一堆。第一个问题日期列读出来是数字。比如Excel里写的是2024-01-01用toArray()读出来可能是45292这种序列值。解决办法是主动判断这一列单元格的格式或者读取的时候用getValue后自己格式化。更简单的方式是把日期列的数据通过PhpSpreadsheet的日期转换方法处理use PhpOffice\PhpSpreadsheet\Shared\Date; if (Date::isDateTime($cell)) { $dateValue Date::excelToDateTimeObject($cell-getValue()); $formatted $dateValue-format(Y-m-d H:i:s); }第二个问题空行和空单元格。用户填的Excel里面经常混入“看起来是空但实际有空格”的单元格直接用toArray()解析后会残留大量null值。所以读取每一行时最好做一个trim判断把全空行过滤掉。第三个问题合并单元格。一旦遇到合并单元格toArray()依然会正常返回但非左上角的单元格值为null如果你不处理导入进来的数据就会缺字段。这时候要用getMergedCells()收集合并区域手动把合并区域的值复制到其他单元格。不过我的建议是在导入模板的说明里明确规定“不要合并单元格”这比代码补救省事太多。3.3 数据清洗与批量入库数据读取进来后千万别直接批量插入数据库。真实场景中Excel里的数据质量参差不齐很常见的情况包括手机号格式不对、日期写错、重复数据混在里面。我的习惯是分三步走第一遍纯结构清洗去掉空白字符、统一日期格式、剔除无效行第二遍业务校验比如手机号是不是11位金额是否为正数必填字段是否有值第三遍去重与入库先查一遍数据表判断是否已经存在相同记录存在就跳过或标记错误。批量入库尽量用批量insert而不是一行一行insert。我在项目里用的是自定义的batchInsert方法每次插入500条配合事务包裹整体性能比逐条插入提升了大概8倍。?php public function batchInsert(array $rows): void { if (empty($rows)) { return; } DB::beginTransaction(); try { $chunks array_chunk($rows, 500); foreach ($chunks as $chunk) { DB::table(orders)-insert($chunk); } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } }如果有部分数据校验失败我建议不要中断整个导入流程而是把错误行收集起来导入完成后返回一个错误明细文件或数组方便用户修正后重新上传。这个体验细节会让你的功能明显比那种“一错全错”的实现高一个档次。4. 常见问题排查与优化实测4.1 导出文件打不开的两种典型情况第一种是文件本身生成了但用Excel打开提示“格式与扩展名不符”。这种情况绝大多数是因为输出时混入了多余的内容。比如代码执行时PHP文件开头有空格或BOM或者Controller里前面输出了调试信息再调用writer下载文件头就坏了。排查方式很简单用文本编辑器打开生成的xlsx文件如果前面有一堆乱码或字符就说明输出缓冲区没清理干净。第二种是文件被服务器以文本方式输出而非下载。这通常是响应头Content-Type设置错误。正确设置应该是header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment; filename . $filename . ); header(Cache-Control: max-age0);如果用的框架是Laravel可以用response()-download()或response()-streamDownload()框架会帮你处理响应头省心不少。4.2 导入时遇到日期和公式的处理导入Excel时日期是重灾区前面提到过Excel的日期序列值问题这里再补充一个更隐蔽的坑当单元格格式为文本时拿到的日期其实就是字符串这时候不需要转换直接当成日期的字符串处理即可。但如果在读取时没有区分格式统一走一遍Date::isDateTime判断逻辑反而混乱。我的解决方案是写了一个formatDateValue函数先判断值类型如果值是数字就按Excel序列日期转如果是字符串且能匹配日期正则就直接保留否则返回null。这样能同时兼容文本日期和常规日期。关于公式默认情况下IOFactory::load会把公式一并读进来此时拿到的单元格值实际上是公式字符串比如SUM(A1:A10)。如果业务上需要的是计算结果就必须用setPreCalculateFormulas(true)让库去计算一遍。但需要注意开启后导入性能会下降如果只是导入未计算过的数据可以不开启。4.3 性能优化的几个实测数据我记得当时专门针对同一份2万行xlsx文件做过一组对比测试数据大致如下处理方式内存峰值耗时整表读入toArray约180MB约4.2s分批读取并逐行处理约65MB约2.8s使用setReadDataOnly(true)约42MB约1.9s读数据批量插入数据库约50MB约3.1s这组数据说明一个最核心的优化点读取的时候只拿数据不拿样式、不拿公式计算结果内存和耗时都可以大幅下降。另外导入时如果只需要读取某个sheet的一部分列可以用setReadFilter()自定义过滤器只读取指定列性能还能再提升一点。4.4 记得处理运行超时PHP默认max_execution_time一般是30秒导入大文件时很容易超时。解决方式有几种在CLI模式下不受限制Web模式下建议把导入任务转成队列异步处理。如果单纯只是小型后台系统不方便上队列可以在程序里临时set_time_limit(0)但这只适用于内部小工具生产环境还是建议用队列。我最后给报表系统做了队列导入用户在页面上传后立刻返回“导入中”后台任务执行完再通知结果。这样用户体验舒服很多也不会因为文件大而把页面请求拖死。5. 个人实操心得与后续扩展建议整个项目做完之后最大的感受是Excel导入导出看着简单但真正稳定落地需要处理的边界情况非常多。选型要放在第一位PhpSpreadsheet确实比老库省心但即使选了新库也不意味着内存和性能问题就自动消失关键还得靠分批读取、过滤无用数据、批量入库这些配套手段。我在实际开发中养成的习惯是把导入导出组件拆成两个类一个ExcelExporter一个ExcelImporter。前者只负责把数据变成文件后者负责把文件变成数据中间通过DTO或数组传参绝不混在一起。这样后续如果要从Excel换成CSV或者支持新的文件格式改动面都会小很多。最后再分享一个小技巧导出的模板文件可以提前在Excel里手工做好然后用PhpSpreadsheet读取模板再填充数据而不是每次都从零创建。这样表头、样式、公式都能保持一致代码里也不用写一堆样式代码。我的报表系统里所有导出模板都是这么干的维护起来非常方便。如果你也正在做类似的功能希望这篇实操记录能帮你打个底。真踩到没见过的坑也正常保持耐心顺着文件格式、内存占用、输出缓冲这三大方向排查大多都能找到答案。

相关新闻

小说网站爬虫实战:从目录到TXT的完整Python实现

小说网站爬虫实战:从目录到TXT的完整Python实现

搞爬虫的人应该都见过这类需求:任选一个小说网站,爬取任意一部小说,以记事本的形式保存。看起来就一句话,实际上踩坑点非常多。我前阵子接了一个类似的项目,目标是抓取一部网络签约小说并输出为纯文本文件,…

2026/10/13 22:19:57 阅读更多 →
C++共享单车租赁系统实战:面向对象设计与状态机实现

C++共享单车租赁系统实战:面向对象设计与状态机实现

1. 项目缘起与整体设计思路1.1 为什么选择共享单车这个场景来练手共享单车租赁管理这个题目,乍一看像是那种课程设计里被用烂了的“XX管理系统”,但我自己真正动手写了一遍之后才发现,它其实是一个被严重低估的练手项目。原因很简单&#xff…

2026/10/13 22:19:57 阅读更多 →
FastAPI生产级实践:异步架构、性能调优与高并发部署全指南

FastAPI生产级实践:异步架构、性能调优与高并发部署全指南

写在最前面:FastAPI这几年在Python后端圈子里讨论度一直不低,但我真正被它"圈粉"是在一次真实迁移之后。某个给内部数据平台用的查询服务,原先用Flask加多线程模式跑,并发一过200就开始超时报警,每次大促前都…

2026/10/13 22:19:57 阅读更多 →

最新新闻

免费帝国插件内容流水线搭建:采集、伪原创、发布与推送全流程避坑指南

免费帝国插件内容流水线搭建:采集、伪原创、发布与推送全流程避坑指南

简介:这是一款面向站长与SEO从业者的帝国CMS配套插件,聚焦关键词采集、文章采集、伪原创改写、自动发布与搜索引擎推送等环节,帮助希望提升站点收录与排名、又缺乏持续内容产出精力的用户搭建半自动化的优化流程。资源以zip压缩包形式提供&am…

2026/10/13 23:17:20 阅读更多 →
DETR论文笔记与源码解析:端到端目标检测的Transformer实践

DETR论文笔记与源码解析:端到端目标检测的Transformer实践

1. 从标题拆解这个项目到底在讲什么1.1 标题里的三个关键词:论文笔记、源码、DETR看到“论文笔记源码 DETR:End-to-End Object Detection with Transformers”这个标题,我第一反应是:这是一份典型的“论文精读代码复现”组合型笔记。它面向的…

2026/10/13 23:17:20 阅读更多 →
语音FFT变换详解:分帧、加窗、频谱泄漏与Python实现

语音FFT变换详解:分帧、加窗、频谱泄漏与Python实现

拿到一段语音,最先做的往往不是“听”,而是把它丢进傅里叶变换里,看看这个声音到底由哪些频率成分构成。语音信号分析里的FFT变换过程,本质上就是回答一个很基础但又绕不开的问题:这段声音里,低频、中频、高…

2026/10/13 23:17:20 阅读更多 →
LTMU元更新器:让长期目标跟踪不再因更新策略而丢目标

LTMU元更新器:让长期目标跟踪不再因更新策略而丢目标

最近在整理长期目标跟踪相关的资料时,又把这篇文章翻出来重读了一遍。说实话,很多跟踪领域的论文看的是“怎么把特征提得更好”,但LTMU这篇不一样,它瞄准的是一个常被忽略却又致命的问题——跟踪器到底该什么时候更新。这个决定看…

2026/10/13 23:17:20 阅读更多 →
Docker入门与实战——容器实战思考

Docker入门与实战——容器实战思考

容器实战思考1、Docker为什么会成功2、研发人员该如何看待容器2.1、快速上手新技术2.2、容器化的代码仓库2.3、面向业务编程2.4、使用Docker Hub发布开源项目3、容器化开发模式3.1、操作流程3.2、场景示例3.3、注意事项3.4、容器与生产环境1、Docker为什么会成功 Docker实现所…

2026/10/13 23:17:20 阅读更多 →
前馈算法(Feedforward)完全指南:多层感知机与矩阵运算——《Nature of Code》第10章神经网络系列 10.12

前馈算法(Feedforward)完全指南:多层感知机与矩阵运算——《Nature of Code》第10章神经网络系列 10.12

教程文档示例工程 【免费下载链接】website-archive Archive of the Coding Train website (first version) 项目地址: https://gitcode.com/gh_mirrors/we/website-archive 点击查看 免费下载 本文以 Coding Train 网站存档中《Nature of Code》第 10 章 Neural N…

2026/10/13 23:16:20 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →