news 2026/9/5 5:56:04

基于NLP与网络爬虫的序列化文本内容演化趋势分析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于NLP与网络爬虫的序列化文本内容演化趋势分析实战

这次我们来看一个关于“基金会”编号统计与内容演化的技术分析项目。这个项目的核心不是讨论虚构故事本身,而是聚焦于如何利用技术手段,对海量、非结构化的网络文本内容进行自动化抓取、解析、分类与趋势分析。它解决的核心问题是:当一个大型、持续更新的叙事体系(如“基金会”系列)内容量爆炸式增长,其编号体系与内容主题发生显著偏离时,如何高效、准确地量化这种变化,并洞察其演化规律。

对于技术开发者、数据分析师和内容研究者而言,这个项目的价值在于提供了一套可复用的方法论和潜在的工具链思路。它关注数据获取的可行性、处理流程的自动化、分析维度的设计以及结果的可视化。本文将重点拆解从目标设定、数据采集、文本处理、特征提取到趋势分析的全链路技术要点,并提供一套可供验证的实操流程。

1. 核心能力速览

能力项说明
分析对象大型、序列化、持续更新的网络文本集合(如“基金会”文档、系列小说、技术文档版本等)
核心目标量化编号体系与内容主题的偏离程度,分析内容演化趋势
关键技术栈网络爬虫、文本解析、自然语言处理(NLP)、时序分析、数据可视化
数据处理规模支持从数百到数万篇文档的批量处理
自动化程度可实现从数据采集到报告生成的全流程或半自动化
输出成果统计图表(编号分布、主题变迁时序图)、分析报告、关键偏离点列表
适合场景内容生态分析、社区文化研究、叙事结构量化、文档体系治理

2. 适用场景与使用边界

这个分析框架主要适用于以下几类人群和场景:

  • 社区研究者与数据分析师:希望量化特定网络文化现象(如“基金会”系列)的发展阶段、主题爆发期和内容转向。
  • 内容平台运营者:需要监控大型连载作品或用户生成内容(UGC)体系的健康度,识别内容同质化或偏离核心设定的趋势。
  • 技术文档维护者:管理版本化的技术文档时,需要分析各版本新增内容与原始架构的契合度。
  • 自然语言处理学习者:作为一个完整的项目实践,涵盖数据工程、文本挖掘和可视化展示。

使用边界与注意事项:

  1. 版权与合规:所有数据采集行为必须严格遵守目标网站的robots.txt协议,尊重内容版权。本文所述方法仅用于技术学习与合规范围内的分析,禁止用于商业爬取、侵犯著作权或对目标服务器造成负载压力。
  2. 数据代表性:分析结论严重依赖于数据源的完整性和准确性。如果数据采集不全,结论可能有偏差。
  3. 文本理解局限:基于当前NLP技术(如主题模型、关键词提取)的分析结果是对文本内容的数学化近似,无法完全替代人类的深度语义理解,解读时需结合背景知识。
  4. 伦理风险:分析对象涉及虚构内容时,应聚焦于公开的、已发布的文本特征,避免对创作本身或社区进行主观价值评判。

3. 环境准备与前置条件

进行此类分析,你需要一个具备基础编程和数据处理能力的环境。以下是通用的准备清单:

  • 操作系统:Windows 10/11, macOS, 或 Linux 发行版均可。Linux 环境在部署爬虫和后台任务时通常更便捷。
  • 编程语言Python 3.8+是首选,因其在数据科学和爬虫领域的丰富生态。
  • 核心Python库
    • 数据获取:requests(HTTP请求),BeautifulSoup4lxml(HTML解析),Scrapy(大型爬虫项目可选)。
    • 数据处理:pandas(数据分析),numpy(数值计算)。
    • 文本处理:jieba(中文分词,如分析中文内容),nltk/spaCy(英文分词与NLP),re(正则表达式)。
    • 自然语言处理:scikit-learn(用于TF-IDF、主题模型如LDA),gensim(主题模型),transformers(如需使用BERT等模型进行深度语义分析,需要更多计算资源)。
    • 可视化:matplotlib,seaborn,plotly
  • 开发环境:Jupyter Notebook (用于交互式分析) 或 PyCharm/VSCode 等IDE。
  • 硬件要求:常规配置即可。大规模文本处理或使用深度学习模型时,需要更大内存(建议16GB+)。GPU不是必须,但能加速transformers库的某些模型。
  • 存储空间:预留足够空间存储原始网页、清洗后的文本和中间数据,具体取决于目标数据量。

