从数据采集到离线计算:基于Hadoop与Django的小说推荐系统实现解析
2026/9/18 7:42:00 网站建设 项目流程

简介:一份以Hadoop+Python为基础的协同过滤小说推荐系统毕业论文文档,适合计算机、软件工程等专业学生用于毕业设计参考。论文完整阐述基于协同过滤算法的推荐系统设计与实现,覆盖需求分析、系统总体设计、功能模块划分等环节,重点说明如何利用Python进行数据预处理与模型训练、Hadoop处理大规模用户行为数据、Django构建用户界面与后台管理、MySQL存储用户及小说数据,并通过爬虫技术采集小说信息。系统还围绕个人中心、用户管理、小说信息管理、系统管理等模块展开,贯彻高内聚低耦合设计原则,提升可维护性与扩展性。资源为1个docx文件,压缩包约5MB,包含中英文摘要、目录及完整正文,结构清晰便于按章节查阅。已有314人学习,对准备开题、撰写论文或理解推荐系统落地方案的读者具有直接借鉴价值。

1. 小说推荐系统的难点不在算法,在数据链路

很多人以为基于协同过滤的小说推荐系统,核心是算法模型,拿到论文和源码的第一反应也是先找协同过滤的相似度计算函数。实际拆完这套基于 Hadoop、Python、Django 的项目后你会发现,最难的部分反而是数据链路:小说数据从哪里来、用户行为数据如何存储、相似度矩阵多久算一次、推荐结果怎么在 Web 接口里低延迟读出来。这些问题没想清楚,协同过滤代码写得再漂亮也无法上线。本文按“数据采集 → 离线计算 → Web 服务 → 效果调优”的顺序完整拆解这个系统的实现路径,读者可以跟着步骤在自己的环境里复现一套可演示的推荐链路。

2. 协同过滤算法的矩阵构建与相似度计算

2.1 为什么选协同过滤而不是内容推荐

小说推荐这个场景有一个明显特点:物品(小说)的元数据质量参差不齐。很多爬取到的书籍简介只有几十个字,标签也经常缺失,基于内容的推荐(Content-Based)很难从稀疏的文本特征里算出可靠的作品相似度。而用户行为数据——收藏、点击、评分、阅读时长——是用户在真实使用中产生的,数据量越大越能反映兴趣倾向,这正是协同过滤的适用区间。

协同过滤分为基于用户的(User-Based)和基于物品的(Item-Based)两种。小说推荐系统中,用户数量通常远大于小说数量,而且小说是长尾商品,用户对物品的评分矩阵非常稀疏。基于物品的协同过滤先计算小说之间的相似度,再根据用户历史行为推荐相似小说,相似度矩阵可以离线预先算好,线上只需要查表聚合,非常契合 Django 这种同步 Web 框架的使用方式。因此这套系统的推荐核心选用 Item-Based Collaborative Filtering。

2.2 用户-小说评分矩阵的构建

原始行为数据是用户对小说的收藏、评分、阅读时长记录,不能直接用来算相似度。常见做法是先把行为归一化成 1~5 分的隐式评分,再进行矩阵构建。

import pandas as pd import numpy as np from scipy.sparse import csr_matrix # reading_log 包含 user_id, novel_id, action_type, score, duration log = pd.read_csv("reading_log.csv") # 行为加权:收藏权重最高,评分直接取分,阅读时长按分钟数折算 weight_map = {"favorite": 4.5, "rating": 1.0, "read": 0.1} log["weighted_score"] = log.apply( lambda row: row["score"] * weight_map.get(row["action_type"], 1.0) if row["action_type"] == "rating" else weight_map.get(row["action_type"], 0.0), axis=1, ) # 每个用户对同一本小说的最终评分取加权均值 user_novel_rating = ( log.groupby(["user_id", "novel_id"])["weighted_score"] .mean() .reset_index() ) # 转成行为矩阵,未产生的行为填充为 0 matrix = user_novel_rating.pivot( index="user_id", columns="novel_id", values="weighted_score" ).fillna(0) # 转为稀疏矩阵,节省内存并加速后续矩阵乘法 sparse_matrix = csr_matrix(matrix.values) print("行为矩阵形状:", sparse_matrix.shape)

