news 2026/10/13 1:29:31

基于知识图谱的古诗词问答系统:Python+Neo4j完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于知识图谱的古诗词问答系统:Python+Neo4j完整实现

简介:基于知识图谱的古诗词问答系统完整源码包,面向计算机相关专业毕业生、课程设计与项目实战学习者,适合作为本科毕设、课程设计或期末大作业的完整参考。资源以Python后端、知识图谱数据与前端展示为核心,共521个文件,含98个py源码、129个txt文本说明、24个json结构化数据,以及css/js/html前端资源和jpg/png图片素材,压缩包整体50.58MB。项目经导师指导并高分通过,代码测试运行无误,覆盖数据预处理、实体关系抽取、图谱存储、问句解析、检索排序与可视化展示等核心环节,从语料清洗到前端界面均有完整工程实现,学习者可据此快速掌握知识图谱问答系统的设计与编码实践。已有331人学习下载。内含清晰目录结构与README文档,可快速定位关键模块,理解古诗词知识图谱问答系统的完整实现思路,并支持在此基础上扩展新功能。

1. 基于知识图谱的古诗词问答系统:一个能直接复现的 Python 毕设项目

基于知识图谱的古诗词问答系统,是这几年计算机毕业设计里既出效果又能讲清技术点的方向。这套 Python 源码我本地完整跑通过,整体链路很清晰:Bootstrap 页面负责对话交互,后端 Python 把用户的问题拆成意图和实体,再构造 Cypher 查询 Neo4j 知识图谱,最终把诗作、诗人、朝代信息渲染回前端。它能解决普通关键词搜索答不了的自然语言问题,比如“李白写过哪些带‘月’字的诗”“王维的山水田园诗代表作是什么”。适合正在做知识图谱、问答系统方向毕设的学生,也适合想快速上手 Neo4j + Python 全栈项目的开发者。

2. 从自然语言到 Cypher:知识图谱问答的核心流程与设计逻辑

2.1 为什么用图数据库而不用 MySQL:关系查询成本差异

古诗词领域的数据,核心资产就是“关系”。诗属于某个作者,作者属于某个朝代,诗里包含若干名句,这些关系放进 MySQL 不是不能做,而是查询体验很别扭。用户问“王维的代表作里有没有写山水田园的诗”,换成 SQL 就是三张表 JOIN 加 LIKE 过滤;可问题再绕一步,“这些诗的意象在其他诗人的哪些作品里也出现过”,SQL 的 JOIN 链条会越写越长,维护起来像看天书。

放在 Neo4j 里,关系是存储层的一等公民。同样的场景,一条 Cypher 就能表达:从 Author 节点经过 write 边到 Poem,再经过 has_topic 边到主题节点。括号和箭头直接描述路径,业务模型和存储模型是对应的,这就是图数据库在知识图谱问答里的核心优势。

SELECT p.title FROM poem p JOIN author a ON p.author_id = a.id JOIN poem_topic pt ON pt.poem_id = p.id JOIN topic t ON t.id = pt.topic_id WHERE a.name = '王维' AND t.name = '山水田园';

这段 SQL 的逻辑并不难,但你注意看表之间的关联字段:author_id、poem_id、topic_id,每一个 JOIN 都是在手动拼关系。一旦需求变成“查所有写过山水田园诗的诗人”,SQL 又得重新组织;而图数据库里,关系的方向和类型已经在边上存好了,换查询角度只是换 Cypher 的箭头方向。

MATCH (a:Author {name:'王维'})-[:write]->(p:Poem) WHERE p.content CONTAINS '山' OR p.content CONTAINS '水' RETURN p.title, p.content

这里CONTAINS是 Cypher 的字符串包含匹配,对应 SQL 的LIKE %山%。我在本地测试时,同一批数据用 MySQL 和 Neo4j 各查一遍,写 Cypher 的时间大概是写 SQL 的一半,而且改查询方向时基本不用推翻重来。对于毕设答辩来说,这个选型理由本身就能讲三分钟。

2.2 问答主流程:意图识别、实体抽取与查询模板

问答系统听起来智能,但本科毕设级别的主流实现不依赖大模型,而是把流程拆成三个明确步骤:意图识别、实体抽取、查询构造。意图识别负责判断问题类型,实体抽取负责抓出“李白”“静夜思”这类关键词,查询构造则把两者拼成 Cypher。

