基于Java与Spark2x的新闻大数据实时分析可视化系统实战
简介这份资源是面向高校课程设计与大数据入门者的完整项目包主题为基于Java与Spark2x构建新闻网大数据实时分析与可视化系统适合需要完成综合性大数据作业或希望打通实时处理链路的开发者。包内共36个文件以10个jar依赖、7个scala与6个java源码为核心辅以xml配置、js脚本、png效果图及md说明文档压缩包约3.44MB结构紧凑便于快速导入与二次开发。项目覆盖数据采集、清洗、流式处理到前端图表展示的完整流程涉及Kafka、HBase、Spark Streaming等组件的整合思路并附有参考步骤与运行说明可帮助读者理解微服务拆分、集群规划与可视化交互设计等关键环节。目前已有917人学习下载适合作为课程设计参考或大数据实时分析项目的实践起点。1. 从一份能跑通的 Spark2x 新闻分析项目说起新闻网站每天产生的点击流、页面浏览日志、栏目访问记录单机脚本处理起来越来越吃力。我手上这份编号 100012794 的资源是一套基于 Java 和 Spark2x 的新闻网大数据实时分析可视化系统解压后能看到news_spark-master、flume_hbase、weblogs、sparkStu几个目录还有一份参考步骤.txt和README.md。它解决的不是从零搭一个推荐系统这种大命题而是把新闻日志从采集、落库、离线/实时计算到前端图表展示这条链路完整串起来适合做课程设计、大数据实训或者想拿一个能跑通的最小闭环来改造成自己业务场景的工程师。Java 在这里承担后端逻辑和 Spark 作业的编写Spark2x 负责内存计算和流式处理Flume 负责把日志往 HBase 里灌前端用图表把结果铺出来。下面我按资源是什么、怎么用、坑在哪的顺序拆一遍。2. 资源结构与技术栈拆解先看清每个目录在干什么2.1 目录清单与模块职责解压后的顶层结构不算复杂但每个目录对应一个明确的职责先理清楚再动手能省掉后面很多这个文件到底该放哪的纠结。目录/文件职责关键技术news_spark-master项目主工程含 Spark 作业与后端代码Java、Spark2x、Mavenflume_hbaseFlume 采集配置与 HBase Sink 相关类Flume NG、HBaseweblogs新闻网站访问日志样本文本日志sparkStuSpark 学习/测试用子工程Spark SQL、DataFramez_pic可视化截图与前端素材图片资源参考步骤.txt部署与运行步骤说明操作指引pom.xmlMaven 依赖与构建配置Mavenflume_hbase里那几个类值得单独说KfkAsyncHbaseEventSerializer.java和SimpleHbaseEventSerializer.java是 Flume 往 HBase 写数据时的序列化器SimpleRowKeyGenerator.java负责生成 RowKeyflume-ng-hbase-sink.jar是编译好的 Sink 包。这套组合说明数据流向是日志 → Flume → HBase → Spark 读取 → 计算 → 可视化。2.2 为什么选 Java Spark2x 这套组合Java 做大数据后端不是最新潮的选择但在这类课程设计和企业实训项目里它的优势很实在生态成熟Spark 本身是 Scala 写的Java API 覆盖了绝大多数常用算子Maven 依赖管理稳定不会像某些新框架那样三天两头换坐标调试时堆栈信息可读出问题能定位到具体行。Spark2x 的核心卖点是内存计算和统一 API。批处理用SparkSession读 HBase 或 HDFS 上的历史日志流处理用Spark Streaming接实时数据两者共用同一套 DataFrame 抽象。对于新闻分析这种既要看历史趋势、又要看实时热点的场景不用在批和流之间维护两套代码逻辑这是选它的直接理由。提示Spark2x 的版本号在pom.xml里锁定不要随手改成 3.x部分 API 签名和依赖坐标有差异改完可能编译不过。2.3 数据链路从日志到图表的完整路径整条链路可以拆成四段。第一段是采集新闻网站的访问日志落到本地文件或 KafkaFlume 的 Source 读进来经过 Serializer 转成 HBase 能存的格式Sink 写进 HBase 表。第二段是存储HBase 按 RowKey 分布数据SimpleRowKeyGenerator决定了数据怎么散列直接影响后续 Scan 的效率。第三段是计算Spark 作业从 HBase 拉数据用 DataFrame 做聚合、过滤、排序算出栏目热度、时段分布、用户行为路径这些指标。第四段是展示后端把结果以接口形式吐给前端前端用图表库渲染。这四段里第一段和第三段是最容易出问题的地方。Flume 的 Serializer 写错数据进不了 HBaseSpark 读 HBase 时 Scan 范围没控好任务跑几个小时不出结果。后面会具体说。3. 环境搭建与 Flume-HBase 采集链路配置3.1 基础环境准备与依赖检查动手之前先把基础环境对齐。JDK 用 1.8Spark2x 对高版本 JDK 的兼容性在部分发行版上有坑Maven 3.6 以上HBase 和 Hadoop 的版本要和 Spark 编译时用的版本匹配否则运行时会报NoSuchMethodError这类让人头大的错误。# 检查 JDK 版本必须是 1.8 java -version # 检查 Maven mvn -version # 检查 Hadoop 和 HBase 是否在 PATH 中 hadoop version hbase version # 进入项目目录先跑一次编译看依赖能不能拉全 cd news_spark-master mvn clean package -DskipTests编译这步很关键。如果mvn package就报依赖找不到先检查 Maven 的settings.xml里仓库地址是否可用再看pom.xml里 Spark 和 HBase 的依赖 scope 是不是provided——集群上已经有这些包的情况下打成 fat jar 反而会冲突。3.2 Flume 采集配置与 HBase Sink 参数flume_hbase目录下的配置决定了日志怎么进 HBase。一个典型的 Flume Agent 配置长这样# flume-hbase-agent.conf a1.sources r1 a1.sinks k1 a1.channels c1 # Source监听日志目录按行读取 a1.sources.r1.type spooldir a1.sources.r1.spoolDir /data/news/weblogs a1.sources.r1.fileSuffix .done # Channel内存通道容量根据日志量调整 a1.channels.c1.type memory a1.channels.c1.capacity 10000 a1.channels.c1.transactionCapacity 1000 # SinkHBase Sink指定序列化器 a1.sinks.k1.type org.apache.flume.sink.hbase.HBaseSink a1.sinks.k1.table news_log a1.sinks.k1.columnFamily info a1.sinks.k1.serializer com.news.flume.SimpleHbaseEventSerializer a1.sinks.k1.serializer.rowKeyGenerator com.news.flume.SimpleRowKeyGenerator a1.sinks.k1.channel c1capacity和transactionCapacity这两个参数是血泪经验日志量大时capacity给小了Channel 满了 Source 会阻塞采集延迟肉眼可见地涨给太大又吃内存。一般按峰值日志量的 1.5 倍估。serializer指向的类必须和flume_hbase里编译出来的 jar 一致类名写错 Flume 启动时直接抛ClassNotFoundException。3.3 自定义 Serializer 与 RowKey 生成逻辑SimpleHbaseEventSerializer的核心工作是把一行日志拆成 HBase 的列。日志格式如果是时间戳 用户ID 栏目 URL 停留时长Serializer 就要按这个结构解析并写入对应列。SimpleRowKeyGenerator决定 RowKey 怎么拼常见做法是时间戳反转 用户ID这样既能按时间范围 Scan又能避免热点写入。// SimpleRowKeyGenerator.java 核心逻辑示意 public class SimpleRowKeyGenerator implements RowKeyGenerator { Override public byte[] generateRowKey(Event event) { // 从 event header 或 body 中取时间戳 String ts new String(event.getBody()).split(,)[0]; // 时间戳反转保证新数据排在前面便于按时间倒序 Scan String reversed new StringBuilder(ts).reverse().toString(); return Bytes.toBytes(reversed); } }RowKey 设计是 HBase 使用里最容易被低估的一环。如果直接用时间戳正序新数据全往最后一个 Region 写热点问题会让写入性能断崖式下跌。反转时间戳是个取巧但有效的办法代价是范围 Scan 时要反过来理解顺序。4. Spark2x 计算作业编写与可视化数据出口4.1 从 HBase 读取数据构建 DataFrameSpark 读 HBase 没有官方 DataFrame 接口常见做法是用TableInputFormat走newAPIHadoopRDD再转成 DataFrame。下面这段代码是项目里sparkStu模块的典型写法// 构建 SparkSession SparkSession spark SparkSession.builder() .appName(NewsAnalysis) .master(local[*]) // 集群模式改为 yarn .getOrCreate(); // 配置 HBase 连接 Configuration hbaseConf HBaseConfiguration.create(); hbaseConf.set(hbase.zookeeper.quorum, localhost); hbaseConf.set(TableInputFormat.INPUT_TABLE, news_log); // 控制 Scan 范围避免全表扫描 Scan scan new Scan(); scan.setCaching(500); scan.setCacheBlocks(false); hbaseConf.set(TableInputFormat.SCAN, TableMapReduceUtil.convertScanToString(scan)); // 读取 HBase 数据为 RDD再转 DataFrame JavaPairRDDImmutableBytesWritable, Result hbaseRDD spark.sparkContext().newAPIHadoopRDD( hbaseConf, TableInputFormat.class, ImmutableBytesWritable.class, Result.class); // 解析 Result提取需要的列 JavaRDDRow rows hbaseRDD.map(tuple - { Result result tuple._2(); String ts Bytes.toString(result.getValue(Bytes.toBytes(info), Bytes.toBytes(ts))); String column Bytes.toString(result.getValue(Bytes.toBytes(info), Bytes.toBytes(column))); long duration Long.parseLong(Bytes.toString(result.getValue(Bytes.toBytes(info), Bytes.toBytes(duration)))); return RowFactory.create(ts, column, duration); }); // 定义 Schema StructType schema new StructType(new StructField[]{ new StructField(ts, DataTypes.StringType, false, Metadata.empty()), new StructField(column, DataTypes.StringType, false, Metadata.empty()), new StructField(duration, DataTypes.LongType, false, Metadata.empty()) }); DatasetRow df spark.createDataFrame(rows, schema); df.createOrReplaceTempView(news_log);setCaching(500)控制每次 RPC 拉多少行太小会导致 RPC 次数暴增太大吃内存。setCacheBlocks(false)在纯扫描场景下建议关掉避免占满 RegionServer 的块缓存。Scan 范围一定要设全表扫描在数据量上来之后就是灾难。4.2 核心分析指标与 Spark SQL 实现新闻分析常见的指标有各栏目访问量排名、每小时访问趋势、用户平均停留时长、高频访问路径。用 Spark SQL 写起来很直接// 栏目热度排名 DatasetRow columnRank spark.sql( SELECT column, COUNT(*) AS pv, AVG(duration) AS avg_duration FROM news_log GROUP BY column ORDER BY pv DESC); // 每小时访问趋势 DatasetRow hourlyTrend spark.sql( SELECT substr(ts, 12, 2) AS hour, COUNT(*) AS pv FROM news_log GROUP BY substr(ts, 12, 2) ORDER BY hour); // 结果写出到 MySQL 或 HBase供前端读取 columnRank.write() .format(jdbc) .option(url, jdbc:mysql://localhost:3306/news_analysis) .option(dbtable, column_rank) .option(user, root) .option(password, your_password) .mode(overwrite) .save();substr(ts, 12, 2)这种写法依赖时间戳的字符串格式固定如果日志里时间格式不统一先做一步清洗再聚合。写出模式用overwrite还是append取决于前端是看全量还是增量实时看板一般用append配合时间窗口。4.3 可视化数据接口与前端对接后端算完的结果落到 MySQL 后前端通过 HTTP 接口拉数据。项目里z_pic目录下的截图显示用的是 ECharts 这类图表库。接口层可以用 Spring Boot 快速搭RestController RequestMapping(/api/news) public class NewsAnalysisController { Autowired private JdbcTemplate jdbcTemplate; GetMapping(/columnRank) public ListMapString, Object columnRank() { // 从 MySQL 读 Spark 算好的结果 return jdbcTemplate.queryForList( SELECT column, pv, avg_duration FROM column_rank ORDER BY pv DESC LIMIT 20); } }前端拿到 JSON 后直接喂给 ECharts 的series和xAxis。这一步的坑在于字段名要对齐Spark 写出的列名如果是大写MySQL 里可能变成小写前端取不到值。统一在 SQL 里用别名固定字段名。5. 避坑与常见问题排查5.1 Flume 写入 HBase 时报 RegionTooBusy现象Flume 日志里频繁出现RegionTooBusyException写入吞吐骤降。原因RowKey 设计不合理导致热点写入单个 Region 承受了大部分流量。解决检查SimpleRowKeyGenerator的散列逻辑加入用户ID或随机前缀做盐化把写入分散到多个 Region。如果已经建表考虑预分区。5.2 Spark 任务卡在最后一个 Stage现象Spark UI 上任务跑到 99% 卡住长时间不结束。原因数据倾斜。某个 key 对应的数据量远大于其他 key单个 task 处理不过来。解决先df.groupBy(column).count().orderBy(desc(count)).show()找出倾斜的 key对它加随机前缀打散后再聚合或者用repartition增加并行度。5.3 编译通过但运行时报 NoSuchMethodError现象mvn package成功提交到集群跑的时候抛NoSuchMethodError或ClassNotFoundException。原因依赖冲突。pom.xml里引入的某个库版本和集群上已有版本不一致。解决用mvn dependency:tree看依赖树把冲突的包排除掉或者把 scope 改成provided让集群提供。5.4 前端图表数据不更新现象Spark 作业跑完了MySQL 里数据也更新了但前端图表还是旧数据。原因接口层有缓存或者前端轮询间隔太长。解决检查后端有没有加Cacheable有的话清掉前端轮询间隔调到 30 秒以内实时看板可以更短。5.5 HBase Scan 超时现象Spark 读 HBase 时报ScannerTimeoutException。原因单次 Scan 返回数据量太大超过了 RegionServer 的超时阈值。解决调大hbase.rpc.timeout和hbase.client.scanner.timeout.period同时把 Scan 拆成多个小范围分批读。6. 进阶技巧把离线结果和实时流接起来这套项目默认跑的是离线批处理但新闻分析的价值很大一部分在实时性上。一个实用的进阶做法是用 Spark Streaming 接 Kafka 里的实时日志做窗口聚合把结果写到 HBase 或 Redis前端同时读离线和实时两张表用不同颜色区分。// Spark Streaming 接 Kafka做 1 分钟窗口聚合 JavaStreamingContext jssc new JavaStreamingContext(sparkConf, Durations.seconds(10)); JavaInputDStreamConsumerRecordString, String stream KafkaUtils.createDirectStream( jssc, LocationStrategies.PreferConsistent(), ConsumerStrategies.Subscribe(Arrays.asList(news_log_topic), kafkaParams)); // 解析并做窗口聚合 stream.map(record - record.value()) .flatMap(line - Arrays.asList(line.split(\n)).iterator()) .mapToPair(line - { String[] parts line.split(,); return new Tuple2(parts[2], 1L); // 按栏目计数 }) .reduceByKeyAndWindow((a, b) - a b, Durations.minutes(1), Durations.seconds(10)) .foreachRDD(rdd - { rdd.foreachPartition(partition - { // 写入 Redis 或 HBase供前端实时读取 }); }); jssc.start(); jssc.awaitTermination();窗口大小和滑动间隔要根据业务定。1 分钟窗口、10 秒滑动意味着每 10 秒更新一次最近 1 分钟的聚合结果前端看起来就是实时的。窗口太大延迟高太小计算频繁、资源吃紧。验证实时链路是否正常我一般会做两件事一是往 Kafka 里手动灌几条测试数据看 Redis 里有没有对应 key 更新二是把 Spark Streaming 的 batch duration 和实际处理时间对比如果处理时间持续大于 batch duration说明积压了要加资源或调并行度。从那以后我每次接实时链路都强制先跑一遍灌数据 → 看中间存储 → 对前端接口这三步确认每一段都通了再上生产。这套项目把离线链路搭得比较完整实时部分留了扩展空间改起来不算费劲。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

