news 2026/9/15 23:19:37

基于Django与ECharts的考研院校推荐系统:爬虫、可视化与算法实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Django与ECharts的考研院校推荐系统:爬虫、可视化与算法实践

这套系统我做了将近两个月,中间踩了不少坑,也重构了好几版。趁着刚答辩完,把整个实现过程整理出来,给正在做类似选题的同学一个参考。项目本身是典型的Web全栈作品:爬虫负责数据采集,Django提供后端服务和页面渲染,ECharts做可视化展示,再配合一套基于规则和简单算法的推荐逻辑。技术上不算特别深,但胜在链路完整,从数据获取到最终呈现,每层都能在答辩时讲出东西来。

先说清楚这套系统到底解决什么问题。考研选校选专业,最核心的是对比院校的分数线、报录比、招生人数这些硬数据。但网上信息分散在各个渠道:学校官网、考研论坛、各类资讯站,手动整理非常痛苦。系统的核心链路是:爬虫定时从目标站点采集数据,清洗后存MySQL,Django提供查询接口和后台管理,前端用ECharts生成可视化图表,再结合用户输入的成绩、地区偏好、专业方向,给出推荐院校列表。数据是实时更新的,不是静态页面,这是它区别于普通展示型毕设的关键点。

适合谁来参考?基础层面,如果你正在准备Django方向的毕设,或者想做爬虫加可视化的组合,这套东西可以直接抄作业。进阶层面,如果你想理解一个完整的、带数据采集和推荐逻辑的系统怎么设计,同样有参考价值。下面按模块拆开讲。

1. 项目整体设计与系统架构

1.1 核心需求拆解:这不是一个简单的增删改查系统

很多人的毕设止步于用户管理加几张图表,答辩时老师一问数据从哪来就卡壳。这套系统的价值恰恰在于把“数据”这个环节做实了。需求拆解下来有四个核心块:

第一块是数据采集,需要爬取考研相关的院校信息、专业目录、历年分数线、报录比等结构化数据。第二块是数据存储与管理,采集到的数据要清洗、去重、入库,还要能通过后台维护。第三块是可视化展示,把数据库里的冷数据变成图表,让用户直观看到分数线趋势、院校分布、专业热度这些信息。第四块是推荐逻辑,根据用户输入的意向地区、专业门类、预估分数,输出合理的院校建议。

这个拆解决定了整个项目的技术选型。爬虫端我用了requests加BeautifulSoup,原因后面细说。后端Django 3.2加MySQL存储,缓存用Redis。可视化没有自研组件,直接上ECharts,它是目前生态最好的前端图表库,地图、折线、柱状、词云都有现成方案。

1.2 为什么是Django而不是Flask或FastAPI

选Django就一个核心原因:自带后台管理。毕设答辩时,“系统管理功能完善”是很重要的评分项。Django原生的admin站点在注册模型后可以直接进行增删改查,不需要额外编写管理页面。我用Django admin做了院校信息、专业目录、分数线数据的管理入口,再用simpleui插件做了一层界面美化,整个后台看起来像模像样。

另一个考量是ORM的成熟度。Django的ORM是自带的,不需要额外引入SQLAlchemy。对于考研推荐系统这种多表关联查询比较多的场景,ORM的效率优势很明显。比如筛选符合条件的院校,条件涉及地区、专业、历年分数线,用ORM的链式查询写起来非常直观。当然,如果你对性能有极致要求,FastAPI加异步数据库驱动会更好,但对于毕设这个体量,Django的成熟稳定才是最重要的。

1.3 爬虫选型:requests加BS4,为什么不直接上Scrapy

Scrapy是爬虫框架里的重武器,支持分布式、中间件、Item Pipeline,功能很强大。但我最终选择了requests加BeautifulSoup,而且这个选择在答辩时帮我挡了好几个问题。

