大数据旅游景点数据分析与可视化:Spark 链路与大屏实践
发布时间:2026/9/17 12:50:55来源:尧图网络
简介这是一份面向计算机、数据科学相关专业学生及毕业设计写作者的论文资料围绕基于大数据技术的热门旅游景点数据分析与可视化系统展开。文档涵盖研究背景与意义、数据采集与预处理、统计分析与机器学习建模、Django 交互式可视化实现、结果解释与预期成果等章节并给出 Hadoop、Spark、Scrapy、BeautifulSoup、MySQL、ECharts 等工具选型与实施步骤能帮助读者解决选题定位、技术路线设计和论文结构组织等问题摘要、关键词及英文摘要齐全便于借鉴系统架构与图表、地图、仪表盘设计。包内仅 1 个 docx 文档约 2.19MB内容完整、查阅方便。目前已有 529 人学习下载适合需要快速形成毕设或课设方案、参考数据分析与可视化全流程写作范式的读者。1. 热门旅游景点数据分析项目里大数据技术到底解决什么问题五一假期前一周一个做文旅数据看板的朋友找我聊某网红城市的景点评论数据从几十万条涨到三千万条原来用 Pandas 跑的脚本从二十分钟变成六小时还经常内存溢出。这不是代码写得差是单机处理模式到了数据量拐点。基于大数据技术的热门旅游景点数据分析与可视化要解决的就是这个拐点之后的事——把 OTA 评论、门票订单、搜索指数、客流统计这几路数据统一进分布式存储用 Spark 做清洗和热度指标计算最后把热度排名、客流趋势、评论词云推到可视化大屏上。做数据科学与大数据技术毕设的同学拿这条链路练手能一次覆盖采集、存储、计算、可视化四个环节已经工作的后端或数据分析师接手文旅项目也能直接复用这套组件组合。重点不在于工具多新而在于每一段都有明确的输入输出和可量化的指标别把大数据做成堆组件。2. 旅游景点数据分析的技术链路与大数组件选型把旅游景点分析拆开看数据从产生到进大屏中间要过四道关采集、存储、计算、输出。每道关的工具选择不需要追新但必须匹配数据量和时效要求。一个日均评论增量在几十万条、历史存量过亿的文旅项目和一个小城市几十个景点的日度报表项目完全用不上同一套架构。常见做法是先确认三个数——存量数据规模、增量频率、查询响应时间——再决定组件组合。下面按数据源、计算引擎、存储层、集群规格四个角度分别说清楚。2.1 从数据来源倒推OTA 评论、门票订单、搜索指数各走哪条链路旅游景点的数据来源大致分四类每类的采集方式和数据特征都不一样。OTA 平台的景点评论和评分是核心热度信号。这类数据通常通过公开 API 或者合规的页面采集获取数据结构是半结构化的 JSON字段包括景点 ID、用户 ID、评分、评论正文、发布时间。评论正文需要做分词和情感倾向判断属于文本分析范畴几千万条评论不可能逐条人工看必须走批量处理。门票订单数据一般来自景区自己的票务系统是结构化最好的一类字段固定订单号、景点 ID、票种、数量、金额、下单时间、核销时间。这类数据直接走数据库同步或者 CDC变更数据捕获进数仓不需要太多清洗。搜索指数反映的是潜在客流来自搜索引擎或者地图应用的热度接口通常是按天或者按小时聚合的数值型数据量不大但更新频率高适合直接入库。客流统计数据来自景区闸机或者手机信令脱敏后的聚合结果精度从小时级到天级不等。信令数据体量大一天一个中型城市能产生几千万条记录适合直接进分布式文件系统做批处理。这四类数据在进入计算引擎之前先要统一到同一个数据模型上。我一般会定义一张宽表景点 ID、日期、省份、城市、评分均值、评论数、订单数、订单金额、搜索热度、客流量。后面的所有分析和可视化都基于这张表或者它的衍生表避免每个图表各查一套逻辑。2.2 Hadoop、Spark、Flink 在旅游数据场景的选型对比大数据技术原理与应用这门课里讲得最多的三个计算组件就是 Hadoop MapReduce、Spark 和 Flink。放在旅游景点数据分析这个具体场景下三者的适用边界其实很清楚。对比维度Hadoop MapReduceSparkFlink计算模型磁盘批处理内存批处理/微批流原生流处理延迟分钟到小时级秒到分钟级毫秒到秒级适合的旅游数据任务历史全量归档统计日度热度指标计算、评论情感分析实时客流监控、大屏实时刷新开发语言Java 为主Scala/Python/JavaJava/Scala与 Hive 集成原生好一般学习成本中低PySpark 友好高对于数据科学与大数据技术毕设或者中小型文旅项目我的建议是主计算用 Spark。原因是 PySpark 的 DataFrame API 对 Python 用户足够友好清洗、聚合、关联这些操作几行代码就能写完调试也方便。Flink 更适合大屏上的实时客流曲线但引入 Flink 意味着多维护一套集群和状态管理如果只是做日度或者小时级更新Spark Structured Streaming 的微批模式就够用。Hadoop 的 MapReduce 现在很少直接写但 HDFS 作为底层存储和 YARN 作为资源调度仍然在很多集群里跑着Spark on YARN 是很常见的部署方式。2.3 存储层设计HDFS、Hive、MySQL、Redis 各存什么存储层的分工原则是原始数据进 HDFS结构化数仓表进 Hive对外查询结果进 MySQL需要快速读取的维度数据放 Redis。HDFS 存原始数据按日期分区路径格式一般是/warehouse/tourism/ods/comment/dt2025-01-01/。这样做的目的是保留原始数据用于回溯如果后面的清洗逻辑有 bug可以重新从 ODS 层算一遍不用再去采集一次。Hive 建数仓分层表ODS 层对应原始数据DWD 层做清洗和标准化DWS 层做轻度聚合ADS 层直接对可视化提供结果。建表时注意分区字段和存储格式。-- ODS 层原始评论数据按日期分区用 Parquet 列式存储压缩比更好 CREATE TABLE ods_comment ( comment_id STRING COMMENT 评论唯一ID, spot_id STRING COMMENT 景点ID, user_id STRING COMMENT 用户ID, score DOUBLE COMMENT 评分1-5, content STRING COMMENT 评论正文, publish_time STRING COMMENT 发布时间 ) COMMENT OTA景点评论原始表 PARTITIONED BY (dt STRING COMMENT 日期分区) STORED AS PARQUET TBLPROPERTIES (parquet.compressionSNAPPY); -- ADS 层景点热度日汇总表直接对接可视化查询 CREATE TABLE ads_spot_heat_daily ( spot_id STRING COMMENT 景点ID, spot_name STRING COMMENT 景点名称, city STRING COMMENT 所在城市, avg_score DOUBLE COMMENT 平均评分, comment_cnt BIGINT COMMENT 评论数, order_cnt BIGINT COMMENT 订单数, order_amount DOUBLE COMMENT 订单金额, heat_score DOUBLE COMMENT 综合热度分 ) COMMENT 景点日度热度汇总表 PARTITIONED BY (dt STRING) STORED AS PARQUET;ODS 层用 Parquet 列式存储压缩方式选 SNAPPY因为旅游评论数据里文本字段占大头列存对“只读评分和景点 ID”这类查询能减少大量 IO。分区字段 dt 放在最后Hive 会按日期切目录查询时加WHERE dt2025-01-01就能做分区裁剪。MySQL 存 ADS 层的结果表和维度表给可视化后端做查询。Redis 缓存景点名称映射、城市列表这类高频访问的小表以及大屏首页的聚合数字。这也是 redis 可视化管理工具里常见的 Redis 用法——不是拿来存大数据的是拿来加速那些每秒被查几百次的小查询。2.4 集群最小规模与资源配置建议一套能跑通旅游景点分析全链路的集群最低配置可以压到三台机器。一台做 NameNode 和 ResourceManager另外两台做 DataNode 和 NodeManager。角色数量单机配置说明主节点18 核 16G500G SSDNameNode ResourceManager Hive Metastore MySQL从节点28 核 32G1T HDDDataNode NodeManager跑 Spark Executor可视化服务14 核 8G100G SSD可复用主节点跑 Nginx 后端 API RedisSpark Executor 的内存建议设成从节点物理内存的 60% 到 70%留出空间给操作系统和 DataNode。按 32G 内存算单节点可以起 3 个 Executor每个 Executor 分 6G 内存driver 如果跑在 YARN 上再单独留 4G。小项目不建议一上来就搞高可用NameNode 单点虽然不优雅但能跑等数据量到 TB 级再加 Standby 节点也不迟。3. 用 Spark 做景点数据清洗与热度指标计算从 ODS 层的原始评论表到 ADS 层的热度汇总表中间要经过清洗、标准化、多表关联和指标计算四步。这一段是整条链路里最需要关注细节的部分——清洗规则定得不对后面算出来的热度排名就会失真多表关联的 join 键选得不好Spark 跑起来会倾斜到某个 Executor 卡死。下面按清洗、指标定义、PySpark 实现、落库调优的顺序来说。3.1 景点评论数据的清洗规则OTA 评论数据的脏法很有规律常见的有四类评分异常空值、0 分、超过 5 分、重复评论同一用户对同一景点短时间反复发、广告灌水正文里带手机号、微信号、文本噪声HTML 标签、多余空白、乱码。清洗规则要逐条落到代码里不能只靠肉眼。from pyspark.sql import SparkSession, functions as F from pyspark.sql.window import Window spark SparkSession.builder \ .appName(tourism_clean) \ .config(spark.sql.adaptive.enabled, true) \ .config(spark.sql.adaptive.coalescePartitions.enabled, true) \ .enableHiveSupport() \ .getOrCreate() # 读 ODS 层某天分区 raw spark.sql(SELECT * FROM ods_comment WHERE dt2025-01-01) # 1. 过滤异常评分和空值 step1 raw.filter( (F.col(score).isNotNull()) (F.col(score) 1) (F.col(score) 5) (F.col(spot_id).isNotNull()) ) # 2. 去除 HTML 标签和多余空白 step2 step1.withColumn( content_clean, F.regexp_replace(F.col(content), [^], ) ).withColumn( content_clean, F.regexp_replace(F.col(content_clean), \\s, ) ) # 3. 过滤广告正文里带手机号或微信的评论直接丢弃 ad_pattern r(1[3-9]\\d{9}|微信|加V|QQ) step3 step2.filter(~F.col(content_clean).rlike(ad_pattern)) # 4. 按用户景点日期去重保留发布时间最新的一条 w Window.partitionBy(user_id, spot_id, F.to_date(publish_time)) \ .orderBy(F.col(publish_time).desc()) step4 step3.withColumn(rn, F.row_number().over(w)) \ .filter(F.col(rn) 1) \ .drop(rn) step4.write.mode(overwrite).insertInto(dwd_comment)spark.sql.adaptive.enabled打开自适应查询执行让 Spark 在运行时根据数据量自动调整 join 策略和分区数对于旅游评论这种分布不均的数据集效果明显。正则1[3-9]\\d{9}匹配手机号、[^]匹配 HTML 标签注意在 Python 字符串里反斜杠要写两次。去重窗口按user_id spot_id 日期分区排序字段选发布时间倒序这样每个用户在同一个景点下的同一天评论只保留最新一条既去了水军也没丢真实数据。3.2 热度指标拆解评分、点评量、搜索量、客流怎么加权热度的本质是“有多少人关注、到访以及满意度如何”。这四个维度量纲不同需要先标准化再加权。heat_score 0.30 × norm(comment_cnt) 0.35 × norm(order_cnt) 0.20 × norm(search_index) 0.15 × norm(avg_score)其中norm()用 min-max 归一化把每个指标缩放到 0 到 1 之间。权重加起来为 1具体值看业务侧重。指标权重建议说明评论数0.30反映讨论热度订单数0.35反映实际到访意愿权重最高搜索指数0.20反映潜在兴趣平均评分0.15满意度的修正项权重的调法如果只看“现在火不火”订单数权重往上调如果做的是“未来趋势预判”搜索指数权重可以提到 0.35。权重不要拍脑袋定死最好拿历史客流数据做相关性验证看哪个组合的排序和实际客流排名最接近。归一化的时候有个坑如果某个指标的最大值和最小值差距极大比如某个网红景点评论数是普通景点的 100 倍min-max 会把大部分景点压到接近 0 的位置。这时改用对数归一化或者分位数归一化更稳妥。3.3 PySpark 完整实现从 DWD 层到 ADS 层热度表# 读 DWD 层评论按景点日期聚合 comment_agg spark.sql( SELECT spot_id, dt, AVG(score) AS avg_score, COUNT(1) AS comment_cnt FROM dwd_comment WHERE dt2025-01-01 GROUP BY spot_id, dt ) # 读订单聚合结果 order_agg spark.sql( SELECT spot_id, dt, COUNT(1) AS order_cnt, SUM(amount) AS order_amount FROM dwd_order WHERE dt2025-01-01 GROUP BY spot_id, dt ) # 读搜索指数 search_agg spark.sql( SELECT spot_id, dt, SUM(search_index) AS search_index FROM dwd_search WHERE dt2025-01-01 GROUP BY spot_id, dt ) # 多表关联用 left join 防止某个数据源缺失导致景点丢失 merged comment_agg \ .join(order_agg, on[spot_id, dt], howleft) \ .join(search_agg, on[spot_id, dt], howleft) \ .fillna({order_cnt: 0, order_amount: 0.0, search_index: 0}) # 取全局最大最小值用于归一化 stats merged.select( F.max(comment_cnt).alias(max_c), F.min(comment_cnt).alias(min_c), F.max(order_cnt).alias(max_o), F.min(order_cnt).alias(min_o), F.max(search_index).alias(max_s), F.min(search_index).alias(min_s), F.max(avg_score).alias(max_sc), F.min(avg_score).alias(min_sc) ).collect()[0] def norm(col_name, max_v, min_v): if max_v min_v: return F.lit(0.5) return (F.col(col_name) - min_v) / (max_v - min_v) heat merged.withColumn( heat_score, F.round( 0.30 * norm(comment_cnt, stats[max_c], stats[min_c]) 0.35 * norm(order_cnt, stats[max_o], stats[min_o]) 0.20 * norm(search_index, stats[max_s], stats[min_s]) 0.15 * norm(avg_score, stats[max_sc], stats[min_sc]), 4 ) ).orderBy(F.col(heat_score).desc()) # 写 ADS 层 heat.write.mode(overwrite).insertInto(ads_spot_heat_daily)用 left join 而不是 inner join 是为了保证景点不因为某个数据源当天缺数就消失缺失的订单数补 0。归一化用 collect 把最大最小值拉到 driver 端再通过 withColumn 广播回去这样避免了用 join 的方式合并统计结果。如果 min 和 max 相等比如当天只有一条记录直接给 0.5 的中性值避免除零。写完 ADS 层之后可以快速验证结果spark.sql( SELECT spot_name, heat_score, comment_cnt, order_cnt FROM ads_spot_heat_daily WHERE dt2025-01-01 ORDER BY heat_score DESC LIMIT 10 ).show()这个查询的结果就是可视化大屏上热度排行榜的数据来源。3.4 结果落库与 Spark 参数调优ADS 层的结果通常会同步到 MySQL 供前端查询用 Spark JDBC 写入heat.select(spot_id, dt, avg_score, comment_cnt, order_cnt, heat_score) \ .write \ .format(jdbc) \ .option(url, jdbc:mysql://master:3306/tourism) \ .option(dbtable, ads_spot_heat_daily) \ .option(user, etl) \ .option(password, ******) \ .option(batchsize, 5000) \ .option(isolationLevel, NONE) \ .mode(append) \ .save()batchsize设 5000 是因为 ADS 层一天的数据量通常只有几千行设太大反而浪费内存。isolationLevel设 NONE 可以关闭 JDBC 事务提升写入速度因为 ADS 表是按日期 append 的不存在并发冲突。调优参数方面处理旅游评论数据时建议开几个配置参数建议值作用spark.sql.shuffle.partitions200控制 shuffle 后分区数数据量小可以降到 50spark.sql.adaptive.enabledtrue自动调整 join 策略和分区spark.sql.adaptive.skewJoin.enabledtrue自动处理 join 倾斜spark.executor.memory6g单 Executor 堆内存spark.executor.cores2单 Executor 核数和内存比例约 1:3spark.sql.shuffle.partitions默认是 200对于几百万行的旅游数据可能偏大产生太多小文件。开 AQE 之后 Spark 会自动合并小分区但建议还是手动估一下——数据量在 500 万行以内设成 50 到 100 比较合适。4. 旅游可视化大屏的搭建从 ECharts 到实时刷新分析结果跑出来之后最后一步是让人能看懂。旅游景点的可视化大屏通常要展示这几类信息全国或者全省的景点热度分布、Top N 热度排行、评论情感占比、客流趋势曲线、评论关键词词云。前端的核心工具是 EChartsPython 侧可以用 Pyecharts 快速生成图表配置。如果做毕设Pyecharts Flask 的组合最省时间如果是企业级项目更常见的是后端只返 JSON前端用 Vue 或 React 配合 ECharts 官方组件自己渲染。4.1 图表选型热力图、词云、桑基图分别看什么不同类型的旅游数据对应不同的图表选错了不仅难看还会误导结论。数据内容推荐图表理由景点地理位置分布与热度地图热力图直观展示地域集中度Top N 景点热度排行横向柱状图排名一目了然适合大屏评论关键词词云快速感知高频词客流随时间变化折线图趋势清晰适合叠加同比各城市景点数量占比环形图占比关系直观客源地到景点的流动桑基图或飞线图展示来源去向大屏的尺寸通常是 1920×1080横向柱状图和折线图各占三分之一宽度比较合适地图放中间或者左上角做主视觉。4.2 用 Pyecharts 生成景点热度地图和评论词云Pyecharts 是 ECharts 的 Python 封装生成 HTML 文件就能直接在大屏里嵌 iframe 或者通过 Flask 渲染。from pyecharts import options as opts from pyecharts.charts import Map, Bar, WordCloud, Line from pyecharts.globals import ThemeType # 各省景点平均热度地图 province_data [(四川, 82.5), (云南, 78.3), (浙江, 75.1), (广东, 71.4), (北京, 69.8), (陕西, 66.2)] map_chart ( Map(init_optsopts.InitOpts(themeThemeType.DARK, width900px, height500px)) .add(景点热度, province_data, china) .set_global_opts( title_optsopts.TitleOpts(title全国景点热度分布), visualmap_optsopts.VisualMapOpts( min_50, max_90, range_color[#1e3a5f, #f5a623, #d0021b] ) ) ) map_chart.render(heat_map.html) # 评论关键词词云 word_data [(风景优美, 820), (排队久, 450), (性价比高, 390), (讲解专业, 310), (停车难, 280), (适合亲子, 260)] wc ( WordCloud() .add(, word_data, word_size_range[20, 80], shapecircle) .set_global_opts(title_optsopts.TitleOpts(title评论关键词)) ) wc.render(wordcloud.html)visualmap_opts里的range_color决定热力渐变建议用深蓝到橙红的三段色不要用彩虹色大屏上看不清。min_和max_要按实际数据范围设如果设成 0 到 100 而实际数据集中在 60 到 80 之间地图上的颜色差异就看不出来。词云的word_size_range控制最大最小字号大屏上最小不要低于 20px否则后排观众看不清。4.3 大屏适配方案与前端布局大屏适配的核心问题是分辨率不统一。做法是固定设计稿尺寸为 1920×1080然后按浏览器实际宽高算缩放比// 大屏等比缩放保持 16:9 设计稿比例 function resizeScreen() { const designWidth 1920; const designHeight 1080; const scaleX window.innerWidth / designWidth; const scaleY window.innerHeight / designHeight; const scale Math.min(scaleX, scaleY); const container document.getElementById(screen); container.style.transform scale(${scale}); container.style.transformOrigin left top; // 居中偏移 container.style.left (window.innerWidth - designWidth * scale) / 2 px; container.style.top (window.innerHeight - designHeight * scale) / 2 px; } window.addEventListener(resize, resizeScreen); resizeScreen();用transform: scale()而不是改每个元素的宽高原因是缩放后 ECharts 内部的 canvas 坐标不变图表不会因为重新计算布局而变形。transformOrigin设成左上角再通过 left 和 top 把容器推回居中位置。4.4 实时刷新WebSocket 与轮询的取舍大屏上的客流数据如果是分钟级更新用轮询就够如果是秒级刷新WebSocket 更合适。轮询实现简单前端每 30 秒请求一次 API后端从 Redis 或 MySQL 读最新值返回。WebSocket 需要后端保持长连接但对服务器压力更小因为有新数据才推。// 轮询方式每 30 秒拉一次热度排行 setInterval(async () { const res await fetch(/api/spot/heat/top10); const data await res.json(); updateBarChart(data); // ECharts 的 setOption 更新 }, 30000); // WebSocket 方式服务端有新数据时主动推送 const ws new WebSocket(ws://localhost:8080/ws/realtime); ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type heat_update) { updateBarChart(msg.data); } };轮询的坑在于时间间隔设太短会把后端打满建议客流曲线 30 秒、热度排行 5 分钟各设一档不要所有图表都用一个频率。WebSocket 的坑在于断线重连没有默认处理需要自己在onclose里加退避重连逻辑否则大屏跑一夜之后刷新就断了。5. 调度、增量更新与可视化项目的排错技巧链路跑通之后日常运维才是真正花时间的地方。旅游数据每天增量进来如果每次都全量重算不仅浪费资源还会让下游看板延迟。另外可视化项目一旦上线被人围观各种“大屏卡住”“数字不对”的问题就会冒出来。下面几个技巧是实际项目里最常用到的。5.1 用 Airflow 做景点数据每日增量调度Airflow 的 DAG 定义适合把采集、清洗、聚合、推送到大屏串成流水线。关键点是每个任务只处理当天分区不要重复计算历史。from airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime, timedelta default_args { owner: tourism, retries: 2, retry_delay: timedelta(minutes5), start_date: datetime(2025, 1, 1) } with DAG( spot_heat_daily, default_argsdefault_args, schedule_interval0 3 * * *, # 每天凌晨3点跑 catchupFalse ) as dag: extract BashOperator( task_idextract_ota_comment, bash_commandpython /opt/etl/extract_comment.py --date {{ ds }} ) clean_and_agg BashOperator( task_idspark_clean_agg, bash_commandspark-submit --master yarn /opt/etl/heat_calc.py --date {{ ds }} ) sync_mysql BashOperator( task_idsync_ads_to_mysql, bash_commandpython /opt/etl/sync_mysql.py --date {{ ds }} ) extract clean_and_agg sync_mysqlschedule_interval用 cron 表达式0 3 * * *表示凌晨 3 点执行此时 OTA 平台前一天的评论数据已经基本落完。catchupFalse防止 Airflow 补跑历史所有日期第一次上线时特别重要否则会瞬间触发几百个任务把集群打满。{{ ds }}是 Airflow 内置的模板变量代表当前执行日期直接传给脚本作为分区参数。5.2 数据倾斜和 OOM 的排查手法Spark 跑旅游数据最容易遇到两个问题join 倾斜和 driver OOM。join 倾斜的典型表现是某个 task 卡在 99% 不动其他 task 早就完成了。原因通常是热门景点的数据量远大于冷门景点按 spot_id 做 join 时全挤到一个分区。排查方法是看 Spark UI 的 Stage 页面对比各 task 的 shuffle read size差一个数量级就是倾斜。处理方式有三种开 AQE 的自动倾斜处理spark.sql.adaptive.skewJoin.enabledtrue给热门 spot_id 加随机前缀打散后再聚合或者把小表广播出去用 broadcast join 避免 shuffle。driver OOM 通常发生在collect()或者toPandas()的时候。上面算归一化时用了collect()取一行统计值这是安全的但如果对全量结果做collect()几百万行数据全拉到 driver 内存必崩。检查方法是看 driver 日志里的java.lang.OutOfMemoryError后面跟的是 Java heap space 还是 GC overhead limit exceeded。前者调大spark.driver.memory后者说明对象太多改用write落盘而不是collect。5.3 大屏加载性能的 3 个调优参数大屏打开慢八成是数据量和渲染两头的问题。第一个参数是后端接口返回的数据行数。热度排行只返 Top 10 到 Top 20地图只返有数据的省份不要把所有景点全量返给前端。一个 3000 行的 JSON 序列化和传输时间可能比 ECharts 渲染还长。第二个参数是 ECharts 的animationDuration。大屏上默认动画 1000ms 看起来流畅但如果一个页面有七八个图表同时初始化动画会互相抢 GPU。把animationDuration调到 300 到 500ms或者对静态图表直接关闭animation: false。第三个参数是数据刷新频率。前面说过不要所有图表用同一个轮询间隔。另外可以在 Nginx 层给静态资源加缓存把大屏的 JS 和 CSS 缓存时间设成 7 天每次只拉新的 JSON 数据。本文还有配套的精品资源点击获取
网站建设高端定制企业官网