意图识别常见的做法是关键词表驱动。古诗词问答的意图类型很有限,我一般会维护一个映射关系,每个意图对应一组触发词。

def classify_intent(question: str) -> str: if any(w in question for w in ["哪些诗", "诗有哪些", "代表作", "写过"]): return "AUTHOR_POEM" if any(w in question for w in ["作者是谁", "谁写的", "写于"]): return "POEM_AUTHOR" if any(w in question for w in ["名句", "千古名句"]): return "FAMOUS_LINE" if any(w in question for w in ["哪个朝代", "什么朝代", "时期"]): return "POEM_DYNASTY" return "UNKNOWN"

这个函数的参数question是前端传来的原始问题字符串,返回值是意图标签。逻辑上只用any做包含匹配,代价是关键词覆盖不全,但配合后面的实体抽取已经能覆盖日常提问。这里有个细节值得注意:意图识别结果不要直接丢给查询构造,要把原始问题原样传递下去,因为实体抽取还需要它。

实体抽取是问答系统的另外一个关键环节。古诗词实体是闭式集合,也就是说,系统里能问的诗人、诗作、朝代名都在知识图谱里存着,不需要训练命名实体识别模型,直接用词典匹配即可。

def extract_entities(question: str, entity_dict: dict) -> dict: hits = {} for category, terms in entity_dict.items(): for term in sorted(terms, key=len, reverse=True): if term and term in question: hits[category] = term break return hits

这个函数的参数含义要说明一下:entity_dict是一个字典,key 是实体类别,比如 author、poem、dynasty,value 是名称列表;question是问题文本;返回值hits保存命中的实体。排序用key=len, reverse=True是为了让长的词条优先匹配,避免“白居易”被误匹配成“白”。这个顺序问题,我在真机调试时翻过车,后面避坑章节会再提。

意图和实体都拿到了,下一步就是查询模板选择。模板的本质是“意图到 Cypher 的映射”,可以用字典实现。

CASES = { "AUTHOR_POEM": ( "MATCH (a:Author {name:$name})-[:write]->(p:Poem) " "RETURN p.title, p.content" ), "POEM_AUTHOR": ( "MATCH (p:Poem {title:$title})<-[:write]-(a:Author) " "RETURN a.name" ), } def build_query(intent: str, entities: dict): if intent == "AUTHOR_POEM" and "author" in entities: return CASES["AUTHOR_POEM"], {"name": entities["author"]} if intent == "POEM_AUTHOR" and "poem" in entities: return CASES["POEM_AUTHOR"], {"title": entities["poem"]} return None, None

注意这里我使用了$name、$title参数占位符,而不是把实体值直接拼进 Cypher。这样做的原因有两个:一是防止特殊字符破坏语法,二是参数化查询能避免 Cypher 注入。很多初写问答系统的同学会忽略这一点,直接在字符串里+拼接,一旦用户输入包含引号,整条查询就会报错。

2.3 前端界面与后端路由:问答在哪里完成闭环

这套源码里能看到一组 Bootstrap 相关的静态文件,包括 bootstrap.4.6.min.css、nifty.min.css、font-awesome.min.css、datatables.bootstrap.css 等。这些不是无关杂物,它们构成了一套带聊天窗口和数据展示能力的后台界面。nifty 是一套基于 Bootstrap 的后台管理模板,配合 jQuery 和少量 JavaScript,就能实现对话消息的发送、接收和渲染。

实际交互链路是:用户在聊天框输入问题,前端 JavaScript 通过 Ajax 把问题 POST 给后端接口,后端完成意图识别、实体抽取、查询构造、图谱查询四步,再把结果以 JSON 返回,前端解析后填充进对话气泡。整套链路的核心入口是一个 Flask 路由。

@app.route("/api/ask", methods=["POST"]) def ask(): data = request.get_json() question = data.get("question", "") intent = classify_intent(question) entities = extract_entities(question, entity_dict) query, params = build_query(intent, entities) if query is None: return jsonify({"code": 1, "answer": "暂无法回答该问题,换个问法试试。"}) result = graph.run(query, parameters=params).data() answer = render_answer(intent, result) return jsonify({"code": 0, "answer": answer, "result": result})