4. 分析流程设计与实施步骤

整个项目遵循数据流水线(Data Pipeline)思想,分为以下几个阶段。

4.1 阶段一:目标定义与数据源探查

在写任何代码之前,必须明确分析目标并探查数据源。

  1. 明确目标:例如,“分析‘基金会’文档中,编号001之后的条目,其主题关键词如何随时间(或编号递增)演变,并识别与‘起源故事’核心设定偏离的关键节点。”
  2. 识别数据源:确定目标文档所在的网站或仓库。通过浏览器开发者工具(F12)查看网页结构。
    • 找到列表页(列出所有001-xxx编号条目的页面)。
    • 查看单篇文档页面的URL规律、HTML结构。
    • 检查是否有反爬机制(如频率限制、登录要求、动态加载)。务必遵守robots.txt
  3. 设计数据模型:规划要提取的字段,例如:编号标题发布日期(如有)、正文内容作者标签等。

4.2 阶段二:数据采集与爬虫编写

编写爬虫系统性地抓取数据。这里给出一个基于requestsBeautifulSoup的简单示例框架。

步骤1:获取条目列表

import requests from bs4 import BeautifulSoup import pandas as pd import time def get_entry_list(list_url): """ 从列表页解析所有条目的链接和编号。 """ headers = {'User-Agent': 'Your-Custom-User-Agent (用于合规标识)'} try: resp = requests.get(list_url, headers=headers, timeout=10) resp.raise_for_status() # 检查请求是否成功 except requests.exceptions.RequestException as e: print(f"获取列表页失败: {e}") return [] soup = BeautifulSoup(resp.text, 'html.parser') entry_links = [] # 以下选择器需要根据实际网站结构调整 for link_tag in soup.select('div.entry-list a.entry-link'): href = link_tag.get('href') text = link_tag.get_text(strip=True) # 使用正则表达式从链接或文本中提取编号,例如‘SCP-001’ import re match = re.search(r'(SCP-|Item #?)(\d+)', text or href) if match: entry_id = match.group(2).zfill(3) # 补零为三位数,如‘001’ entry_links.append({'id': entry_id, 'url': href, 'title': text}) # 增加延迟,避免对服务器造成压力 time.sleep(1) return entry_links # 假设列表页URL list_url = "https://example-scp-wiki.org/list" all_entries = get_entry_list(list_url) print(f"共找到 {len(all_entries)} 个条目。")

步骤2:抓取单篇文档内容

def fetch_entry_detail(entry_info): """ 根据条目信息字典,抓取详情页并解析内容。 """ url = entry_info['url'] entry_id = entry_info['id'] print(f"正在抓取: {entry_id}") headers = {'User-Agent': 'Your-Custom-User-Agent'} try: resp = requests.get(url, headers=headers, timeout=15) resp.raise_for_status() except requests.exceptions.RequestException as e: print(f"抓取 {entry_id} 失败: {e}") return None soup = BeautifulSoup(resp.text, 'html.parser') # 解析标题和正文 - 选择器需适配目标网站 title_elem = soup.select_one('#page-title') title = title_elem.get_text(strip=True) if title_elem else '' content_elem = soup.select_one('#page-content') # 清理正文中的脚本、样式等无关元素 if content_elem: for tag in content_elem(['script', 'style', 'div.sidebar', 'table.license-box']): tag.decompose() content = content_elem.get_text(separator='\n', strip=True) else: content = '' entry_info['title'] = title entry_info['content'] = content entry_info['content_length'] = len(content) time.sleep(2) # 重要:请求间延时,体现友好爬取 return entry_info # 示例:抓取前5篇进行测试 test_data = [] for entry in all_entries[:5]: detail = fetch_entry_detail(entry) if detail: test_data.append(detail) # 转换为DataFrame查看 df_test = pd.DataFrame(test_data) print(df_test[['id', 'title', 'content_length']].head())