大模型性价比评估实战:从成本拆解到迁移落地的完整方法论

大模型性价比评估实战:从成本拆解到迁移落地的完整方法论

1. 从"性能比肩、成本六折"说起:这个标题到底在讲什么第一次看到"性能比肩、成本六折"这个说法,我的第一反应不是兴奋,而是习惯性地去找它的比较基准。任何一句关于模型性能的论断,如果不把"跟谁比、在什…

2026/10/12 1:38:40 阅读更多 →
无人机俯视车辆行人检测:VisDrone数据集YOLOv5训练与避坑指南

无人机俯视车辆行人检测:VisDrone数据集YOLOv5训练与避坑指南

简介:无人机俯视视角下的车辆与行人目标检测数据集,面向目标检测算法研发人员与相关专业学习者,专为YOLO系列模型快速迭代而整理。资源包含一千多张无人机实拍俯视图像,所有样本均标注车辆与行人两类目标,可应用于智慧…

2026/10/12 1:56:12 阅读更多 →
材料力学复习资料这样整理才有效:南理工期末与考研实战主线

材料力学复习资料这样整理才有效:南理工期末与考研实战主线

又到了期末复习月,群里又开始疯传各种南京理工大学材料力学复习资料,PPT、课后题答案、往年卷、手写笔记,加起来快有一个G。资料是真的多,但很多人最后挂在试卷上的原因,不是资料少,而是资料堆在一起不知道…