这段路由的作用是把 HTTP 请求转成业务调用。graph.run是图数据库连接对象执行查询的方法,返回结果是一个列表,每一项是一条记录。render_answer负责把记录拼成用户能看懂的中文句子,比如把“静夜思”拼成“《静夜思》是李白所作”。实际改业务时,主要动手点就在build_query和render_answer,其他环节基本稳定。

3. 古诗词知识图谱的实体关系设计与 LOAD CSV 导入全流程

3.1 实体与关系:四类节点与五类关系的设计原型

动手写代码前先建模。古诗词问答系统的最小可用模型通常包含四类实体:诗人、诗作、朝代、诗句。诗人需要姓名、别名、字号、朝代;诗作需要标题、正文、作者;朝代需要名称和大致时间范围;诗句需要原文和出处。这些实体之间存在稳定的语义关系。

常见的实体属性设计如下表:

实体关键属性说明
Authorid, name, alias, dynasty诗人姓名与别名
Poemid, title, content, dynasty_id诗作标题与全文
Dynastyname, period朝代名称与时段
Sentencetext, poem_id名句原文与出处

关系设计上,五类关系基本够用:Author 到 Poem 的 write 边,表示作品归属;Poem 到 Dynasty 的 in_dynasty 边,表示作品朝代;Author 到 Dynasty 的 belongs_to 边,表示诗人生活朝代;Poem 到 Sentence 的 has_sentence 边,表示名句出自某作品;Poem 到 Poem 的引用关系则视数据源而定,有就加,没有不强求。

这里有一个冗余设计值得说明:Author 和 Poem 都设了 dynasty 相关字段。为什么不只保留 Poem 的朝代属性?因为用户会问“李白是哪个朝代的”,也会问“静夜思是什么朝代的”。两个问题分别查 Author 和 Poem,各有各的朝代答案,而不是每次都要通过关系去跳。实际建模时保留这种合理冗余,能让查询模板简洁很多。

3.2 导入流程:从 CSV 到图的三条 Cypher 命令

Neo4j 官方推荐用 LOAD CSV 做批量导入,数据文件必须放在 Neo4j 安装目录的 import 文件夹下。拿到古诗词数据集后,通常先整理成三份 CSV:poets.csv 存诗人、poems.csv 存诗作、sentences.csv 存名句。第一行是字段名,后续每行是一条记录,编码务必是 UTF-8。

导入的第一步是创建约束。约束有两个作用:一是保证节点唯一,二是加速后续 MATCH 查询。没有约束时,重复执行导入脚本会生成大量重复节点,这是知识图谱项目最常见的脏数据来源。

CREATE CONSTRAINT author_name IF NOT EXISTS FOR (a:Author) REQUIRE a.name IS UNIQUE; CREATE CONSTRAINT poem_title IF NOT EXISTS FOR (p:Poem) REQUIRE p.title IS UNIQUE;

语法要点是IF NOT EXISTS,防止重复执行时报错。REQUIRE ... IS UNIQUE是 Neo4j 4.x 的写法,早期版本用的是ASSERT ... IS UNIQUE,如果你的 Neo4j 版本不同,这里可能要改。运行完可以执行SHOW CONSTRAINTS确认约束是否生效。

第二步导入诗人数据。poets.csv 的结构类似 id,name,alias,dynasty 四列,用 MERGE 而不是 CREATE,是因为 MERGE 会先查重,配合刚才建的唯一约束,重复执行不会产生重复节点。

LOAD CSV WITH HEADERS FROM 'file:///poets.csv' AS row MERGE (a:Author {name: trim(row.name)}) SET a.alias = row.alias, a.dynasty = trim(row.dynasty);

第三步入诗作并建立关系。poems.csv 里有一个 author_name 字段,用来匹配已经存在的 Author 节点。匹配不到时,这条诗作会被跳过,所以这一步之前一定要保证诗人表先导入完成。

LOAD CSV WITH HEADERS FROM 'file:///poems.csv' AS row MATCH (a:Author {name: trim(row.author_name)}) MERGE (p:Poem {title: trim(row.title)}) SET p.content = row.content MERGE (a)-[:write]->(p);

