尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于深度学习的NCF视频推荐:源码拆解与工程化实践
简介基于深度学习神经网络协同过滤模型NCF的视频推荐系统源码面向推荐系统学习者和深度学习实践者便于理解如何用神经网络建模用户与视频之间的交互关系。资源完整覆盖从数据读取、特征构造、模型构建到训练评估的流程能够直观展示NCF相对传统协同过滤的优势适合课程练习、项目复现或算法对比。压缩包共三个文件包括一个用Python编写的核心脚本、一个CSV格式的测试数据文件以及一个Markdown说明文档整体仅四KB左右结构精简便于下载后快速阅读、运行和调试。所有源码均已在本地编译验证难度适中内容经助教老师审定能够满足自主学习和教学使用需求。已有约两百人学习浏览适合对照论文逐行理解模型细节也可替换为自有视频观看或点击数据进行迁移实验作为毕业设计或推荐系统方向的入门参考。1. 基于深度学习的 NCF 视频推荐系统这份源码到底解决什么问题手里有几十万条视频观看日志想让每个用户下次打开时看到自己真正想看的视频传统的矩阵分解在数据跑到一定规模后指标几乎不再增长而基于深度学习的神经网络协同过滤NCF是这类问题里投入产出比最高的升级路线之一。标题里的源码 zip本质就是一套把 Python 数据处理、深度学习模型训练、视频推荐候选生成和离线评估串起来的完整工程骨架。你拿到手后不需要从零造轮子改数据路径、调几个参数就能跑通从原始日志到 Top-N 推荐结果的闭环。这篇文章按“原理怎么看、代码怎么写、坑在哪、怎么上线验证”的顺序拆开讲适合正在做视频站推荐、内容 App 冷启动或者拿这个方向写课程设计的人。2. 先看懂 NCF 再动手神经网络协同过滤的原理与选型很多人在跑通代码之后才发现自己根本不知道为什么这么设计遇到负采样比例不对、评估指标虚高这种问题也无从下手。所以我不急着贴训练代码先把 NCF 的原理和选型理由讲透。2.1 矩阵分解为什么到顶内积拟合不了非线性交互传统协同过滤把行为矩阵拆成用户因子和物品因子预测分数等于两个向量的内积。但在视频场景里一个用户的行为并不线性有人只看健身视频有人白天看装机教程、晚上刷美食还有人会因为一部剧追完整个演员的作品集。这些跨类目的兴趣组合靠一个点积很难描述清楚。矩阵分解相当于给每个用户压了一条兴趣直线而实际兴趣往往是分叉的、带边界的曲面。NCF 的核心改动是把“内积”这个固定算子换成“神经网络拟合的交互函数”。模型仍然给用户和视频分别学 embedding但预测分数改由网络输出中间有非线性激活于是能表达更复杂的用户-物品关系。刚开始跑 NCF 的人最容易有的错觉是“这不就是加了层 MLP 的矩阵分解”实际差别在于交互层从线性变成非线性模型容量和解空间都变了。还有一点容易被忽略视频推荐几乎没有显式评分大部分行为是隐式的“看过/没看过”。矩阵分解那套预测 1 到 5 分的思路在这里并不自然NCF 从设计上就是为隐式反馈服务的输出经过 sigmoid 当作点击概率来建模这更贴合视频站的真实信号。选型时不用纠结“要不要上注意力机制、要不要上 Transformer”对大多数视频推荐的第一版NCF 是性价比最高的选择。2.2 GMF、MLP 与 NeuMFNCF 模型结构拆分NCF 论文里最经典的结构是双通道后来的工业实现大多沿用下面这三块模块交互方式特点在视频场景中的作用GMF用户向量与物品向量逐元素相乘保留矩阵分解的逐维度匹配语义快速捕捉“用户喜欢动作片”这种粗粒度偏好MLP拼接向量后过若干全连接层非线性交叉表达能力更强捕捉跨类目、组合型兴趣比如“科技评测萌宠”NeuMFGMF 与 MLP 输出拼接后再映射两类信息互补最终打分统一由融合层完成效果通常最好GMF 分支把用户 embedding 和视频 embedding 做逐元素乘积再通过一个线性层映射成分数本质是“每个维度单独匹配再求和”。MLP 分支则把两个 embedding 拼接起来过几层带 ReLU 的全连接让网络自己学高阶交叉。NeuMF 在最后一层把两个分支的输出拼起来再过一个线性层我用 concat 而不是手动加权相加原因是让模型自己学两个分支的权重省去人工调参。embedding 维度这件事值得多说一句用户数或视频数过百万时embedding 维度取 16 到 64 就够再大只会涨显存效果提升很有限。MLP 层我常用[64, 32, 16]或[32, 16]两层也能跑出不错的结果。真正影响效果的是负采样个数、学习率和训练数据质量而不是网络深度。2.3 隐式反馈的样本构造正样本、负样本与曝光偏差训练 NCF 时我们把“用户看过某个视频”记为正样本需要同时构造负样本。最常见的做法是随机负采样从用户没看过的视频里随机抽几个标成 0。但这里有两个隐藏问题一是“没看过”不等于“不喜欢”可能是没曝光过二是随机负采样会把热门视频大量漏进负样本导致模型倾向把所有视频都预测成正样本。下面这段代码是我在视频推荐项目里最常用的负采样写法直接给定一个行为日志 DataFrame返回训练用的用户 ID、视频 ID 和标签数组import numpy as np import pandas as pd def build_train_samples(df, neg_ratio4, seed42): rng np.random.default_rng(seed) df[label] 1 pos df[[user_id, video_id, label]].values neg_list [] all_video df[video_id].unique() for u, v, _ in pos: # 找出该用户没看过的视频作为负样本候选池 watched df.loc[df[user_id] u, video_id].values candidates all_video[~np.isin(all_video, watched)] neg rng.choice(candidates, sizeneg_ratio, replaceFalse) for nv in neg: neg_list.append((u, nv, 0)) neg np.array(neg_list) X_u np.concatenate([pos[:, 0], neg[:, 0]]).astype(np.int64) X_i np.concatenate([pos[:, 1], neg[:, 1]]).astype(np.int64) y np.concatenate([pos[:, 2], neg[:, 2]]).astype(np.float32) return X_u, X_i, y这里neg_ratio是负采样比例视频推荐里常见取 4数据量大时可以降到 2数据稀疏就提到 6 到 8。代码里用np.isin过滤掉用户看过的视频避免把正样本当成负样本。注意它仍属于随机负采样如果手里有曝光日志应该优先用“曝光但未点击”作为负样本那才是真实的负反馈信号。3. 用 PyTorch 实现 NCF 视频推荐数据处理、模型构建、训练评估一次跑通原理说完接下来按“最小可跑通”的方式把这份视频推荐源码里最核心的三段代码拆开讲。环境方面Python 3.8 以上加 PyTorch 2.x 就够。3.1 行为日志清洗与用户、视频 ID 编码第一步是把原始观看日志变成模型能吃的整数 ID。原始数据可能长这样user_id, video_id, timestamp, watch_ratio。watch_ratio是观看时长占比我一般把它作为有效交互的判定条件。import pandas as pd log pd.read_csv(view_log.csv) # 只看完一半以上的行为才记为正样本 log log[log[watch_ratio] 0.5] # 过滤低频用户和低频视频低于 5 次交互的直接丢弃 user_cnt log.groupby(user_id)[video_id].count() video_cnt log.groupby(video_id)[user_id].count() log log[log[user_id].isin(user_cnt[user_cnt 5].index)] log log[log[video_id].isin(video_cnt[video_cnt 5].index)] # 重新编码为从 0 开始的整数 ID log[user_id] log[user_id].astype(category).cat.codes log[video_id] log[video_id].astype(category).cat.codes num_users log[user_id].nunique() num_videos log[video_id].nunique()这段代码把行为日志压缩成训练所需的三元组结构。watch_ratio 0.5这个阈值是经验值如果你的场景是“点赞”或“收藏”可以直接去掉这一行把原始行为视为正样本。低频过滤很关键否则会出现大量只出现过一次的用户和视频让 embedding 矩阵变成一个稀疏的黑匣子模型既学不稳也容易过拟合。过滤后重新编码 ID用astype(category).cat.codes能同时完成字符串或大整数 ID 的映射。但你要注意保存映射关系否则上线推理时拿不到新用户 ID 对应的编号这一步在正式系统里通常用两张字典表来完成。3.2 定义 NCF 模型embedding 层、GMF 分支、MLP 分支与融合输出模型定义我用 PyTorch 写结构就是前面原理部分说的三块GMF 逐元素乘积、MLP 拼接、最后融合。import torch import torch.nn as nn class NCF(nn.Module): def __init__(self, num_users, num_items, embed_dim32, mlp_dims[64, 32, 16]): super().__init__() self.user_emb nn.Embedding(num_users, embed_dim) self.item_emb nn.Embedding(num_items, embed_dim) # GMF 分支用户向量与视频向量逐元素相乘后映射到 1 维 self.gmf_out nn.Linear(embed_dim, 1) # MLP 分支拼接后过两层全连接中间用 ReLU mlp_layers [] in_dim embed_dim * 2 for d in mlp_dims: mlp_layers.append(nn.Linear(in_dim, d)) mlp_layers.append(nn.ReLU()) in_dim d self.mlp nn.Sequential(*mlp_layers) self.mlp_out nn.Linear(in_dim, 1) # 融合两个分支的输出让模型自己学权重 self.predict nn.Linear(2, 1) def forward(self, user_ids, item_ids): u self.user_emb(user_ids) v self.item_emb(item_ids) gmf self.gmf_out(u * v) mlp self.mlp(torch.cat([u, v], dim-1)) mlp self.mlp_out(mlp) out self.predict(torch.cat([gmf, mlp], dim-1)) return torch.sigmoid(out).squeeze(-1)这里有几个参数直接影响模型行为和效果。embed_dim32是 embedding 维度。视频站用户量和物品量通常在几十万以上32 维已经足够表达大部分偏好结构再大收益甚微且显存开销翻倍。mlp_dims[64, 32, 16]是 MLP 分支的隐藏层宽度从 64 递减到 16和常见推荐模型的习惯一致。如果你更在意推理速度可以把embed_dim降到 16mlp_dims改成[32, 16]效果损失通常在一个点以内。注意最后torch.sigmoid我们把它当作点击概率来优化。负样本占比高的场景下预测值会普遍偏低这很正常不要因此怀疑代码写错了。3.3 训练循环与 Top-N 评估损失函数、学习率与命中率训练阶段用 BCE 损失和 Adam 优化器。对隐式反馈来说这就是一个二分类问题正样本是“看过”负样本是“没看过”。学习率我一般从1e-3起步如果 loss 震荡就降到5e-4。def train_one_epoch(model, opt, loss_fn, X_u, X_i, y, batch_size1024): model.train() idx torch.randperm(len(X_u)) total_loss 0 for start in range(0, len(X_u), batch_size): batch idx[start:start batch_size] opt.zero_grad() pred model(X_u[batch], X_i[batch]) loss loss_fn(pred, y[batch]) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() total_loss loss.item() return total_loss / max(1, len(X_u) // batch_size)评估函数重点看 Top-N 命中率也就是每个测试用户真实看过的视频有没有出现在模型打分最高的前 N 个里。torch.no_grad() def evaluate_hr_ndcg(model, test_users, test_true, all_video_ids, k10): model.eval() hits 0 ndcg 0.0 for user, true_item in zip(test_users, test_true): # 构造候选集真实视频 随机 999 个负视频 cand np.random.choice(all_video_ids, 1000, replaceFalse) cand np.append(cand, true_item) scores model( torch.full((len(cand),), user, dtypetorch.long), torch.tensor(cand, dtypetorch.long) ) topk torch.topk(scores, k).indices.tolist() if true_item in topk: hits 1 rank topk.index(true_item) ndcg 1.0 / np.log2(rank 2) return hits / len(test_users), ndcg / len(test_users)这里的候选集取“1 个真实视频 999 个随机负视频”是为了模拟线上“从全量视频池里挑一个”的难度。评估指标 HR10命中率和 NDCG10排序质量要一起看HR 高只说明推荐列表里有对的NDCG 高才说明对的排在前面。如果你用随机切分而不是按时间切分来划分训练测试集这两个指标都会虚高后面避坑章节会专门讲。4. NCF 视频推荐避坑指南五个高频翻车现场与排查路径代码跑通不难真正花时间的是调参和排错。这里整理五个我在视频推荐项目里反复遇到的坑按“现象、原因、解决”写清楚。4.1 训练 loss 一直在降推荐结果却全是热门视频现象模型训练正常loss 稳步下降但打开推荐列表前十个全是平台热门视频每个用户的推荐结果几乎一样。原因这是隐式反馈数据里最常见的陷阱。热门视频天然拥有大量正样本模型只要学会“预测热门视频为 1”就能拿到很低的 loss。此时个性化信号被头部流量淹没了。解决两步走。第一步负采样时把热门视频的曝光概率降低或者按物品热度做降采样避免热门视频频繁出现在负样本里第二步评估时单独统计“非热门视频命中率”只统计播放量在后 80% 的冷门视频有没有被推荐出来。如果这个指标有明显提升说明模型学到的是真实偏好而不是热度偏置。4.2 Embedding 维度一调大显存直接爆掉现象为了追求效果把embed_dim从 32 调成 128结果训练还没开始显卡就 OOM 了。原因NCF 最大的显存开销不是网络层而是两个 embedding 表。100 万用户乘以 128 维再乘 4 字节单用户表就占 512MB两个表加上中间变量小显存卡直接扛不住。解决不要盲目调大 embedding 维度。先看用户和物品数量百万量级用 32 维、千万量级用 16 维是合理起点。如果确实需要更大容量用 hash 分桶把用户 ID 映射到固定区间或者把 embedding 切成多份做模型并行。实在不行就给 embedding 表单独开 CPU 内存通过nn.Embedding.from_pretrained配合torch.no_grad做离线索引。4.3 新用户没有任何行为模型返回空列表现象线上接入后注册当天的新用户请求推荐接口直接抛异常或者返回空列表。原因NCF 的输入是用户 ID模型只为训练集中出现过的用户保存了 embedding。一个全新的user_id在 embedding 矩阵里根本找不到对应向量拿不到任何排序结果。这是协同过滤类模型的通病不是代码 bug。解决在模型外面包一层兜底逻辑。新用户直接返回热榜加分类榜比如“全站热门 Top50”加“每个频道热门 Top10”同时记录新用户的实时点击行为攒够 5 到 10 条交互后重新请求 NCF 打分。工业系统里通常会单独训练一个用内容特征做冷启动的双塔模型接在 NCF 前面但对源码级项目来说先把兜底接口写好比什么都重要。4.4 离线评估 HR10 虚高上线后点击率反而下降现象测试集上 HR10 做到 0.35感觉效果不错上线 A/B 测试却发现新推荐位的点击率还不如原来按播放量排序的版本。原因离线评估和线上环境不一致。最常见的问题是“随机切分数据”把用户十天的行为随机分成训练集和测试集模型在训练时已经见过测试集里视频的其它用户行为相当于开卷考试。另一个问题是位置偏差线上用户本来就更容易点排在前面和封面更醒目的视频离线评估完全没有这个因素。解决评估协议改成按时间切分用第 1 天到第 14 天的行为训练用第 15 天到第 21 天的行为测试模拟真实预测场景。这和你跑量化交易策略回测是一个道理训练集和测试集错位越多指标就越接近上线后的真实水平。上线后先切 10% 流量跑三天对比点击率、人均完播时长和次留再决定要不要全量。4.5 训练到一半 loss 变成 NaN或者模型输出全是一个值现象训练第 10 个 epoch 时 loss 突然变成nan重启之后有时又变成模型对所有样本都输出相似分数。原因学习率过大导致梯度爆炸或者 embedding 初始化方差太大某些维度数值溢出到inf。负采样比例过大时样本类别严重不平衡也会让输出层梯度不稳定。解决先用lr1e-3以下的学习率配合 Adam 优化器在训练循环里加torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)这行保护代码我已经写在前面训练函数里了。如果nan还是出现检查数据里是否有空 ID 或缺失值把负采样比例从 8 降到 4。模型输出全是一个值的另一常见原因是最后一层权重初始化过大给self.predict换用nn.init.xavier_uniform_能缓解。5. 从源码到正式视频推荐系统召回、排序与冷启动源码跑通只是第一步真正要用在视频站上需要把单模型扩展成“召回 排序 兜底”的完整链路。这一章讲清楚工程化的三个关键环节。5.1 召回层百万视频量级下没法全量过 NCF如果把全量视频都塞给 NCF 打分用户数 10 万、视频数 50 万时一次推荐就要算 500 亿次前向这在线上完全不可行。所以工业流做法是分两步先用召回把候选视频从百万级缩小到几百级再由 NCF 精排。NCF 训练完的item_emb.weight本身就是现成的视频向量可以直接用来构建近似最近邻索引。我一般用 faiss 建一个内积索引因为视频向量的模长本身包含一定热度信息用内积比纯余弦更合适。import faiss import numpy as np item_vec model.item_emb.weight.detach().numpy().astype(float32) index faiss.IndexFlatIP(item_vec.shape[1]) index.add(item_vec) # 给某个视频找 50 个相似视频 scores, idx index.search(item_vec[123:124], 50)这段代码把模型训练好的视频 embedding 灌进 faiss 索引查询时按向量内积找相似视频。召回阶段还可以叠加多个通道用户最近看过的 10 个视频各取 20 个相似视频再合并去重加上一个全局热门通道兜底候选集就基本覆盖了用户可能感兴趣的范围。5.2 排序层NCF 打分和视频内容特征怎么融合纯 NCF 只用到了用户 ID 和视频 ID没用到视频标题、分类、上传时长这些内容特征。在正式视频系统里排序阶段通常会把 NCF 的输出当成一个特征再叠加人工设计的统计特征一起进入上层模型。常见做法有三种。第一种最省事把 NCF 预测分数、视频完播率、点赞率、和该用户频道的匹配度拼成一个特征向量喂给一个浅层 DNN 或梯度提升树做最终排序。第二种是把视频侧信息直接加到 NCF 输入里比如给视频 embedding 拼接类型 embedding 和热度 embedding。第三种是重排阶段再做一次规则干预比如连续三个短视频后插入一条长视频或者限制同一作者的视频最多出现两条。深度强化学习算法近几年也开始用于重排阶段目标是让整次会话的完播时长最大化而不是只优化单次点击。但那是比较靠后的迭代方向第一版先把 NCF 和统计特征融合跑稳别急着上强化学习否则模型复杂度和调参成本会一起失控。5.3 冷启动与新内容没有行为数据时怎么兜底冷启动分两头新用户没有行为新视频没有曝光。对新用户最实用的是分渠道热门榜按“全站播放热度、分类热度、注册时间相似用户的热度”各取一个列表融合。对新视频可以先用视频标题和封面抽文本向量、图像向量把它和已有视频做相似度匹配推荐给偏好该内容类型的用户。在这种临时推荐里我经常用 python 构建邻接矩阵把视频之间的共现关系直接算成相似度表。所谓共现就是“被同一个用户在短时间内连续观看的两个视频”统计它们的共现次数按用户数归一化后得到一张视频相似度图。这张图虽然粗糙但用来给新视频做冷启动兜底比纯热榜精准得多。等新视频积累到一定曝光量再让它进入 NCF 的正常训练流程。日志回流是维持这个闭环的关键。线上每次曝光、点击、完播都要写回日志模型每天或每周增量训练一次。增量训练时不要从零开始而是在前一天模型参数上继续训练这样既能吸收新数据又不会让旧用户偏好被冲掉。6. 离线评估与上线验证NCF 源码里最值得先改的三处代码最后说验证。一套视频推荐 NCF 源码拿到手我通常先改三处评估切分方式、指标报告维度、小流量实验开关。6.1 把随机切分改成时间切分这是改动优先级最高的一处。用timestamp排序后按前 70% 训练、后 30% 测试而不是按行随机切。代码改动不大但能直接挤掉离线指标里虚高的水分。6.2 补齐三个被忽略的指标表格里加三列非热门命中率、覆盖率、单用户推荐列表去重率。非热门命中率衡量个性化强度覆盖率看推荐结果有没有集中在热门视频上去重率防止同一个作者的视频扎堆出现在一屏里。指标含义合理范围参考HR10真实观看视频出现在前 10 的比例0.1 到 0.4 之间视稀疏度而定NDCG10前 10 列表排序质量通常比 HR 低 30% 到 50%非热门命中率冷门视频被推荐出来的比例低于 5% 说明模型退化成热榜6.3 留好 A/B 测试接口这是从源码到上线最关键的一步。在代码里预留一个is_ab开关线上放量时按用户 ID 哈希分流让 10% 流量走 NCF 推荐其余继续走老策略对比三天后的点击率和人均完播时长再决定是否全量。我自己在视频推荐上踩过最大的坑是把离线指标优化当成了目标结果上线后才发现用户根本不买账。后来养成的习惯是无论离线指标涨得多漂亮每次改动都先切小流量跑三天看数据再决定下一步。推荐系统是个反馈闭环离线是起点线上才是考场。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

