Python知识图谱问答系统课程设计:Neo4j图数据库构建与Cypher查询实战
简介这份资源是面向高校学生与Python学习者的知识图谱问答系统课程设计完整源码适合作为毕业设计、课程大作业或知识图谱入门实战的参考方案项目评分在95分以上。压缩包共486个文件约32.56MB涵盖py、java、class、vue、js、ts、xml、txt、png、sql、owl等多种类型分别对应后端服务、前端页面、知识图谱数据、问答逻辑与部署配置等模块。源码按功能划分为五个文件夹Kbqa-website-deploy负责项目部署buildQAModule构建问答模块buildKnowledgeGraph完成知识图谱搭建buildFrontendWebsite与buildBackendService分别实现前端与后端服务并附有详细的项目介绍和部署文档按图索骥即可跑通整个系统。目前已有3188人学习下载读者可借此掌握从图谱构建、问答匹配到前后端联调的完整流程理解工程目录组织与常见问题排查思路快速完成一份高质量课程设计。1. 知识图谱问答系统从课程设计到95分项目的落地拆解很多同学做 Python 课程设计时选题要么是爬虫加可视化要么是管理系统增删改查答辩时老师一眼就能看出工作量。而基于知识图谱的问答系统恰好卡在“有技术含量”和“能独立完成”之间——它涉及数据采集、图谱构建、语义解析、查询推理、前端交互五个环节每个环节都能讲出东西但又不至于像深度学习项目那样调参调到崩溃。这个标题下的核心工作是用 Python 把一批结构化或半结构化数据变成图数据库里的实体关系网络再让用户用自然语言提问系统自动翻译成图查询语句并返回答案。适合有 Python 基础、学过数据结构、想拿高分但不想碰 GPU 的本科生。下面按我实际做过的路径把选型、构建、查询、避坑一次讲透。2. 知识图谱构建从原始数据到 Neo4j 图库的完整链路2.1 为什么选 Neo4j 而不是 NetworkX 或 RDFLib课程设计里常见的图存储方案有三种NetworkX 纯内存、RDFLib 做语义网、Neo4j 图数据库。NetworkX 画图方便但一旦节点超过几千个查询和持久化就是灾难每次重启程序都要重新加载。RDFLib 适合做本体推理但 Cypher 查询的直观程度远不如 Neo4j而且课程设计答辩时老师更认可“用了数据库”这个点。Neo4j 社区版免费Python 驱动成熟Cypher 语句可读性强配合 py2neo 或官方 neo4j 驱动都能快速上手。我一般会建议数据量在 1 万节点以内Neo4j 社区版完全够用内存占用可控启动也快。安装 Neo4j 有两种方式桌面版和 Docker。课程设计环境用 Docker 最省事一行命令拉起数据卷挂载到本地换电脑也能迁移。# 拉取 Neo4j 社区版镜像指定 5.x 版本 docker pull neo4j:5.20-community # 启动容器映射 7474 浏览器端口和 7687 Bolt 协议端口 docker run -d \ --name kg-qa-neo4j \ -p 7474:7474 -p 7687:7687 \ -v $(pwd)/neo4j_data:/data \ -e NEO4J_AUTHneo4j/your_password \ neo4j:5.20-community启动后浏览器打开http://localhost:7474用默认账号neo4j和上面设置的密码登录。NEO4J_AUTH环境变量必须设置否则容器启动后会要求你改密码脚本化部署时很麻烦。数据卷挂载到neo4j_data目录删容器不丢数据。2.2 用 Python 把 CSV 数据灌进图数据库假设你手头有一份电影领域的 CSV包含电影、导演、演员、类型四类实体和它们之间的关系。原始数据可能是三张表movies.csv、persons.csv、relations.csv。第一步是用 pandas 读进来做清洗第二步是用 neo4j 官方驱动批量写入。import pandas as pd from neo4j import GraphDatabase # 连接 Neo4j注意 auth 参数是元组 driver GraphDatabase.driver( bolt://localhost:7687, auth(neo4j, your_password) ) # 读取清洗后的实体和关系数据 movies pd.read_csv(data/movies_clean.csv) persons pd.read_csv(data/persons_clean.csv) relations pd.read_csv(data/relations_clean.csv) def create_movie_node(tx, movie_id, title, year, rating): # MERGE 保证幂等重复执行不会产生重复节点 tx.run( MERGE (m:Movie {id: $id}) SET m.title $title, m.year $year, m.rating $rating, idmovie_id, titletitle, yearyear, ratingrating ) def create_relation(tx, from_id, to_id, rel_type, from_label, to_label): # 关系类型不能参数化必须用字符串拼接但要做白名单校验 allowed {ACTED_IN, DIRECTED, BELONGS_TO} if rel_type not in allowed: raise ValueError(f非法关系类型: {rel_type}) query ( fMATCH (a:{from_label} {{id: $from_id}}) fMATCH (b:{to_label} {{id: $to_id}}) fMERGE (a)-[:{rel_type}]-(b) ) tx.run(query, from_idfrom_id, to_idto_id) with driver.session() as session: # 批量写入电影节点 for _, row in movies.iterrows(): session.execute_write( create_movie_node, row[id], row[title], int(row[year]), float(row[rating]) ) # 批量写入人物节点这里省略逻辑同上 # 批量写入关系 for _, row in relations.iterrows(): session.execute_write( create_relation, row[from_id], row[to_id], row[rel_type], row[from_label], row[to_label] ) driver.close()这段代码的关键点有三个。第一MERGE而不是CREATE保证脚本可以重复跑调试时不会因为重复执行把图搞脏。第二关系类型不能用参数化查询Cypher 不支持[:$type]这种写法所以必须拼接字符串但一定要加白名单校验否则就是 Cypher 注入漏洞。第三execute_write会自动处理事务重试比手动开事务省心。参数方面batch_size如果数据量大可以改成批量提交但课程设计的数据量通常几千条逐条写也能在几十秒内完成。2.3 数据清洗里最容易翻车的三个字段实体对齐是知识图谱构建里最容易被低估的环节。我见过太多项目图建好了查询也能跑但一问“周星驰演过哪些电影”返回结果里混进了“周星弛”“周星驰导演”这种脏数据。原因就是清洗阶段没做归一化。第一个坑是同名不同实体。比如“刘德华”既是演员也是歌手如果只按名字建节点两个身份会合并成一个。解决办法是给每个实体加type属性查询时带上类型约束。第二个坑是别名未合并。比如“星爷”和“周星驰”在原始数据里可能是两条记录需要维护一张别名表在写入前统一替换成标准名。第三个坑是关系方向写反。比如“导演”关系应该是(Person)-[:DIRECTED]-(Movie)如果写成(Movie)-[:DIRECTED]-(Person)查询时就会得到荒谬的结果。建议在写入前用 pandas 做一次关系方向校验把不符合预期的行打印出来人工确认。提示清洗阶段每做一步都存一份中间 CSV出问题时可以回滚到上一步不用从头再来。3. 自然语言转 Cypher问答系统的核心翻译层怎么搭3.1 意图识别与实体抽取的轻量方案问答系统的前端输入是“周星驰演过哪些电影”后端要把它变成MATCH (p:Person {name:周星驰})-[:ACTED_IN]-(m:Movie) RETURN m.title。这个翻译过程分两步先识别用户意图问的是演员作品、导演作品、电影评分还是类型归属再抽取实体人名、电影名、类型名。课程设计里不需要上 BERT 微调用规则加词典就能覆盖 80% 的常见问法。我一般会建一个意图模板表用正则匹配问句模式。import re # 意图模板正则模式 - 意图标签 INTENT_PATTERNS [ (r(.?)演过哪些电影, actor_movies), (r(.?)导演过哪些电影, director_movies), (r(.?)的评分是多少, movie_rating), (r(.?)是什么类型, movie_genre), (r哪些电影是(.?)类型的, genre_movies), ] def detect_intent(question): for pattern, intent in INTENT_PATTERNS: match re.search(pattern, question) if match: return intent, match.group(1).strip() return None, None # 测试 q 周星驰演过哪些电影 intent, entity detect_intent(q) print(intent, entity) # 输出: actor_movies 周星驰这段代码的逻辑很直白按顺序匹配正则命中就返回意图和捕获的实体名。参数方面正则里的(.?)是非贪婪匹配避免把“周星驰演过哪些电影”里的“周星驰演过”整个吞掉。如果问句是“演过《功夫》的演员有哪些”这个模板就匹配不上需要再加一条反向模式。实际项目中意图模板通常要写 15 到 20 条覆盖主要问法。实体抽取比意图识别更依赖词典。把图数据库里所有 Person 和 Movie 的 name 属性拉出来构建一个前缀树或简单的集合然后用最大正向匹配去问句里找实体。如果问句里的词不在词典里就返回“未识别到实体”让用户换个说法。3.2 模板填充生成 Cypher 的四个参数化细节意图和实体都拿到后下一步是填充 Cypher 模板。这里最容易出问题的是字符串拼接和参数传递。CYPHER_TEMPLATES { actor_movies: ( MATCH (p:Person {name: $name})-[:ACTED_IN]-(m:Movie) RETURN m.title AS title, m.year AS year, m.rating AS rating ORDER BY m.year DESC ), director_movies: ( MATCH (p:Person {name: $name})-[:DIRECTED]-(m:Movie) RETURN m.title AS title, m.year AS year ORDER BY m.year DESC ), movie_rating: ( MATCH (m:Movie {title: $title}) RETURN m.rating AS rating, m.year AS year ), } def build_query(intent, entity): template CYPHER_TEMPLATES.get(intent) if not template: return None, None # 参数化传递避免 Cypher 注入 params {name: entity} if name in template else {title: entity} return template, params第一个细节模板里的$name和$title是参数占位符实际执行时通过 driver 的session.run(query, params)传入不要用 Python 的 f-string 直接拼。第二个细节ORDER BY放在RETURN之后如果写反了 Cypher 会报语法错误。第三个细节返回字段用AS起别名前端拿到的 JSON 键名才统一。第四个细节如果实体名在数据库里不存在查询会返回空列表前端要处理这种“查无结果”的情况而不是直接报错。3.3 把翻译层和 Neo4j 驱动接起来翻译层输出 Cypher 和参数后执行查询就是一行代码的事。但这里有个性能陷阱每次查询都新建 driver 会拖慢响应。正确做法是在应用启动时创建一个全局 driver所有查询复用。from neo4j import GraphDatabase class KGQAService: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def query(self, question): intent, entity detect_intent(question) if not intent: return {error: 无法识别问题意图请换个说法} cypher, params build_query(intent, entity) if not cypher: return {error: 不支持的查询类型} with self.driver.session() as session: result session.run(cypher, params) records [dict(record) for record in result] if not records: return {error: f未找到与「{entity}」相关的信息} return {intent: intent, entity: entity, data: records} def close(self): self.driver.close()session.run返回的是惰性结果集list(result)或列表推导会触发实际查询。dict(record)把每条记录转成 Python 字典方便后续 JSON 序列化。如果查询结果很大可以加LIMIT限制返回条数课程设计里通常 50 条足够展示。注意Neo4j driver 是线程安全的但 session 不是。如果在 Flask 或 FastAPI 里用每个请求创建独立 session不要跨请求共享。4. 避坑与排查课程设计答辩前必须过的五道坎4.1 中文实体写入后查询不到现象Python 脚本里明明写入了“周星驰”但在 Neo4j 浏览器里用MATCH (p:Person {name:周星驰}) RETURN p查不到。原因Neo4j 默认使用 UTF-8 编码但 CSV 文件如果是从 Excel 导出的可能是 GBK 编码。pandas 读取时没指定encoding参数中文变成乱码写入。解决pd.read_csv(data.csv, encodingutf-8)或encodinggbk都试一下写入前打印前几行确认中文正常。Neo4j 浏览器里查询时确保输入法没有把引号打成中文引号。4.2 Cypher 查询返回空但数据库里确实有数据现象MATCH (m:Movie {title:功夫}) RETURN m返回空但用MATCH (m:Movie) RETURN m LIMIT 10能看到“功夫”节点。原因属性值里有不可见字符比如首尾空格或换行符。CSV 读取时strip()没做干净。解决写入前对所有字符串字段做str.strip()查询时也可以用WHERE trim(m.title) 功夫临时验证。根治办法是在清洗阶段统一处理。4.3 关系类型拼写错误导致查询静默失败现象MATCH (p:Person)-[:ACTED_IN]-(m:Movie) RETURN m返回空但数据明明写入了。原因写入时关系类型写成了ACTEDIN或acted_inCypher 关系类型大小写敏感且不支持模糊匹配。解决在 Neo4j 浏览器里执行CALL db.relationshipTypes()查看所有关系类型确认拼写。写入脚本里把关系类型定义成常量避免手误。4.4 问答系统对同义问法识别率低现象“周星驰演过什么电影”能回答“周星驰的作品有哪些”就识别不了。原因意图模板只覆盖了一种问法正则没有做同义词扩展。解决把“演过”“出演”“参演”“作品”都写进正则的或分支里例如r(.?)(演过|出演|参演|的作品)哪些?电影。更系统的做法是维护一个同义词词典匹配前先做替换。4.5 答辩演示时 Neo4j 服务没启动现象本地跑得好好的换到答辩教室的电脑上Python 脚本报连接拒绝。原因Neo4j 是独立服务不是 Python 包换电脑后没启动或端口被占用。解决准备一个start.sh脚本把 Docker 启动命令写进去答辩前先执行。如果教室电脑没有 Docker提前装好 Neo4j Desktop 并导入数据备份。最稳妥的办法是录屏演示但有些老师要求现场跑那就把数据导出成 Cypher 文件现场用cypher-shell导入。5. 让问答系统多走一步从单轮查询到多跳推理的改造技巧单轮查询只能回答“周星驰演过哪些电影”这种一跳问题。如果用户问“周星驰演过的电影里哪些是李力持导演的”就需要两跳先找周星驰演的电影再筛出其中导演是李力持的。课程设计里加这个功能答辩时能明显拉开差距。改造思路是在 Cypher 模板里支持多段 MATCH。比如针对“某人演过某导演的哪些电影”模板写成MULTI_HOP_TEMPLATE MATCH (p:Person {name: $actor})-[:ACTED_IN]-(m:Movie) MATCH (d:Person {name: $director})-[:DIRECTED]-(m) RETURN m.title AS title, m.year AS year ORDER BY m.year DESC 参数从单个实体变成两个实体意图识别也要相应升级。可以在正则里捕获两个实体名比如r(.?)演过的电影里哪些是(.?)导演的然后分别传入$actor和$director。如果只捕获到一个实体就回退到单跳模板。验证多跳查询是否正确我一般会先在 Neo4j 浏览器里手动跑一遍 Cypher确认返回结果符合预期再把语句复制到 Python 模板里。浏览器里可以用EXPLAIN或PROFILE看查询计划如果出现CartesianProduct说明两个 MATCH 之间没有关联结果会爆炸。正确的写法是两个 MATCH 共享变量mNeo4j 会自动做连接。还有一个实用技巧给查询结果加一个confidence字段。如果多跳查询返回空自动降级到单跳查询并告诉用户“未找到同时满足两个条件的电影以下是该演员的全部作品”。这样演示时不会出现“查不到”的尴尬用户体验也更平滑。最后说个血泪经验课程设计报告里一定要把知识图谱的 schema 画出来用表格列出实体类型、关系类型和属性。答辩老师看代码之前先看 schemaschema 清晰印象分就稳了。我当初偷懒没画被问了三次“你的图里到底有哪些东西”现场翻代码很狼狈。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