核心原因有三点。第一,考研数据站的页面结构相对规整,没有复杂的渲染逻辑,不需要Selenium这类浏览器自动化工具。第二,requests的并发控制更直观,配合ThreadPoolExecutor可以精确控制线程数,反爬压力小。第三,最重要的,Scrapy有自己独立的运行机制,和Django的数据模型天然隔离。如果要打通,需要做信号回调或者通过ORM直接操作,复杂度会上一个台阶。requests方案里,直接把解析结果用Django ORM入库,逻辑链路最短。

需要说明的是,采集目标选择了公开的、允许抓取的考研资讯站点,模拟浏览器UA降低访问压力,控制请求频率,没有破解任何反爬机制。这也是毕设项目应该有的边界意识。

2. 爬虫采集模块的详细实现

2.1 采集目标与数据字段设计

爬虫不是上来就写的,第一步要把数据字段定清楚。我最后定下来的核心数据模型有七个字段组:院校基础信息(学校名称、院校代码、所在省份、城市、办学层次、是否985/211/双一流)、专业目录(专业代码、专业名称、所属门类)、招生数据(年份、专业、招生人数、报考人数、录取人数)、分数线数据(年份、专业、复试线、录取最低分、录取平均分)、报录比(由报考人数除录取人数直接计算)、学费和奖助政策(简化学费字段)、院校简介(爬取概况页面生成摘要)。

字段设计这一步非常关键,直接决定后面可视化图表能画什么。比如你在第一步漏了“所在省份”这个字段,后面想做全国院校分布地图就得回头重爬。我的建议是宁可多采集几个字段,先用通用字段模板扩充,后面用不到可以舍弃。

2.2 单线程爬虫框架:写一个可复用的基础模块

这里给出核心代码结构,单线程版本是理解整个流程的基础。

import requests from bs4 import BeautifulSoup import time from random import uniform class BaseSpider: def __init__(self): self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' }) self.base_url = 'https://example-data-site.com' def fetch(self, url, retry=3): """带重试机制的请求方法""" for i in range(retry): try: resp = self.session.get(url, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except Exception as e: print(f'请求失败: {url}, 第{i+1}次重试, 错误: {e}') time.sleep(uniform(2, 4)) return None def parse(self, html): """解析页面,子类实现""" raise NotImplementedError def run(self): """入口方法""" html = self.fetch(self.base_url) if html: self.parse(html)

几个细节说明。resp.encoding = resp.apparent_encoding这一步非常关键,很多网站页面声明的是UTF-8,实际内容却是GBK编码,不强行重置会乱码。requests.Session重用连接,减少TCP握手开销。uniform(2, 4)随机延时区间,避免请求节奏过于规律。

2.3 并发采集改造:用ThreadPoolExecutor提升效率

单线程爬1000个页面,假设每页3秒,要50分钟。用并发可以将时间压缩到十分之一。我在详情页采集中用了concurrent.futures.ThreadPoolExecutor,最大线程数设置为8。

from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_detail_pages(self, detail_urls): results = [] with ThreadPoolExecutor(max_workers=8) as executor: future_to_url = {executor.submit(self.fetch_detail, url): url for url in detail_urls} for future in as_completed(future_to_url): url = future_to_url[future] try: data = future.result() if data: results.append(data) except Exception as e: print(f'采集详情页失败: {url}, 错误: {e}') return results

这里有个很关键的权衡。线程数调到16甚至32行不行?理论上行,但实际执行的时候,过高的并发会造成请求频率过快,触发网站的反爬机制,轻则IP被临时封禁,重则需要换代理。8个线程配合2到4秒的随机延时,是一个既能保证速度又相对安全的区间。

2.4 数据清洗:入库前的必要处理

爬下来的数据不能直接用,必须做清洗。我遇到最多的问题包括:字符串两端的空格和换行符、HTML实体转义、数字字段里混入“约”“左右”等中文描述,以及数据去重。