2026/10/12 1:55:36 阅读更多 →

最新新闻

Vue实战进阶:从环境配置到动态路由与多端部署要点

Vue实战进阶:从环境配置到动态路由与多端部署要点

学Vue这几年,从最开始只会照着教程敲todo list,到后来完整负责过好几个基于Spring Boot加Vue的前后端分离项目,中间踩过的坑、绕过的弯,攒下来不少东西。这份Vue网站制作学习心得,我按“从入门到实战的进阶之路”来写&…

2026/10/12 13:21:33 阅读更多 →
豆瓣Top250数据分析大作业:Python爬虫+可视化完整源码与避坑指南

豆瓣Top250数据分析大作业:Python爬虫+可视化完整源码与避坑指南

简介:这份资源是面向高校学生与Python初学者的数据可视化课程设计完整方案,围绕豆瓣电影Top250数据集展开,涵盖数据采集、清洗、统计分析与图表呈现全流程,适合用作毕业设计、期末大作业或课程设计的高分参考。压缩包共221个文件&…

2026/10/12 13:21:33 阅读更多 →
基于SpringBoot+Vue的办公管理系统:全栈设计与实现指南

基于SpringBoot+Vue的办公管理系统:全栈设计与实现指南

每年到这个时间点,都会有一批学弟学妹来问我:"毕设选题到底怎么选?Java后端会不会太难?前端是不是要写很多页面?"我的回答一直很统一:如果方向是Java Web或全栈开发,基于SpringBoot …