DBeaver CE 24.2.2:全能数据库工作台连接 MySQL 实战指南

DBeaver CE 24.2.2:全能数据库工作台连接 MySQL 实战指南

简介:DBeaver Community Edition 24.2.2 Windows 版是一款开源数据库管理工具与 SQL 客户端,面向需要在 Windows 环境下快速连接、查询和维护数据库的开发、运维及初学者,可解决数据查询、结构浏览与基础管理等工作。该版本以“下载即用”为特…

2026/10/12 0:25:11 阅读更多 →
三维LBM渗流模拟实战:从D3Q19模型到渗透率计算

三维LBM渗流模拟实战:从D3Q19模型到渗透率计算

简介:这是一份基于格子玻尔兹曼方法(LBM)的三维渗流模拟MATLAB源码,主要面向流体力学、多孔介质渗流方向的工程师与科研人员,适用于地下水流动、石油开采、土壤污染扩散等典型渗流场景。该方法通过离散玻尔兹曼方程模拟…

2026/10/12 0:24:11 阅读更多 →
煤气罐目标检测数据集怎么用:VOC、YOLO、COCO等标注格式转换与训练避坑指南

煤气罐目标检测数据集怎么用:VOC、YOLO、COCO等标注格式转换与训练避坑指南

简介:煤气罐检测数据集面向智慧社区、智能安防及煤气罐识别告警等实际项目,可服务课程作业、算法设计与竞赛实践。整套数据包含1071张jpg原图,并同步提供voc(xml)、yolo(txt)、json三种格式标签,分别适配VOC系列、YOLO系列及Label…