import re def clean_data(raw): """清洗爬取的数据""" cleaned = {} for key, value in raw.items(): if isinstance(value, str): # 去除HTML标签 value = re.sub(r'<[^>]+>', '', value) # 去除不可见字符 value = value.strip() value = re.sub(r'\s+', ' ', value) # 数字字段提取纯数字 if key in ['recruit_num', 'apply_num', 'admit_num']: digit_match = re.search(r'\d+', value) value = int(digit_match.group()) if digit_match else 0 cleaned[key] = value return cleaned

数字提取这里特别容易踩坑。比如“招生人数:约 120人(含推免)”,直接转int会报错,用正则提取首个数字串就安全。对于这种含推免的复杂情况,入库时还需要标记招生类型,否则推荐结果会误导用户。

2.5 去重策略与增量爬取

考研数据每年都会更新,完整爬虫需要支持增量采集。我在院校表上建了唯一索引,以院校代码 + 年份 + 专业代码作为联合唯一约束。入库时用get_or_create或者update_or_create,这样重复执行不会产生脏数据。

from apps.school.models import RecruitData obj, created = RecruitData.objects.update_or_create( school_code=data['school_code'], year=data['year'], major_code=data['major_code'], defaults={ 'recruit_num': data['recruit_num'], 'apply_num': data['apply_num'], 'admit_num': data['admit_num'], 'recurve_score': data['recurve_score'], 'avg_score': data['avg_score'], } )

3. Django后端与可视化实现

3.1 数据模型设计:数据库不只是存数据,还要为查询性能服务

Django的ORM模型设计直接决定接口层代码的复杂度。考研推荐系统的表结构我分成六个核心表,分别为用户表、院校表、专业目录表、招生数据表、分数线表、用户收藏表。

其中院校表和专业目录表是多对多关系,一张专业目录可以对应多个院校,一个院校也有多个专业。推荐筛选时最频繁的查询是从招生数据表出发,先关联院校表过滤地区,关联专业表过滤专业方向,再用分数条件过滤。这三层关联如果要避免N+1查询,必须在查询时用select_relatedprefetch_related做联表预取。

class School(models.Model): code = models.CharField(max_length=20, unique=True, verbose_name='院校代码') name = models.CharField(max_length=100, verbose_name='院校名称') province = models.CharField(max_length=50, verbose_name='所在省份') city = models.CharField(max_length=50, verbose_name='所在城市') level = models.CharField(max_length=20, choices=LEVEL_CHOICES, verbose_name='办学层次') is_985 = models.BooleanField(default=False) is_211 = models.BooleanField(default=False) is_double = models.BooleanField(default=False) class Meta: db_table = 'school' class RecruitData(models.Model): school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='recruit_data') major = models.ForeignKey(Major, on_delete=models.CASCADE, related_name='recruit_data') year = models.IntegerField(verbose_name='年份') recruit_num = models.IntegerField(default=0, verbose_name='招生人数') apply_num = models.IntegerField(default=0, verbose_name='报考人数') admit_num = models.IntegerField(default=0, verbose_name='录取人数') class Meta: db_table = 'recruit_data' unique_together = [('school', 'major', 'year')]

在Django admin中注册后,后台就能直接管理数据。这里推荐使用simpleui美化后台界面,具体安装方式在requirements.txt里加一行simpleui即可。

3.2 数据可视化接口与ECharts联动

后端只提供JSON接口,前端页面通过Ajax拉取数据后渲染图表。这种方式解耦了前后端,也让答辩时能分块讲清楚。我封装了几个主要的接口:

  • /api/score_trend/?school=xx&major=xx:返回某个学校某个专业近5年复试线与录取最低分数据
  • /api/school_distribution/?province=xx:返回全国院校分布热力数据,用于地图展示
  • /api/major_hot/?top=20:返回专业热度排名,按报考人数排序
  • /api/recommend/?province=xx&major_category=xx&score=xx:返回推荐院校列表

以分数线趋势图为例,接口返回的数据结构是:

{ "school": "xxx大学", "major": "计算机科学与技术", "trend": [ {"year": 2019, "recurve_score": 310, "min_score": 320, "avg_score": 345}, {"year": 2020, "recurve_score": 320, "min_score": 330, "avg_score": 355}, {"year": 2021, "recurve_score": 315, "min_score": 328, "avg_score": 348} ] }