2026/10/12 13:21:33 阅读更多 →
claude-mem:为Claude打造跨会话长期记忆的本地记忆管理层

claude-mem:为Claude打造跨会话长期记忆的本地记忆管理层

你有没有过这种体验:跟Claude聊一个项目,前期费了好大劲把背景、依赖、约束讲清楚,刚进入正题,上下文窗口满了,或者新开一个会话,全部打回原形。每次对话就像金鱼一样,七秒记忆,项目…

2026/10/12 13:21:33 阅读更多 →
云数据库HTTP API歌单更新实战:权限、批量与避坑指南

云数据库HTTP API歌单更新实战:权限、批量与避坑指南

1. 从一次歌单同步的诡异失败说起做云音乐类小程序,绕不开的一个核心功能就是歌单管理。用户在小程序端新建、编辑、删除歌单,这些操作最终都要落到云数据库里。我最近在重构一个云音乐小程序的管理系统,前面几篇已经把基础架构、页面路由、用…

2026/10/12 13:21:33 阅读更多 →
C++中判断汉字:从编码原理到跨平台实现

C++中判断汉字:从编码原理到跨平台实现

1. 从一次文本统计翻车说起:为什么“判断汉字”没那么简单前阵子帮一个做数据清洗的朋友处理一批用户提交的文本,需求听起来特别朴素:把每条记录里的汉字挑出来,统计一下字数,非汉字的字符(标点、数字、字母…

2026/10/12 13:20:31 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练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/11 14:36:54 阅读更多 →