逻辑顺序很重要:先 MATCH 作者,再 MERGE 诗作,最后 MERGE 关系。trim函数用于清理中文 CSV 里常见的首尾空格和换行符,我见过因为一行数据末尾带\r导致匹配失败的案例,加 trim 能省掉这类问题。诗作重名的情况,比如李商隐的《无题》有多首,只用 title 做唯一键会冲突,实际项目中要加上 author_name 组成复合键,这里为了展示基础流程用了简化写法。

3.3 数据清洗与去重:导入前的三个必须处理项

古诗词公开数据集的原始质量参差不齐,常见问题有三个:繁体字与简体字混用、全角半角不统一、同名诗作不同作者。直接导入会导致图谱出现“诗”和“詩”两个节点,用户问“诗中带月字”时只命中一半结果。

我一般会在导入前用 Python 做一次清洗,处理完再生成 CSV。核心代码可以这么写。

import csv import unicodedata import re def clean_text(text): text = unicodedata.normalize("NFKC", text) text = text.replace("\u3000", "").strip() text = re.sub(r"\s+", " ", text) return text def dedup_poems(rows): seen = {} for row in rows: key = (clean_text(row["title"]), clean_text(row["author"])) if key not in seen: seen[key] = row else: if len(row.get("content", "")) > len(seen[key].get("content", "")): seen[key] = row return list(seen.values())

clean_text里NFKC标准化能把全角字母数字转成半角,\u3000是全角空格,直接删除。dedup_poems用“标题 + 作者”作为去重键,当同一作者有同名诗时,保留正文更长的版本。这一步虽然简单,但能让图谱的节点数量稳定下来,避免知识图谱里出现大量冗余实体。

清洗完的数据再交给 LOAD CSV,导入时就很少遇到匹配不到的情况。我在第一次做这个项目时没做清洗,直接导入了网上找的语料,结果图里出现两个“李白”,其中一个名字后面带了个不可见字符,问答系统死活查不到数据,排查了很久才定位到问题。所以清洗这一步,建议放到所有导入操作的最前面。

4. 源码本地运行:Python 环境、Neo4j 连接参数与问答请求的调试验证

4.1 环境依赖:版本选择与安装命令

这份资源解压后,进入项目的根目录,先别急着运行,把环境搭对能省掉一半的报错。Python 版本建议控制在 3.8 到 3.10 之间,太新的版本可能会遇到依赖库兼容问题。Neo4j 建议使用 Community 版 4.4,这是目前毕设源码里兼容性最好的版本。

python -m venv venv source venv/bin/activate pip install flask py2neo

虚拟环境隔离依赖,避免把项目装进全局 Python。第二步是激活虚拟环境,Windows 下要换成venv\Scripts\activate。第三步安装两个核心库:Flask 是 Web 框架,负责提供前端页面和 /api/ask 接口;py2neo 是 Neo4j 的 Python 驱动,负责执行 Cypher。

库的版本要和 Neo4j 匹配。py2neo 目前常见的可用版本是 2021.2.3,如果你在代码里看到Graph("bolt://localhost:7687", auth=("neo4j", "password"))这种写法,说明用的就是 py2neo。如果源码用的是官方驱动 neo4j-driver,那连接方式会是GraphDatabase.driver(...),两者 API 差别很大,确认清楚再装依赖。

4.2 配置连接:bolt 地址、账户密码与端口

项目里通常有一个配置文件,可能是 config.py,也可能是 config.yaml,用来集中管理 Neo4j 连接参数。打开后找到下面这一段,参数值按本机环境改。

class Config: NEO4J_URI = "bolt://localhost:7687" NEO4J_USER = "neo4j" NEO4J_PASSWORD = "123456" NEO4J_DATABASE = "neo4j" FLASK_HOST = "127.0.0.1" FLASK_PORT = 5000

参数含义逐一说清:NEO4J_URI是数据库的 Bolt 协议地址,端口默认 7687,这是驱动连接用的端口,不是浏览器访问的 7474;NEO4J_USER默认是 neo4j,安装时创建;NEO4J_PASSWORD是你在浏览器里初始化时修改的密码,不是安装时的初始密码;NEO4J_DATABASE在 Neo4j 4.x 后支持多数据库,默认是 neo4j;FLASK_HOST设为 127.0.0.1 只允许本机访问,设成 0.0.0.0 则允许局域网访问,答辩演示时可以用后者,但纯本地调试不建议开。