2026/10/12 0:24:11 阅读更多 →

最新新闻

JanusGraph 核心能力与存储后端选型:从超大规模图处理到 CAP 权衡

JanusGraph 核心能力与存储后端选型:从超大规模图处理到 CAP 权衡

图数据库分布式数据库后端 【免费下载链接】janusgraph JanusGraph: an open-source, distributed graph database 项目地址: https://gitcode.com/gh_mirrors/ja/janusgraph 点击查看 免费下载 导读:本文围绕 JanusGraph 官方文档《The Benefits of Ja…

2026/10/12 2:03:07 阅读更多 →
Langchain01_框架之模型的创建与调用

Langchain01_框架之模型的创建与调用

模型创建3种方式 1.使用特定的Model Class(最直接,但不好用) LangChain为一些大模型供应商提供了专门的Model类,导入对应的具体类(如 ChatOpenAI、ChatAnthropic、ChatDeepSeek、ChatOllama、ChatHunyuan、ChatTongy…

2026/10/12 2:03:07 阅读更多 →
ET高级定制版与睿排引擎:从智能排版到可打印的完整工程实践

ET高级定制版与睿排引擎:从智能排版到可打印的完整工程实践

/* 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 2:03:07 阅读更多 →
SQL练习题全解析:从建表到嵌套查询的避坑指南

SQL练习题全解析:从建表到嵌套查询的避坑指南

/* 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 2:03:07 阅读更多 →
MySQL存储引擎深度对比:InnoDB与MyISAM的差异、调优与迁移实践

MySQL存储引擎深度对比:InnoDB与MyISAM的差异、调优与迁移实践

/* 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 2:03:07 阅读更多 →
PaperSpine 执行效率方法论:精确复用、昂贵操作凭证与有界失败恢复的工程实践

PaperSpine 执行效率方法论:精确复用、昂贵操作凭证与有界失败恢复的工程实践

AI 技能AI 写作人工智能深度研究AI 应用 【免费下载链接】PaperSpine PaperSpine5 — local-first, evidence-bound paper research, writing, figures, review and delivery. Download: https://wubing2023.github.io/PaperSpine/v5/ 项目地址: https://gitcode.co…

2026/10/12 2:02:07 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器: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 阅读更多 →