4.3 阶段三:数据清洗与预处理

原始文本包含大量噪声,需要清洗。

import re def clean_text(text): """ 基础文本清洗函数。 """ if not isinstance(text, str): return "" # 移除多余的换行和空白字符 text = re.sub(r'\s+', ' ', text) # 移除特定的广告、导航文本(根据网站特征定制) text = re.sub(r'^.*?(?=«|»|Navigation|Discussion)', '', text, flags=re.DOTALL) text = text.strip() return text # 应用清洗 df_test['content_clean'] = df_test['content'].apply(clean_text) # 分词(以中文为例,英文可用nltk.word_tokenize) import jieba def chinese_tokenize(text): words = jieba.lcut(text) # 移除停用词和单字 stopwords = set(['的', '了', '在', '是', '我', '有', '和', '就', ...]) # 加载停用词表 words = [w for w in words if w not in stopwords and len(w) > 1] return words df_test['tokens'] = df_test['content_clean'].apply(chinese_tokenize)

4.4 阶段四:特征提取与量化分析

这是核心步骤,旨在将文本转化为可度量的特征。

1. 关键词频次与TF-IDF分析

from sklearn.feature_extraction.text import TfidfVectorizer # 将清洗后的文本合并为列表 corpus = df_test['content_clean'].tolist() # 使用TF-IDF提取特征词 vectorizer = TfidfVectorizer(max_features=1000, stop_words='english') # 英文停用词 X = vectorizer.fit_transform(corpus) tfidf_df = pd.DataFrame(X.toarray(), columns=vectorizer.get_feature_names_out()) # 查看每个文档最重要的词 for i, row in tfidf_df.iterrows(): top_words = row.nlargest(5).index.tolist() print(f"文档 {df_test.iloc[i]['id']} 关键词: {top_words}")

2. 主题模型(LDA)探测内容分布

from sklearn.decomposition import LatentDirichletAllocation # 使用词袋模型 from sklearn.feature_extraction.text import CountVectorizer count_vectorizer = CountVectorizer(max_df=0.95, min_df=2, stop_words='english') X_counts = count_vectorizer.fit_transform(corpus) # 训练LDA模型,假设我们想找出5个主题 n_topics = 5 lda = LatentDirichletAllocation(n_components=n_topics, random_state=42) lda.fit(X_counts) # 查看每个主题下的关键词 def print_topics(model, feature_names, n_top_words): for topic_idx, topic in enumerate(model.components_): message = f"主题 #{topic_idx}: " message += ", ".join([feature_names[i] for i in topic.argsort()[:-n_top_words - 1:-1]]) print(message) print_topics(lda, count_vectorizer.get_feature_names_out(), 10) # 为每篇文档分配主要主题 topic_distribution = lda.transform(X_counts) df_test['dominant_topic'] = topic_distribution.argmax(axis=1)

4.5 阶段五:时序/序列趋势可视化

将分析结果按编号顺序(可近似视为时间序列)进行可视化。

import matplotlib.pyplot as plt import seaborn as sns # 假设df_full是包含所有条目的完整DataFrame,且已计算好‘dominant_topic’ df_full = pd.DataFrame(...) # 你的完整数据 # 1. 主题随编号的分布变化(堆叠面积图) topic_by_id = pd.crosstab(df_full['id'], df_full['dominant_topic'], normalize='index') topic_by_id.plot(kind='area', stacked=True, figsize=(15, 6)) plt.title('主题分布随条目编号的变化') plt.xlabel('条目编号') plt.ylabel('主题比例') plt.tight_layout() plt.show() # 2. 特定关键词出现频率的变化(例如,“异常”、“收容”、“故事”) def keyword_frequency_series(df, keyword): """计算每个条目中某个关键词的出现频率""" freq_series = df['content_clean'].apply(lambda x: x.lower().count(keyword.lower()) / (len(x.split()) + 1)) # 粗略的词频 return freq_series df_full['freq_anomaly'] = keyword_frequency_series(df_full, '异常') df_full['freq_narrative'] = keyword_frequency_series(df_full, '故事') plt.figure(figsize=(14, 5)) plt.plot(df_full['id'].astype(int), df_full['freq_anomaly'], label='“异常”词频', alpha=0.7) plt.plot(df_full['id'].astype(int), df_full['freq_narrative'], label='“故事”词频', alpha=0.7) plt.xlabel('条目编号') plt.ylabel('相对词频') plt.title('特定关键词词频随编号变化趋势') plt.legend() plt.grid(True, alpha=0.3) plt.show()