前端拿到数据后,用ECharts折线图渲染,三根线分别表示复试线、录取最低分、录取平均分。图表的tooltip可以看到具体年份的悬停详情。

3.3 核心图表实现:地图、趋势、词云

项目里最有视觉冲击力的是全国院校分布地图,用ECharts的map类型实现。需要中国地图的GeoJSON数据,可以从公开渠道获取各省份坐标数据,再用ECharts的registerMap方法注册。

// 地图配置核心代码 $.getJSON('/static/geo/china.json', function(chinaJson) { echarts.registerMap('china', chinaJson); var chart = echarts.init(document.getElementById('mapChart')); chart.setOption({ title: { text: '全国考研院校分布' }, tooltip: { trigger: 'item' }, visualMap: { min: 0, max: 200, left: 'left', top: 'bottom', text: ['高', '低'], inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } }, series: [{ name: '院校数量', type: 'map', map: 'china', roam: true, data: schoolDistData }] }); });

词云部分用ECharts的词云扩展包,展示了考研热门专业的词汇频率。专业名称在数据库里出现频次越高,词云里显示字号就越大。这个图表能从侧面反映专业热度,答辩时用来引出推荐逻辑很自然。

3.4 推荐算法:基于多条件加权排序的改进方案

推荐模块是整个系统的灵魂。推荐算法的实现不能太简单,也不能复杂到无法在答辩时讲清楚。我用的是“多条件加权评分法”,核心是四个维度:分数适配度(占40%)、地区偏好(占25%)、院校层次(占20%)、招录比竞争度(占15%)。

分数适配度具体算法是:如果用户预估分比该专业录取平均分高15分及以上,得分是100分;高5到15分,得分是75分;在平均分上下5分区间内,得分是60分;低于平均分超过5分,则直接过滤掉,不推荐。地区偏好和院校层次的评分逻辑是布尔判断,命中就加分。招录比竞争度则通过分位数计算,报录比低于3:1的得分高,高于10:1的得分显著降低。

def calculate_score(school, major, user_pref): score = 0 # 分数适配度 diff = user_pref.estimated_score - major.avg_score if diff >= 15: score += 40 elif diff >= 5: score += 30 elif diff >= -5: score += 20 else: return None # 分数差距过大,过滤 # 地区偏好 if school.province == user_pref.province: score += 25 # 院校层次 if user_pref.pref_level == '211' and school.is_211: score += 20 # 报录比竞争度 if major.apply_num > 0: ratio = major.apply_num / major.admit_num if ratio < 3: score += 15 elif ratio < 6: score += 10 elif ratio < 10: score += 5 return score

推荐结果用排行榜形式展示,分数从高到低排序。这个算法的可解释性非常好,答辩时老师问你推荐依据是什么,你可以明确说出每一项打分规则。

4. Redis缓存与系统性能优化

4.1 为什么需要Redis:不只是缓存,更是并发防护

考研推荐系统上线后,最怕的是热点数据重复查库。首页的院校分布地图,每次请求都要聚合几百条数据,没有缓存的话并发上来数据库就危险。我用Redis做了两级缓存:热点列表缓存和查询结果缓存。

热点列表缓存针对下拉框数据,比如省份列表、专业门类列表,这类数据基本不变,缓存24小时。查询结果缓存针对推荐接口,根据查询条件计算MD5作为key,命中直接返回,没有命中再查库、计算、回填缓存。缓存时间设置了10分钟,因为招生数据偶尔会有后台调整,时间太长用户会看到过时信息。

import redis import hashlib import json r = redis.Redis(host='127.0.0.1', port=6379, db=0) def get_recommend_with_cache(user_pref): cache_key = hashlib.md5( json.dumps(user_pref, sort_keys=True).encode() ).hexdigest() cached = r.get(f'recommend:{cache_key}') if cached: return json.loads(cached) result = calculate_recommend(user_pref) r.setex(f'recommend:{cache_key}', 600, json.dumps(result, ensure_ascii=False)) return result