代码中weight_map的取值需要根据系统实际运营数据调整:如果产品中收藏行为极少,说明收藏行为门槛高,权重应上调;阅读时长受章节长度影响大,折算系数建议按平均章节字数归一。fillna(0)把未产生的行为当作 0 分,这里隐含一个假设——用户没看过的小说大概率不感兴趣,实际工程中还可以把 0 替换成用户平均分,减少冷启动噪声。

2.3 相似度计算与 TopN 推荐

物品相似度常用余弦相似度或皮尔逊相关系数。余弦相似度只关注向量方向,对评分尺度不敏感;皮尔逊相关系数会做中心化处理,对用户评分偏好(有的人习惯打高分,有的人习惯打低分)更鲁棒。小说推荐场景下,用户评分往往带有明显的个人倾向,推荐采用皮尔逊相关系数,但如果矩阵过于稀疏,余弦相似度反而更稳定——因为中心化会把本来就稀少的有效评分进一步稀释。

from sklearn.metrics.pairwise import cosine_similarity # 计算物品相似度矩阵,cosine_similarity 对稀疏矩阵直接生效 # 传入矩阵的转置,行变为小说,列变为用户 item_similarity = cosine_similarity(sparse_matrix.T) np.fill_diagonal(item_similarity, 0) novel_ids = matrix.columns.tolist() similarity_df = pd.DataFrame( item_similarity, index=novel_ids, columns=novel_ids, ) def recommend_for_user(user_vector, top_k=10): # user_vector 是用户的评分行为向量,与物品相似度矩阵做乘积 scores = item_similarity.T.dot(user_vector) # 排除用户已经看过的物品 scores[user_vector > 0] = -1 top_items = np.argsort(scores)[::-1][:top_k] return [novel_ids[i] for i in top_items]

逻辑说明:item_similarity.T.dot(user_vector)实际是在算“用户评分过的每一本小说”和“候选池小说”的相似度加权和,用户对某本小说评分越高,这本小说对最终推荐排序的贡献越大。过滤已读物品必须放在排序前做,否则系统会反复推荐用户早已看过的热门书。

矩阵规模扩大后,物品相似度矩阵的存储会从 O(n²) 急速膨胀。5 万本小说就是 25 亿个浮点数,内存完全撑不住。常见做法是在计算后只保留每个物品 Top-N 个相似物品,其余置零,以稀疏格式落盘到 MySQL 或 Redis。

相似度算法稀疏矩阵表现对评分尺度的敏感度计算成本
余弦相似度稳定,推荐首选不敏感
皮尔逊相关系数稀疏时波动大敏感,适合密集评分
Jaccard 相似度只关注是否评分完全不敏感
调整余弦相似度需要先做均值中心化敏感

3. 数据采集层:Scrapy 爬虫与 Hadoop 离线处理的衔接

3.1 数据抓取链路设计

协同过滤需要两类数据:小说基础信息和用户行为数据。小说基础信息(书名、作者、分类、简介、封面)来自爬虫,用户行为数据则来自 Web 端的埋点日志。论文中提到的 Scrapy 爬虫,主要负责前者。爬虫的产出不能直接进入推荐模块,需要经过清洗、去重、格式统一,再写入 MySQL。

这个系统的数据链路设计为三层:Scrapy 负责增量抓取、Hadoop 负责对积累的用户日志做离线批量计算、Django 读取计算好的相似度结果对外提供推荐接口。爬虫层与 Hadoop 层的衔接点在于:爬虫产出的原始 JSON 落盘后,由 MapReduce 作业或 Hive 任务统一解析入库,避免爬虫进程直接连生产数据库造成连接数压力。

3.2 Scrapy 爬虫的核心实现

以抓取小说列表页为例,Spiders 层定义解析规则,Items 层定义结构化字段,Pipeline 层负责清洗入库。这里给出一个简化但可运行的版本。