5. 深度分析:检测“偏离”与演化节点

简单的可视化可以展示趋势,但我们需要定义并量化“偏离”。

方法一:基于主题一致性的突变点检测计算相邻编号条目间主题分布的相似度(如余弦相似度),寻找相似度骤降的点。

from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 假设topic_probs是每个条目的主题概率向量矩阵 (n_entries, n_topics) topic_probs = lda.transform(X_counts_full) # 在完整数据集上训练LDA后得到 similarities = [] for i in range(1, len(topic_probs)): sim = cosine_similarity([topic_probs[i-1]], [topic_probs[i]])[0][0] similarities.append(sim) # 将相似度与编号关联 df_full['topic_sim_to_prev'] = [np.nan] + similarities # 第一个条目无前项 # 找出相似度低于阈值的“突变点” threshold = np.percentile(similarities, 10) # 例如,取最低的10%分位数作为阈值 potential_shift_points = df_full[df_full['topic_sim_to_prev'] < threshold][['id', 'title', 'topic_sim_to_prev']] print("可能的主题突变点(偏离点):") print(potential_shift_points.head(10))

方法二:基于文本向量与“起源”向量距离将早期条目(如001-010)的平均文本向量作为“起源”基准,计算后续每个条目与此基准的距离。

from sklearn.feature_extraction.text import TfidfVectorizer # 计算TF-IDF向量 vectorizer_full = TfidfVectorizer(max_features=2000) X_full_tfidf = vectorizer_full.fit_transform(df_full['content_clean']) # 定义“起源”基准向量(前N篇的平均) origin_indices = df_full[df_full['id'].astype(int) <= 10].index origin_vector = X_full_tfidf[origin_indices].mean(axis=0) # 平均向量 # 计算每个条目与起源向量的余弦距离 from scipy.spatial.distance import cosine distances = [] for i in range(X_full_tfidf.shape[0]): dist = cosine(origin_vector.A.flatten(), X_full_tfidf[i].toarray().flatten()) distances.append(dist) df_full['dist_to_origin'] = distances # 可视化距离变化 plt.figure(figsize=(14, 5)) plt.scatter(df_full['id'].astype(int), df_full['dist_to_origin'], alpha=0.6, s=10) plt.xlabel('条目编号') plt.ylabel('与“起源”主题的余弦距离') plt.title('内容与早期设定偏离度随编号变化') plt.grid(True, alpha=0.3) # 标记距离突然增大的点 large_jump_idx = np.where(np.diff(df_full['dist_to_origin']) > np.percentile(np.diff(df_full['dist_to_origin']), 90))[0] for idx in large_jump_idx: plt.axvline(x=df_full.iloc[idx]['id'], color='r', alpha=0.3, linestyle='--') plt.show()

6. 批量任务与自动化报告

对于持续监控或大规模分析,需要将流程脚本化、批量化。

1. 构建自动化流水线脚本 (pipeline.py)