4.2 数据库查询优化实战

Django ORM的性能问题主要在N+1查询,我查列表页时已经用select_related预取了学校信息,但对于招生数据这种多对多表,还需要prefetch_related

recruit_data = RecruitData.objects.filter( year=2023, major__name__icontains='计算机' ).select_related('school', 'major').prefetch_related('school__school_tags') # 上面的查询完成后,访问每一条recruit_data.school时不会再次查库

另外一个容易忽视的点是分页。推荐结果列表如果不分页,一次性返回所有数据,页面渲染会非常卡。我用了Django内置的Paginator,一页10条数据,前端加滚动分页加载,体验会好很多。

4.3 Redis可视化的必要性

Redis装好之后,没有可视化工具很难直观看到缓存存储情况。这里推荐用RedisInsight或者Another Redis Desktop Manager。用RedisInsight连上之后,可以清楚看到key有哪些、过期时间剩多少、内存占用多少。在毕设答辩时,打开RedisInsight展示缓存命中效果的截图,会让你在系统性能这块的说明更有说服力。

5. 部署上线与常见问题排查

5.1 云服务器部署:宝塔面板加gunicorn加nginx

系统部署我用的是常见三板斧:宝塔面板做服务器环境管理,gunicorn启动Django应用,nginx反向代理。之所以用宝塔,是因为它对Python项目的支持比较完善,安装Python版本、配置MySQL、管理文件都很方便。

部署流程基本是:云服务器安装宝塔,通过面板安装Python 3.10加MySQL 5.7;用宝塔的Python项目管理器创建项目站点,指定项目路径和Python解释器;安装依赖依赖后,用gunicorn绑定本地8080端口启动;nginx配置反向代理,把80端口流量转发到8080。

这里有一个多年踩坑得出的经验:Django项目部署时,静态文件的配置非常容易出错。需要在settings.py里设置STATIC_ROOT,然后执行python manage.py collectstatic收集所有静态文件到指定目录,再在nginx里配置这个目录的访问路径。

5.2 部署必踩的坑:mysqlclient和数据库连接

整个部署过程中最常见的问题是mysqlclient安装失败。在云服务器上编译这个包需要mysql开发头文件,宝塔面板环境默认没有。解决办法就是用pip install mysqlclient之前,先安装libmysqlclient-dev依赖。具体命令取决于系统类型,用apt或者yum安装好之后再装包就顺利了。

还有一个坑是Django连接MySQL时的中文字符集问题。需要在数据库连接配置里明确指定字符集编码为utf8mb4,否则你在本地开发时数据正常,部署到服务器后,写入的中文可能会变问号。utf8mb4相比utf8能存储emoji,这是行业推荐标准。

5.3 爬虫采集遇到的常见问题

采集过程中的问题主要集中在三个方面:编码问题、反爬限制、字段缺失。

编码问题前面提过,强制用apparent_encoding解决。反爬限制出现时,典型表现就是请求响应403或者返回一段验证脚本。我的处理方式是维护一个UA池,随机切换User-Agent,同时在两次请求之间加延时。如果封禁是IP级别的,可以暂时停采,把采集任务放到凌晨执行。

字段缺失的问题在于不同网站的数据口径不一致。比如同一所学校的招生人数,一个站点写的是含推免总数,另一个站点写的是统考名额。我的处理方式是在数据清洗时标记数据来源和口径,推荐评分时优先采用统考口径数据。如果某一个关键字段缺失,这条数据宁可标记为不完整,也不让它参与推荐计算,否则会影响推荐结果的准确性。

5.4 常见问题速查表