改完配置先确认 Neo4j 服务已启动。浏览器访问 http://localhost:7474,能看到登录界面,说明服务正常。如果打不开,回到 Neo4j 安装目录,用命令前台启动看看日志输出。

4.3 跑通一条问答请求:从启动到查库的完整链路

环境就绪后,按顺序执行导入、启动、提问、验证四步。首先进入 Neo4j 浏览器,把第 3 章的 LOAD CSV 脚本执行一遍,确认图里有节点数据。然后启动 Web 服务。

python app.py

如果源码入口文件名不是 app.py,解压后看一眼目录下哪个文件里包含@app.route("/api/ask"),那个就是入口。启动成功的标志是终端出现Running on http://127.0.0.1:5000。此时用浏览器打开 http://127.0.0.1:5000,应该能看到聊天界面。

在对话框输入“李白写过哪些诗”,正常情况前端会返回诗作列表。如果结果不理想,我有两个验证手段:一是观察终端日志,看后端打印的查询语句和返回条数;二是直接在 Neo4j 浏览器里手工执行 Cypher。

curl -X POST http://127.0.0.1:5000/api/ask \ -H "Content-Type: application/json" \ -d '{"question": "李白写过哪些诗"}'

用 curl 绕过前端直接测接口,能快速区分问题是出在前端还是后端。返回的 JSON 里code: 0表示成功,result数组里能看到查询结果。如果 curl 正常但页面无显示,那就是前端 JavaScript 解析问题,去浏览器开发者工具的 Console 面板看报错信息即可。

5. 问答系统调试避坑:五个高频问题与一套通用排查思路

5.1 密码正确却报 Unauthorized:Neo4j 初始密码机制

现象:项目配置文件里写的密码在浏览器能登录,但 Python 端连接时报Unauthorized或Authentication required。

原因:Neo4j 4.x 之后有强制改密机制。首次登录用的密码是安装时设置的,如果你后来在浏览器里改过密码,config.py 里那个旧密码就失效了。另一个常见场景是源码压缩包是作者本人用的,里面密码是作者本机的初始密码,你拿来用当然对不上。

解决:先在浏览器执行SHOW USERS查看当前用户状态,再用新密码同步到 config.py。改完重启 Flask 进程,注意不是刷新页面,是重新执行python app.py。连接类报错,优先怀疑账密不同步,不要先怀疑代码逻辑。

5.2 LOAD CSV 执行成功却查不到数据:文件路径与中文字符编码

现象:LOAD CSV 执行完显示Completed after N ms,但MATCH (a:Author) RETURN count(a)查出来是 0。另一种情况是节点存在,但输入“李白”时搜不到结果。

原因:文件没有放到 Neo4j 的 import 目录,或 CSV 里有 BOM 头导致第一列字段名变成带前缀的乱码。LOAD CSV 遇到这两类问题通常不会报错,只是加载了空数据或错误字段名。

解决:确认文件路径是file:///poets.csv而不是file:///E:/data/poets.csv,只写文件名,文件必须物理放在 import 目录下。CSV 另存为时选择 UTF-8 无 BOM 格式。导入后第一时间在浏览器执行MATCH (a:Author) RETURN a LIMIT 5,可视化确认图里有节点,再继续往下调试。

5.3 页面 CSS 全部失效:静态资源路径 404

现象:页面能打开但完全是裸 HTML,浏览器 Console 里一排 404,报错的正是 bootstrap.min.css、nifty.min.css 这些静态文件。

原因:Flask 默认把静态资源放在 static 目录,页面模板引入 CSS 时如果写了绝对路径,比如href="/css/bootstrap.min.css",而项目实际结构是static/css/bootstrap.min.css,Flask 就找不到。

解决:模板里统一使用url_for('static', filename='css/bootstrap.min.css'),这是 Flask 官方的资源路径生成方式。压缩包里如果混用了绝对路径,用编辑器的全局搜索把/css/、/fonts/这类硬编码替换掉。改完重启并强制刷新浏览器,快捷键 Ctrl+F5 防止缓存干扰判断。

5.4 接口返回正常 but answer 是“抱歉无法回答”:词典覆盖不足

现象:curl 测试能拿到 JSON,code是 0,但answer提示无法回答,或者返回结果明显是错的。