# pipeline.py 示例结构 import logging from data_collector import crawl_list, fetch_details from data_processor import clean_and_tokenize from analyzer import extract_features, train_topic_model, compute_deviations from visualizer import create_all_plots def main(config_path='config.yaml'): logging.basicConfig(level=logging.INFO) # 1. 加载配置 # 2. 数据采集 logging.info("开始数据采集...") entries = crawl_list(config['list_url']) data = fetch_details(entries, delay=config['request_delay']) # 3. 数据处理 logging.info("开始数据清洗...") cleaned_data = clean_and_tokenize(data) # 4. 分析与建模 logging.info("开始特征提取与建模...") features, model = extract_features(cleaned_data) deviations = compute_deviations(features, model) # 5. 可视化与报告 logging.info("生成图表与报告...") create_all_plots(cleaned_data, features, deviations, output_dir=config['output_dir']) logging.info("分析流水线执行完毕。") if __name__ == '__main__': main()

2. 配置管理 (config.yaml)

project: name: "foundation_evolution_analysis" data_source: list_url: "https://example.org/list" request_delay: 2 # 秒,请求间隔 user_agent: "ResearchBot/1.0 (+https://myresearch.edu)" processing: language: "chinese" # or "english" stopwords_path: "./data/stopwords.txt" min_content_length: 50 analysis: n_topics: 8 origin_cutoff_id: 10 deviation_threshold_percentile: 15 output: directory: "./results" formats: ["html", "png", "csv"]

3. 使用任务队列处理大规模数据如果数据量极大,可以考虑使用CeleryDask进行分布式任务调度,将抓取、清洗、分析任务并行化。

7. 资源占用与性能观察

  • 内存占用:主要峰值发生在存储所有文本的DataFrame和大型稀疏矩阵(如TF-IDF矩阵)时。对于数万篇文档,建议准备16GB以上内存。使用pandas时注意数据类型优化,对于文本处理,适时使用生成器或分块处理。
  • CPU占用:爬虫网络I/O等待期间CPU占用低。文本清洗和分词是CPU密集型,LDA主题模型训练是计算密集型,对于大量文档(>5000)可能耗时较长。可以调整n_topicsmax_features参数控制复杂度。
  • 磁盘I/O:首次爬取会保存大量原始HTML和文本,确保磁盘有足够空间(数十GB,取决于原始数据量)。建议将中间结果(清洗后的文本、特征矩阵)序列化(如用picklefeather格式)存储,避免重复计算。
  • 网络流量:爬虫是主要网络使用者。设置合理的request_delay(如2-5秒)是道德和可持续性的关键,也能避免IP被封锁。

性能优化建议

  1. 增量更新:如果分析需要定期运行,设计爬虫只抓取新增或更新的条目,而非全量重抓。
  2. 特征缓存:将计算昂贵的特征(如LDA主题分布、文本向量)保存下来。
  3. 采样分析:对于超大规模数据,可以先按编号或时间间隔采样进行分析,定位关键区间后再做精细分析。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
爬虫抓取不到数据或返回403错误1. 网站有反爬机制(如User-Agent检查、频率限制)
2. 页面结构动态加载(JavaScript)
3. 需要登录或Cookie
1. 检查robots.txt
2. 打印响应状态码和头部
3. 查看返回的HTML内容是否为空或包含验证信息
4. 使用浏览器开发者工具检查网络请求
1. 设置合规的User-Agent头部
2. 大幅增加请求间隔(time.sleep)
3. 对于动态加载,考虑使用SeleniumPlaywright
4. 如需登录,使用requests.Session管理cookies(确保合规)
文本解析混乱,抓取到无关内容HTML选择器不准确或页面结构不一致1. 保存抓取到的原始HTML到本地文件
2. 用浏览器打开保存的文件,对比网页源码
3. 使用更通用或更稳健的CSS选择器或XPath
1. 更新选择器逻辑,可能需处理多种页面模板
2. 增加文本清洗步骤,用正则表达式去除特定噪音块
LDA模型结果难以解释或主题杂乱1. 文本预处理不充分(停用词未去除、未词干化)
2. 主题数量(n_topics)设置不合理
3. 数据量太少或文本太短
1. 检查清洗后的文本样本
2. 尝试不同的n_topics值,使用一致性分数或困惑度辅助选择
3. 增加max_features或调整min_df/max_df
1. 优化预处理流水线
2. 使用GridSearchCV寻找较优的n_topics
3. 考虑使用gensim的LDA实现,它可能提供更多调参选项
可视化图表过于拥挤或无法显示趋势数据点过多(如数千个条目)检查数据维度,尝试对编号进行分箱(binning)或聚合1. 将编号划分为区间(如每50个编号一个区间),计算区间内的平均特征值再绘图
2. 使用滚动平均平滑曲线
3. 使用交互式图表库(如plotly)实现缩放
运行过程中内存不足(MemoryError)1. 同时将全部数据读入内存
2. 特征矩阵过于稀疏但存储方式低效
1. 监控任务管理器的内存使用
2. 使用memory_profiler工具定位内存消耗大的函数
1. 采用分块处理策略
2. 对于稀疏矩阵,确保使用scipy.sparse格式
3. 及时删除不再需要的大变量(del var)或使用函数封装局部作用域