基于改进Unet的高分辨率城市建筑物遥感变化检测毕设方案

基于改进Unet的高分辨率城市建筑物遥感变化检测毕设方案

简介:这份资源面向计算机、遥感与人工智能方向的本科毕业生及需要完成变化检测课题的学习者,提供一套基于改进U-Net的高分辨率城市建筑物遥感变化检测Python源码与配套说明文档。项目在开源方案基础上进行改进,涵盖模型与编码器选择、嵌入SE模…

📅 2026/10/13 0:56:38
MATLAB异构特征ERM建模:工业/医疗/金融多源数据统一分类

MATLAB异构特征ERM建模:工业/医疗/金融多源数据统一分类

简介:本资源是一份面向数据科学家、算法工程师及高校研究人员的MATLAB机器学习实践项目,聚焦经验风险最小化(ERM)框架下的多特征分类预测建模,适用于智能制造、金融风控与医疗健康等领域的高维异构数据智能分析场景。压…

📅 2026/10/13 0:56:38
LSTM与EKF融合:不靠地图的路径跟踪与轨迹预测方法

LSTM与EKF融合:不靠地图的路径跟踪与轨迹预测方法

简介:面向自动驾驶、地理信息系统与物流规划中的路径跟踪和路径预测需求,这份资源完整提供了基于长短期记忆网络(LSTM)的MATLAB工程实现,代码与数据均已配套齐全。资源包共14个文件,压缩包大小114KB&#x…

📅 2026/10/13 0:56:38
MORE NEWS

更多资讯

📰

C#面向对象实战:Ellipse继承Circle的继承边界与IComparable/IComparer排序设计

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

📰

ESP32控制舵机全攻略:从PWM原理到多路联动与网络控制

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

📰

Java数据库安全与性能基石:深入理解PreparedStatement

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

📰

Android Binder源码解析:BBinder服务端实体核心机制

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

📰

Vue实现增删改查面试题全解析:从响应式原理到组件通信

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

📰

面向对象贪吃蛇:从游戏实现到软件工程思维训练

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