import scrapy from novel_recommend.items import NovelItem class NovelSpider(scrapy.Spider): name = "novel_spider" allowed_domains = ["novel-source.example.com"] start_urls = ["https://novel-source.example.com/all"] def parse(self, response): # 列表页正则提取当前页所有小说链接 for href in response.css("div.book-list a::attr(href)").getall(): yield response.follow(href, callback=self.parse_detail) # 翻页逻辑:抓取下一页链接,直到页码超过 200 next_page = response.css("a.next::attr(href)").get() if next_page and response.meta.get("page", 1) < 200: yield response.follow( next_page, callback=self.parse, meta={"page": response.meta.get("page", 1) + 1}, ) def parse_detail(self, response): item = NovelItem() item["novel_name"] = response.css("h1.book-name::text").get() item["author"] = response.css("span.author::text").get() item["category"] = response.css("span.category::text").get() item["intro"] = response.css("div.intro::text").get().strip() item["cover_url"] = response.css("img.cover::attr(src)").get() yield item

关键点在于settings.py中的下载延迟和去重配置。抓取频率过高会导致 IP 被限制,常见做法是设置DOWNLOAD_DELAY = 2.0,同时开启HTTPCACHE_ENABLED = True,避免重复爬取相同页面。翻页深度限制在 200 页是经验值,超过这个深度的小说网站通常存在大量重复或低质内容,继续抓取的边际收益很低。

Pipeline 层做的清洗工作包括:去掉简介中的 HTML 标签、过滤已存在同名的书籍、对缺失封面的条目补默认图。清洗过的小说数据写入 MySQL 的novel_info表,行为日志则单独走日志通道,进入 HDFS 由 Hadoop 离线处理。

3.3 Hadoop 在推荐链路中的定位

Hadoop 在小说推荐系统里承担的是批量计算和存储的角色。用户行为日志按天滚动写入 HDFS,MapReduce 作业在每天凌晨跑一次,输出当天的用户-小说评分汇总表。对于日均百万级的行为数据,这种离线 T+1 模式是性价比最高的选择。

HDFS 的存储路径建议按日期分区:/user/hadoop/novel_logs/dt=2024-06-01/。这样后续做数据回溯或补算时,可以只针对某一天的数据单独跑作业,而不必全量扫描。

# 将 Web 服务器收集的行为日志上传到 HDFS hdfs dfs -mkdir -p /user/hadoop/novel_logs hdfs dfs -put reading_log_$(date +%Y%m%d).log /user/hadoop/novel_logs/ # 查看 HDFS 上数据块分布,确认数据写入均衡 hdfs fsck /user/hadoop/novel_logs -files -blocks

hdfs fsck-files -blocks参数会在输出中列出每个文件的块 ID 和块大小。如果发现大量小文件(比如小于 128MB 的块),说明日志收集端写入太零散,后续需要先合并成 SequenceFile 再跑 MapReduce,否则 Namenode 的内存会被文件元数据占满。

Hadoop 伪分布式搭建是大多数开发者入门的第一步,core-site.xml配置fs.defaultFShdfs://localhost:9000hdfs-site.xml配置副本数为 1。需要注意的是,伪分布式与真实集群在 NameNode 和 DataNode 的启动顺序上一致,但资源调度机制完全不同,本地调试通过不代表集群环境可以直接跑通。

3.4 爬虫数据与行为数据的合并策略

爬虫持续运行会产生小说更新信息,比如最新章节、简介变化、下架标记。合并策略上,novel_info表使用novel_id作为业务主键,不是自增 ID,因为同一个小说来源站点有自己的唯一 ID,直接用来源 ID 可以天然去重。MAP合并使用REPLACE INTO还是INSERT ... ON DUPLICATE KEY UPDATE,取决于是否需要保留历史字段。

行为数据与小说数据的合并发生在离线计算阶段,最终产出的是用户-小说评分宽表。这张宽表直接喂给协同过滤矩阵生成程序,作为下一章的输入数据。