9. 最佳实践与使用建议

  1. 合规与伦理先行:始终将合规性和对数据源的尊重放在第一位。清晰标注你的爬虫身份(User-Agent),遵守robots.txt,控制请求频率,并只将数据用于个人研究或学习。
  2. 从样本开始,迭代开发:不要一开始就抓取全部数据。先用少量样本(如前50个条目)测试整个流水线,确保每个环节(爬取、解析、清洗、分析、可视化)都工作正常,再扩展到全量。
  3. 数据版本化:对爬取的原始数据、清洗后的数据、生成的特征和模型进行版本管理(如使用dvc或简单的备份目录)。这便于回滚、复现结果和对比不同参数下的分析结论。
  4. 模块化设计:将代码组织成独立的模块(crawler.py,cleaner.py,analyzer.py,visualizer.py),通过配置文件管理参数。这提高了代码的可读性、可维护性和可复用性。
  5. 解释重于输出:漂亮的图表不是最终目的。重点在于如何解释主题的变迁、偏离点的含义。需要结合具体的文本内容(回到原始文档)去验证数据信号的实际意义。
  6. 交叉验证:不要依赖单一指标(如余弦距离)就下结论。结合关键词频次变化、主题分布突变、社区元数据(如评论数、评分)等多维度数据进行交叉验证。
  7. 明确分析边界:在报告中明确指出分析的局限性,例如数据截止日期、未包含的内容(如删减的条目、外围故事)、以及NLP模型本身的理解局限。

通过实施这样一个系统化的技术分析项目,你不仅能回答“基金会现在有多少个001”这类数量问题,更能深度揭示其内容生态是如何从“起源故事”演化至今的。这套方法不仅适用于特定的网络叙事体系,经过调整后,也可用于分析开源项目的文档变迁、新闻话题的演变、社交媒体上的趋势流转等任何序列化文本集合,是数据驱动的内容研究利器。

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

技术博客为何拒绝同人文?——剖析CSDN选题内容边界

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

作者头像 李华
网站建设 2026/9/5 5:50:02

递归与链表:从基础到LeetCode实战

一、前言 在数据结构与算法的学习过程中&#xff0c;递归和链表是两个绕不开的重要概念。递归是一种优雅的编程思想&#xff0c;而链表则是数据结构的基础之一。当两者相遇&#xff0c;往往能碰撞出精妙的解法。 本文将从递归的基本概念出发&#xff0c;结合链表这一数据结构&a…

作者头像 李华
网站建设 2026/9/5 5:48:11

蓝牙芯片选型指南:RF性能、Flash与SDK成熟度决定BOM成本

1. 别被“蓝牙6.0”带偏节奏&#xff0c;选芯片先看这三张底牌做蓝牙方案集成这几年&#xff0c;我手里过过的芯片方案少说也有几十种&#xff0c;杰理、高通、中科蓝讯这三个平台更是从入门到放弃、再到真香&#xff0c;反复横跳过好几轮。每次有客户拿着电商页面上“最新蓝牙…

作者头像 李华
网站建设 2026/9/5 5:44:20

瑞芯微RK182X+算力卡:端侧12B大模型部署实战解析

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

作者头像 李华
网站建设 2026/9/5 5:37:01

从图片到PCB:零基础两小时制作个性化电路板画全攻略

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

作者头像 李华