原因:这个报错不是程序运行异常,而是意图识别或实体抽取环节没有命中。比如用户问“静夜思的作者是谁”,实体词典里只有“李白”这类诗人名,没有收录“静夜思”这个诗名,实体抽取就返回空,后续查询模板自然无从构造。

解决:把实体词典改成数据驱动,启动时从 Neo4j 里动态加载诗人表和诗作表,而不是写死在代码里。可以加一条初始化语句,把图里所有 Author 的 name 属性加载进内存,诗作标题同理。这样做的好处是,未来换宋词语料库时,不需要改代码,导入新数据就能自动支持新实体。

5.5 通用排查思路:先看日志和网络面板,再动代码

我调试这套系统时,有个固定流程:先看后端日志,再看浏览器 Network 面板,最后才看代码。终端里启动时加上日志输出,Flask 的 debug 模式会打印每次请求的路径和处理结果。

python app.py --debug

浏览器按 F12 打开开发者工具,切到 Network 面板,重新发起一次提问。如果网络列表里没有请求,说明前端按钮事件没绑定;如果有请求但返回 500,问题在后端异常;返回 200 但内容不对,那就是查询构造或答案渲染的问题。这套流程能把“看起来全对但就是不对”的问题,快速定位到具体环节。从那以后我每次调试毕设源码,都强制自己先走一遍这个流程,省下了不少瞎试的时间。

6. 二次开发与验证技巧:把古诗词换成宋词需要动源码的三个核心点

如果不想和同学撞题,一个有效的课设改动方向是把唐诗语料换成宋词语料。代码改动量不大,但有三处必须动到,否则系统会照旧按“诗”的逻辑去理解“词”。

第一处是数据文件。宋词数据集整理成同样的 CSV 格式,字段名尽量对齐:作者、词牌名、正文、朝代。LOAD CSV 脚本基本不用改,只需要把文件名替换掉。特别注意词牌名要去重,同一个词牌下可能有几十首词,导入时复合键要用“词牌名 + 作者”而不是只用词牌名。

第二处是查询模板。宋词作品节点的类型可以和诗作区分开,或者在 write 关系上保留作品类型属性。

MATCH (a:Author {name:$name})-[:write]->(p) WHERE p.type IS NULL OR p.type = "词" RETURN p.title, p.content

这段 Cypher 里p.type IS NULL是为了兼容原系统的诗作节点,它们没有 type 属性;新增的词作节点会标上 type="词",查询时通过 WHERE 条件过滤。用这种写法,老数据和新数据可以在同一张图里共存,不需要重建数据库。

第三处是意图关键词。宋词的提问方式不太一样,用户更习惯说“苏轼写过哪些词”“这首词的词牌名是什么”。需要把“词”“词牌”“宋词”加进意图分类的关键词列表,同时实体抽取的词典要加入常见词牌名,比如“水调歌头”“念奴娇”“菩萨蛮”。注意词牌名匹配时要用最长优先策略,避免“菩萨蛮”被拆成“菩萨”。

价值最高的技巧是我习惯性的最小数据验证法:不要一上来就全量导入几千首词。先从数据源里抽三首词,导入生成一张微型图,然后依次问“作者是谁”“写过哪些词”“词牌名是什么”三个问题。三道题的答案都对,再跑全量导入,并用 Cypher 做前后节点数对比。

MATCH (a:Author) RETURN count(a); MATCH (p) WHERE p.type = "词" RETURN count(p);

从复现别人的源码,到把源码改成自己的课设,边界就在这些地方。从那以后,我每次拿到陌生的毕设源码,都会先跑通一个最小数据再决定要不要全量导入,这个习惯帮我避开了很多“看着全对、一跑就废”的坑。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:29:28

语音短时时域分析:短时能量、过零率、自相关与AMDF详解

/* 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 1:28:48

EMC设计前移:从原理图到量产的可量化风险评估方法

/* 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 1:28:42

Python递归解迷宫:turtle可视化深度优先搜索与回溯

/* 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 1:28:24

深度学习验证码识别:CNN多标签分类实战

/* 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 1:28:00

OSG 3.6集成Qt 5.13:嵌入式三维渲染窗口的完整实现与踩坑记录

/* 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 1:27:58

OpenCV Rect核心用法与避坑指南:从几何语义到ROI裁剪实战

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

作者头像 李华