问题现象可能原因排查思路
页面中文乱码编码解析错误检查resp.encoding是否已重置为apparent_encoding
数据重复入库缺少去重逻辑检查unique_together约束,改用update_or_create
推荐结果为空分数过滤条件过严放宽分数差值阈值,检查数据表是否有数据
后台管理页面样式异常Django版本与simpleui版本不兼容检查版本对应关系,升级或降级simpleui
部署后静态文件404nginx未配置静态目录确认STATIC_ROOT已设置,collectstatic已执行
爬虫请求超时网络问题或请求频率过高降低线程数,增大超时时间,加入重试机制
Redis连接失败Redis服务未启动或端口未开放检查redis-server进程,测试本地6379端口连通性

5.5 如何用日志定位问题

开发调试最基础的是print加浏览器控制台。部署到服务器之后就不一样了,日志变得尤为重要。我在Django里的日志配置分了两档,开发环境下日志输出到控制台方便调试,生产环境输出到指定日志文件,利用RotatingFileHandler做按大小轮转。爬虫模块单独建了采集日志,记录每次请求的URL、状态码和耗时信息。这个习惯在答辩展示时很有用,直接打开日志文件展示数据采集量、耗时曲线,比你口头描述要强得多。

6. 从毕设到实际应用的思考

做完这套系统,最大的体会是毕设和技术探索之间有本质区别。毕设最重要的是逻辑闭环:数据从哪来、存在哪里、怎么展示、如何应用,每一步都要能讲清楚。比如推荐算法,我的方法只用了加权评分,没有用机器学习。答辩时老师问为什么不用协同过滤,我直接说协同过滤需要大量用户行为数据来训练,而考研场景的用户数据样本量根本不足以支撑,所以基于规则的方案在这个业务场景下是最实用的。

但并不意味着可以用粗糙糊弄过去。数据采集的完整性、可视化图表的丰富度、推荐结果的可解释性,这三个点做好了,系统在同类毕设中肯定是中上水平。爬虫采集了上千条考研数据,覆盖几百所院校,可视化展示了院校分布、分数线趋势、专业热度三类图表,推荐模块实现了多条件加权排序,这些功能点都体现在最终的演示文档里。

如果后续想继续扩展,可以做的方向有很多:数据层面,接入更多数据源,保持数据时效性;算法层面,将多条件加权评分升级为机器学习模型,用历年录取数据训练录取概率预测模型;业务层面,增加用户行为采集,构建用户画像,提供更个性化的推荐。这些扩展点都是在现有架构上自然演进的,不会推翻重来。

最后提醒一句:如果你用的是爬虫采集的数据,务必注意数据来源的合规性和公民个人信息保护,只采集公开可访问的客观事实数据,不涉及个人隐私信息和非公开接口。毕设阶段养好这个习惯,对之后的工程实践很有帮助。

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

爱普生L8058与L8168对比:ICC校色文件安装验证全指南

最近被问得最多的两台打印机&#xff0c;一个是爱普生L8058&#xff0c;一个是L8168。问的人基本都带着同一个问题&#xff1a;差价摆在那里&#xff0c;贵的到底值不值&#xff1f;我自己的答案是&#xff1a;如果只打彩色A4照片&#xff0c;L8058完全够用&#xff1b;如果你经…

作者头像 李华
网站建设 2026/9/15 23:15:09

从等任务到主动侦察:测试新人摆脱学生思维的进阶指南

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

作者头像 李华
网站建设 2026/9/15 23:14:16

游戏评测网站有哪些

游戏评测网站有哪些&#xff1f;找客观评价看这几个入口 游戏评测网站有哪些&#xff0c;随着近年来多款高宣发 3A 商业大作在发售时出现口碑崩盘、优化翻车&#xff0c;很多玩家越来越不敢轻易盲目预购。然而在信息获取上&#xff0c;玩家又经常被“先发评测特权被厂商绑架”、…

作者头像 李华
网站建设 2026/9/15 23:10:05

HTML网页制作入门:从零构建你的第一个页面

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

作者头像 李华
网站建设 2026/9/15 23:06:47

2T增益单元:破解AI芯片内存墙的高密度片上存储方案

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

作者头像 李华
网站建设 2026/9/15 23:04:20

MIM结构超表面全息复现:几何相位与FDTD仿真实战解析

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

作者头像 李华