4. Django ORM 与 MySQL 实现推荐主流程

4.1 数据库实体关系设计

推荐系统的数据表设计遵循高内聚低耦合的原则,核心表划分为四张:用户表、小说信息表、用户行为表、物品相似度表。用户表与行为表是一对多关系,小说表与行为表也是一对多关系,相似度表则专门存放离线计算好的 TopN 相似结果。

CREATE TABLE user_info ( user_id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password_hash VARCHAR(256) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE novel_info ( novel_id INT NOT NULL AUTO_INCREMENT, novel_name VARCHAR(255) NOT NULL, author VARCHAR(64), category VARCHAR(32), intro TEXT, cover_url VARCHAR(512), source_platform VARCHAR(16), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (novel_id), KEY idx_category (category) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE user_behavior ( id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, novel_id INT NOT NULL, action_type VARCHAR(16) NOT NULL, score DECIMAL(3,1) DEFAULT 0, duration_seconds INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_novel (user_id, novel_id), KEY idx_novel_id (novel_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

user_behavior的联合索引(user_id, novel_id)是为了支撑两种高频查询:查某个用户的全部行为记录、查某本小说被哪些用户行为过——这两个查询正是构建评分矩阵时的基础数据来源。DECIMAL(3,1)用于评分字段,比 FLOAT 更精确,避免浮点误差导致相似度计算结果轻微漂移。

4.2 Django ORM 模型定义

Django 的 ORM 模型与 SQL 建表语句是一一映射关系。使用 Django 的manage.py inspectdb可以从现有数据库反向生成模型代码,但生成的模型往往带着大量冗余的 class Meta 配置,建议手工整理一份更简洁的版本。

from django.db import models class UserInfo(models.Model): username = models.CharField(max_length=64, unique=True) password_hash = models.CharField(max_length=256) class Meta: db_table = "user_info" class NovelInfo(models.Model): novel_name = models.CharField(max_length=255) author = models.CharField(max_length=64, null=True) category = models.CharField(max_length=32, null=True) intro = models.TextField(null=True) cover_url = models.CharField(max_length=512, null=True) source_platform = models.CharField(max_length=16, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "novel_info" class UserBehavior(models.Model): user_id = models.IntegerField() novel_id = models.IntegerField() action_type = models.CharField(max_length=16) score = models.DecimalField(max_digits=3, decimal_places=1, default=0) duration_seconds = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "user_behavior"

模型中故意没有使用 Django 的ForeignKey关联到UserInfoNovelInfo,这是性能上的考虑。推荐接口的核心是高分屏查询,如果用外键关联,ORM 会触发额外的 JOIN 查询;保持整数类型字段,可以在需要时手动select_related或直接字典映射。

4.3 推荐服务接口设计

推荐接口是 Django 视图层对外暴露的核心能力。接口接收user_id,返回该用户的个性化推荐列表。这里的关键是不要在视图函数里临时计算相似度,而是直接查询离线计算好的相似度表。

from django.http import JsonResponse from django.views.decorators.cache import cache_page from .models import UserBehavior, NovelInfo, ItemSimilarity @cache_page(60 * 5) def recommend_view(request): user_id = request.GET.get("user_id") if not user_id: return JsonResponse({"code": 400, "msg": "user_id is required"}) # 1. 获取用户最近的行为,用于匹配相似物品 behaviors = UserBehavior.objects.filter( user_id=user_id ).order_by("-created_at")[:50] if not behaviors.exists(): # 无行为数据时返回热门小说,触发冷启动策略 hot_novels = NovelInfo.objects.order_by("-updated_at")[:10] return JsonResponse({ "code": 0, "data": [ {"novel_id": n.id, "novel_name": n.novel_name} for n in hot_novels ], }) # 2. 获取用户行为对应小说的相似物品并加权汇总 novel_ids = [b.novel_id for b in behaviors] similarities = ItemSimilarity.objects.filter( novel_id__in=novel_ids ).order_by("-similarity_score")[:200] recommend_map = {} for sim in similarities: # 过滤掉用户已经读过的书 if sim.similar_novel_id in novel_ids: continue recommend_map[sim.similar_novel_id] = ( recommend_map.get(sim.similar_novel_id, 0) + sim.similarity_score ) # 3. 取加权分 TopN top_pairs = sorted( recommend_map.items(), key=lambda x: x[1], reverse=True )[:10] novels = NovelInfo.objects.filter( id__in=[pair[0] for pair in top_pairs] ) novel_map = {n.id: n.novel_name for n in novels} return JsonResponse({ "code": 0, "data": [ {"novel_id": nid, "novel_name": novel_map.get(nid, "")} for nid, _ in top_pairs ], })

cache_page(60 * 5)是这个接口最关键的优化点,它是一个视图级缓存装饰器,把推荐结果按 URL 参数缓存 5 分钟。推荐算法本身是 T+1 更新的,5 分钟内同一个用户的推荐结果不会变化,缓存能显著降低 MySQL 查询压力。

在 Django 中开启缓存需要在settings.py配置CACHES,最常见的配置是指定django.core.cache.backends.locmem.LocMemCache,这是进程内缓存,无需额外部署 Redis 就能先跑通流程。生产环境再替换成 Redis 或 Memcached。

4.4 MySQL 连接池与慢查询排查

Django 默认的数据库连接在每次请求结束后关闭,高并发场景下频繁握手开销很大。settings.py中的CONN_MAX_AGE参数控制连接存活时间,通常设置为 60 秒。

DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "novel_recommend", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "CONN_MAX_AGE": 60, "OPTIONS": { "charset": "utf8mb4", "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", }, } }

init_command设置STRICT_TRANS_TABLES是一个容易踩坑的细节。MySQL 5.7 之后如果数据长度超出字段定义,默认会截断存入而不是报错,开启严格模式后,存储异常数据会直接抛异常,方便在开发期就发现问题。

慢查询排查方面,开启 MySQL 慢查询日志后,重点观察user_behavior表上按用户和时间范围做分组聚合的 SQL。

-- 查看当前慢查询日志状态 SHOW VARIABLES LIKE 'slow_query_log%'; -- 直接分析某条查询是否走到索引 EXPLAIN SELECT user_id, novel_id, AVG(score) FROM user_behavior WHERE created_at > '2024-06-01 00:00:00' GROUP BY user_id, novel_id;

EXPLAIN输出中,type字段如果是ALL,说明发生了全表扫描,需要确认索引是否命中;如果rows超出实际数据量的十分之一,应当考虑增加复合索引或缩小查询的时间范围。

5. 推荐质量评估与相似度阈值调优

5.1 离线指标评估脚本

协同过滤的推荐效果通常用精确率(Precision)和召回率(Recall)衡量。评估方式是把用户行为数据按时间切分,前 80% 作为训练集,后 20% 作为测试集,用训练集构建相似度矩阵,然后看推荐结果能否覆盖测试集中用户真正发生行为的小说。

import random from collections import defaultdict # 按用户划分训练集与测试集 def split_dataset(records, test_ratio=0.2): test_data = defaultdict(set) train_data = defaultdict(set) for user_id, novel_id in records: if random.random() < test_ratio: test_data[user_id].add(novel_id) else: train_data[user_id].add(novel_id) return train_data, test_data # 计算推荐结果的精确率与召回率 def evaluate_recall(train_data, test_data, recommender, top_k=10): hits = 0 total_test = 0 total_recall = 0 for user_id in test_data: rec_items = recommender(user_id, top_k) test_items = test_data[user_id] hit_count = len(set(rec_items) & test_items) hits += hit_count total_test += len(test_items) total_recall += len(rec_items) precision = hits / total_recall recall = hits / total_test return precision, recall

评估脚本存在的意义不是拿到一个好看的指标数字,而是验证调整相似度阈值对推荐结果的影响。小说推荐场景中用户行为噪声较大,用户随手点了但并没有读的小说也会被记录为正向行为,所以精确率普遍低于电商推荐,单次实验的数值波动也较大。建议至少跑 5 次取均值,并固定随机种子以便复现。

5.2 K 值与相似度阈值的联动调整

Item-CF 有两个关键超参:相似度矩阵中每个物品保留的相似邻居数 K,以及参与推荐聚合时相似度分数的下限阈值。K 值太大,引入的弱关联物品太多,推荐结果趋于热门;K 值太小,覆盖率不够,长尾小说完全无法被曝光。

K 值推荐结果特征精确率趋势覆盖率趋势
10强关联,结果集中偏高偏低
30长短结合,稳定适中适中
50长尾暴露多稍降上升
100接近全局热度排序显著下降最高

实操中不要只看精确率,要结合推荐列表里热门小说与冷门小说的比例来做判断。热门小说的行为数据量大,相似度计算稳定,冷门小说行为数据稀疏,相似度分数噪声大。K 值从 10 调到 30,往往会在精确率只有 2% 左右下降的情况下,让冷门小说的曝光量提升一倍,这对提升用户体验是更重要的改善。

5.3 数据失衡的排查手段

推荐结果里如果大量出现某一类目的小说,比如玄幻占比 70%,问题不在算法,而在行为数据分布。小说网站的阅读量天然呈现长尾分布,头部作品占据了绝大多数行为记录,Item-CF 的相似度聚合会让热门物品的“邻居”也集中到热门类目。

排查方式有两种。第一种是看相似度矩阵中每个类目的平均相似度分数,类目内平均分数明显偏高的,说明该类目的小说占据了相似度矩阵的主体。第二种是在推荐接口返回结果里按类目做统计,确认推荐列表和全站阅读分布的偏离程度。如果偏离过大,可以在相似度聚合后加一个类目均衡的加权项,但要注意不能为了均衡牺牲准确率。

另一个常见问题是行为日志中的异常值:某个用户的日志里存在 1 分钟内对 100 本小说产生点击,这种机器行为或脚本行为会污染相似度矩阵。建议在数据预处理阶段设置行为上限,例如单日行为小说数超过 30 本的用户直接过滤,不参与矩阵构建。

6. 冷启动与混合推荐的工程化落地

冷启动是协同过滤无法回避的短板。新用户没有行为数据,系统无法计算个性化推荐;新小说没有行为记录,无法被任何相似度链路捕捉到。工程化的解法是把协同过滤与简单的规则策略结合起来做混合推荐。

新用户的推荐策略是阶梯式的:第一层返回全站热门小说 TopN,第二层记录用户在前 3 天的浏览行为,第三层在用户行为数超过 5 条后切换到协同过滤推荐。这个策略在代码里只需要在推荐接口中增加一个分支判断。

新小说的冷启动相对隐晦,因为新书没有行为数据,只能依托内容特征做补救。常见做法是给新小说打上类目标签,在相似度聚合阶段,如果候选物品是入库时间不足 7 天的内容,额外补一个小幅权重加分。这个方案效果有限,但它至少保证了新书有机会出现在推荐列表中,等待后续行为数据积累后回归正常计算链路。

相似度矩阵的更新策略也值得细化。每日全量重建在数据量小的时候没有问题,但小说数据量到几万本之后,全量计算耗时超过半小时,更新频率就变得敏感。我一般会把矩阵更新拆成两部分:每日凌晨对全量数据做一次完整重建,白天只对新增行为数据做增量修正,修正方式是读入相似度表中受影响行的记录,用新的行为分数替换旧值。这个方案比全量重建快很多,代价是相似度精度略有下降,但推荐系统本身是滞后指标,精度损失在可接受范围内。

验证推荐链路是否正常工作时,可以用一个简单技巧检查数据流:选一个测试用户账号,指定行为日志表里该用户最近读过的某本小说,然后看推荐接口返回结果中是否包含与这本小说同作者的其它作品。如果包含,说明行为数据、相似度矩阵、Django 接口三层链路是通畅的;如果不包含,优先排查相似度表中similar_novel_id的写入逻辑,多数情况下是离线计算脚本里行列转置时索引错位导致的。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询