
简介这份演示PPT主题为“爬虫HadoopSparkDjango美妆大数据分析可视化系统”是一份面向高校毕业设计答辩场景的演示文稿。内容从大数据时代的选题背景出发贯穿Python与Scrapy爬虫抓取美妆评论、销售数据Hadoop中HDFS与MapReduce完成存储和清洗Spark实现高效计算再到MySQL持久化与Django门户可视化展示的完整流程能够帮助读者梳理系统架构、技术选型和答辩主线。资源包由1个pptx文件组成大小约24.49MBPPT内包含研究背景、关键字与目录、框架简介、系统实现与界面展示、参考文献等模块结构清晰方便直接演示或进一步改造。该资源已有129人学习适合大数据方向学生作为毕业答辩PPT参考、课程设计展示也适合需要快速搭建爬虫与大数据可视化讲解思路的开发者使用。1. 这条链路答辩时最怕被问倒的不是算法大四做毕设、工程师转行练手、甚至公司内部的技术分享很多人都会把“爬虫HadoopSparkDjango美妆大数据分析可视化系统”做成一套全栈演示爬虫把小红书、微博、电商评论里的美妆内容抓下来存进 Hadoop 的 HDFS再用 Spark 做清洗和聚合最后用 Django 搭一个带可视化图表的 Web 站点。但答辩或评审时最常被问倒的往往不是某个机器学习算法而是几个“听起来很基础”的问题数据到底存在 HDFS 的哪个目录Spark 的 task 数你是怎么定的Django 的图表接口是实时查 HDFS 还是走了缓存本文要把这条链路的每个断点接上给出一套能当场跑通、讲得清原理的最小实现方案适合把项目当作品集或答辩演示的开发者。2. 爬虫采集把美妆数据从线上海量页面变成结构化语料2.1 选型先想清楚Requests 脚本和 Scrapy 框架的取舍常见做法是先用 Requests 写一次性脚本快速验证页面结构再迁移到 Scrapy 做规模化采集。开始实战之前先选型。目标站点如果是静态 HTML 页面比如某些电商的评论列表、品牌官网的图文资讯直接用 Requests BeautifulSoup 就够了代码量小断点调试方便。但美妆数据通常分散在多个频道图文帖、短视频简介、商品评论、成分表结构差异大而且往往有翻页、懒加载、反爬校验。这种场景下用 Scrapy 更合理——它有内置的并发调度、去重、重试、中间件机制后续要扩展成分布式爬虫也顺理成章。对于爬虫抓取的数据要提前设计好 Item 字段。美妆数据一般包含以下字段字段名类型说明product_namestring商品名称brandstring品牌用于后续 Spark 分组统计categorystring品类如口红/眼影/面霜pricefloat价格用于价格区间分析ratingfloat用户评分清洗时做缺失值处理commentstring评论正文用于词频分析和情感判断publish_timedatetime发布时间用于时间趋势分析sourcestring数据来源站点标记2.2 用 Scrapy 搭一个可断点续爬的美妆数据采集器安装和初始化项目的过程属于基础工程操作但有几个细节会影响后续链路的质量。pip install scrapy scrapy startproject beauty_crawler cd beauty_crawler scrapy genspider beauty_comment example.com启动后在 items.py 里定义字段这与上面表格对应。spiders 目录下的 beauty_comment.py 是核心采集逻辑import scrapy from beauty_crawler.items import BeautyCommentItem class BeautyCommentSpider(scrapy.Spider): name beauty_comment allowed_domains [example.com] start_urls [https://example.com/beauty/reviews] def parse(self, response): # 提取单个商品评论卡片 for card in response.css(div.review-card): item BeautyCommentItem() item[product_name] card.css(a.product-name::text).get() item[brand] card.css(span.brand::text).get() item[category] card.css(span.category::text).get() # 价格字段有可能为空字符串必须做类型转换前的判断 price_text card.css(span.price::text).get() item[price] float(price_text) if price_text else None item[rating] card.css(span.rating::attr(data-score)).get() item[comment] card.css(div.comment-body::text).get() item[source] example.com yield item # 模拟翻页逻辑这里用请求头中的 cookie 片段做示例 next_page response.css(a.next::attr(href)).get() if next_page: yield scrapy.Request( urlresponse.urljoin(next_page), callbackself.parse, headers{Cookie: sessionidxxx} )运行前打开 settings.py把并发调低、延时调高这是初学阶段最容易忽略的细节。把 DOWNLOAD_DELAY 设为 1.5 秒到 3 秒CONCURRENT_REQUESTS 设为 8 以下。这样做的目的是避免因为请求频率过高被服务端封禁 IP影响整个采集任务的中断率。开爬用scrapy crawl beauty_comment -o comments.csv即可输出 CSV 后后续 Spark 才能直接消费。提示爬虫的合规边界要提前确认。robots.txt 必须遵守仅采集公开数据采集频率控制在不影响目标站点正常服务的水平更不要涉及个人隐私信息。答辩环节如果被问合规性答“遵守 robots 协议、只抓公开页面、数据仅用于学术演示”是稳妥的表述。2.3 把关数据质量去重、过滤、落地格式统一真正的线上数据从来不是干净的。同一款“口红”会出现“口红-哑光”“口红 03 号色”等变体价格字段偶尔混入“待定”“暂无报价”等非数字文本。处理原则是在采集层尽量清洗但不做重活儿复杂的清洗和归一到 Spark 层做。在 Scrapy 的 Pipeline 里我一般做三件事精确去重、空白剔除、字段类型预转换。from itemadapter import ItemAdapter import hashlib class DuplicatesPipeline: def __init__(self): self.seen set() def process_item(self, item, spider): adapter ItemAdapter(item) # 用 product_name comment 前50字符 做指纹防止重复评论进入统计 fingerprint hashlib.md5( f{adapter.get(product_name)}-{adapter.get(comment)[:50]}.encode(utf-8) ).hexdigest() if fingerprint in self.seen: raise DropItem(fDuplicate item found: {fingerprint}) self.seen.add(fingerprint) return item这段关键代码的思路是先把空评论、重复评论挡在爬虫阶段否则这些脏数据进入 HDFS 后Spark 清洗时要多写一大堆当过滤条件。字段类型预转换的意思是在 item 上直接存 float 而非字符串避免 Spark 读 CSV 时类型推断出错。数据落地格式推荐 JSON Lines 而不是标准 JSON 数组原因存储和逐行读取都更友好Spark 读 JSON Lines 可以直接按行解析不需要先加载整个大数组到内存。3. Hadoop 归档把 CSV 和 JSON 变成 HDFS 上的可计算数据资产3.1 数据落 HDFS 前目录分区是第一步规范Spark 读 HDFS 时如果数据是按照日期或品类分目录存储就能利用分区裁剪大幅减少扫描量。HDFS 上的目录结构我习惯这样设计/user/hadoop/beauty/ ├── raw/ # 原始数据区源头数据不做任何加工 │ ├── 2024-01-01/ │ ├── 2024-01-02/ │ └── ... ├── clean/ # Spark 清洗后的数据区 │ ├── product_info/ │ └── comment_clean/ └── agg/ # 聚合结果区给 Django 查询用 ├── brand_top10/ ├── price_distribution/ └── keyword_trend/手动用 hdfs 命令创建目录后把爬虫产出的原始 JSON 文件批量传入hdfs dfs -mkdir -p /user/hadoop/beauty/raw/2024-01-01 hdfs dfs -put comments_20240101.json /user/hadoop/beauty/raw/2024-01-01/这样做的好处是当 Spark 作业只统计某一天的增量数据时可以直接指到对应分区路径避免把半年数据全扫一遍。数据量级在几百 MB 以内时伪分布式或单机版 Hadoop 完全够用不必盲目上三节点集群答辩演示单节点反而更容易把资源问题讲清楚。3.2 HDFS 参数配置里真正影响后续的是这两个值Hadoop 安装完成后需要修改 hdfs-site.xml 里的几个参数。默认配置能跑通但数据量上来之后性能差距明显参数推荐值说明dfs.replication2 或 3单机伪分布式设为 1 即可三节点集群设为 2 可以省一半存储dfs.blocksize128m 或 256m默认 128mSpark 读取时按块划分 input split块太小会增大 task 调度开销dfs.namenode.handler.count50默认 10并发上传文件多时有明显瓶颈修改完后重启 HDFS 相关进程使参数生效。这里有一个经常被忽略的细节Spark 作业读取 HDFS 上大量小文件时task 数量会随之膨胀每个文件至少要启动一个 task 来读。解决方案是在原始数据进入 HDFS 前把爬虫输出的多个小 JSON 合并成更大的文件或者用 Spark 在清洗阶段读一次再 coalesce 落盘。伪分布式环境里我一般让爬虫每 1 小时输出一个块避免几千个小文件直接怼进 HDFS。3.3 Spark 读 HDFS 的常见坑CSV Schema 推断成本高当数据以 CSV 格式存储时Spark 读取需要指定 schema否则它会对每一列做类型推断多次扫描文件数据。对美妆评论这类文本比重大的数据来说推断成本更高。规范做法是在 Spark 代码里显式声明 schemafrom pyspark.sql.types import StructType, StructField, StringType, FloatType, IntegerType schema StructType([ StructField(product_name, StringType(), True), StructField(brand, StringType(), True), StructField(category, StringType(), True), StructField(price, FloatType(), True), StructField(rating, FloatType(), True), StructField(comment, StringType(), True), StructField(publish_time, StringType(), True), StructField(source, StringType(), True) ]) df spark.read.schema(schema).option(multiLine, true).json( hdfs://localhost:9000/user/hadoop/beauty/raw/2024-01-01/*.json )这样的好处有两点一是省去类型推断的时间二是当 price 字段里混入“暂无报价”这类脏数据时能直接让 Spark 读入为 null后续清洗就统一按 null 处理。HDFS 阶段到这里数据已经具备“可计算”的形态下一步进入 Spark 做聚合分析。4. Spark 清洗与聚合在哪一步做用什么算子结果存哪4.1 ETL 的设计边界清洗、归一、衍生字段三层分开Spark 作业是整个分析系统里逻辑最重的部分也是答辩时最容易被深挖的部分。很多同学喜欢在 Spark 里把清洗、分析、结果落库全部写在一个大脚本里运行能跑通但被问到“每个步骤的输入输出是什么”时容易卡壳。规范做法是把 ETL 拆成三层第一层清洗处理空值、去重、格式统一第二层归一把“YSL”“圣罗兰”映射成统一品牌名把“口红色号#03”“03号色”归入口红品类第三层衍生计算评分区间、价格档次、评论长度等新字段用于后续可视化。from pyspark.sql.functions import col, when, length, regexp_replace, lower # 第一层清洗 clean_df df.dropDuplicates([product_name, comment]) \ .filter(col(comment).isNotNull()) \ .filter(length(col(comment)) 5) # 第二层归一用 when 做字典式替换生产环境可以替换成维表 join normalized_df clean_df.withColumn( brand_clean, when(lower(col(brand)).contains(ysl), YSL) .when(lower(col(brand)).contains(圣罗兰), YSL) .when(lower(col(brand)).contains(欧莱雅), L\OREAL) .otherwise(col(brand)) ) # 第三层衍生 result_df normalized_df.withColumn( price_level, when(col(price) 100, 0-100) .when(col(price) 300, 100-300) .when(col(price) 500, 300-500) .otherwise(500) ).withColumn( comment_len, length(col(comment)) )逻辑说明这段代码逐层对 DataFrame 做不可变转换每一行代码的输入输出边界清晰答辩时可以直接指着每一步讲它对下游分析的收益。其中dropDuplicates指定了联合去重字段比全字段去重更贴合真实场景——同一用户对同一商品可能发多条不同评论但完全相同的重复评论一定是采集异常。when().otherwise()实现字典式映射比replace()更灵活可以叠加多个条件分支。4.2 Spark 集群参数怎么定executor 内存与并行度Spark 任务跑得慢很大概率不是代码逻辑问题而是资源参数不合理。在提交作业时要同时考虑数据量级和集群资源。以一个 4 核 8GB 内存的测试节点为例spark-submit \ --master spark://localhost:7077 \ --executor-memory 4g \ --num-executors 2 \ --executor-cores 2 \ --driver-memory 2g \ process_beauty.py参数解析--executor-memory是每个执行器进程可用的 JVM 堆内存大小存储评论数据时主要消耗在 DataFrame 的列式存储上Text 类型字段越多样存开销越大所以不能只参考原始文件大小。--num-executors决定整体并行度上限实际并行度还受数据分区数影响建议分区数大于等于 executor 核数的 2 到 3 倍这样才能让 CPU 在 task 调度切换中保持满载。--driver-memory给 driver 进程它要持有任务调度信息数据量大时会存在 OOM 风险。Spark 的内存参数里还有一个易被忽略但不影响主流程的细节spark.sql.shuffle.partitions默认是 200但小数据集上设 200 反而浪费调度资源。我会在代码开头显式设置spark.conf.set(spark.sql.shuffle.partitions, 8)设置后groupBy 或 join 产生的 shuffle 分区数从 200 降为 8减少大量空 task 的调度开销。答辩时这个参数是很好的加分点因为它说明你对 Spark 内存和调度机制有实际调优经验而不只是跑通了默认配置。4.3 三个答辩必备的聚合结果集落地清洗和衍生完成后要做三个与可视化页面一一对应的聚合结果并写入 HDFS 的 agg 目录。这样 Django 端不直接执行 Spark 计算只读结果数据接口响应速度会快很多。常见的聚合计算如下result_df.createOrReplaceTempView(beauty) # 品牌讨论热度 Top10按评论数排序 brand_top10 spark.sql( SELECT brand_clean, COUNT(*) AS cnt FROM beauty GROUP BY brand_clean ORDER BY cnt DESC LIMIT 10 ) brand_top10.write.mode(overwrite).parquet( hdfs://localhost:9000/user/hadoop/beauty/agg/brand_top10 ) # 价格区间分布 price_dist spark.sql( SELECT price_level, COUNT(*) AS cnt FROM beauty GROUP BY price_level ) price_dist.write.mode(overwrite).parquet( hdfs://localhost:9000/user/hadoop/beauty/agg/price_distribution ) # 评分与评论长度关系用于散点图展示 score_len spark.sql( SELECT rating, AVG(comment_len) AS avg_len FROM beauty WHERE rating IS NOT NULL GROUP BY rating ) score_len.write.mode(overwrite).parquet( hdfs://localhost:9000/user/hadoop/beauty/agg/score_comment_len )结果用 Parquet 列式存储而不是 CSV原因是 Django 读取时只需要取少数列Parquet 的列裁剪能大幅减少 IO 开销。比如均价和人数两列CSV 格式必须读完整行文本再解析Parquet 只需要读对应列的数据块在结果集达到千万行量级时差距会变得十分明显。5. Django 可视化输出REST 接口 ECharts 图表的连法5.1 Django 的 MTV 分层在可视化系统里的实际映射被问到“Django 的 MTV 模式有什么用”回答不是背书而是指出这个系统里的具体映射M 对应 model 层T 对应模板V 是视图函数。在可视化场景中不需要 Model 参与数据写入直接用视图函数读取 Spark 聚合结果再通过 JSON 接口传给前端模板渲染。先创建 app 并配置 URLpython manage.py startapp dashboard在 dashboard/views.py 里写一个读取 Parquet 结果并返回 JSON 的接口import pandas as pd from django.http import JsonResponse def brand_top10(request): # 用 pandas 读取 Parquet 文件注意需要 pyarrow 或 fastparquet 引擎 df pd.read_parquet(/user/hadoop/beauty/agg/brand_top10, enginepyarrow) data [ {brand: row[brand_clean], cnt: int(row[cnt])} for _, row in df.iterrows() ] return JsonResponse({code: 0, data: data})内容说明这段代码把 Spark 算好的结果用 pandas 读出来转成结构化字典数组再序列化为 JSON 返回。这里有个值得注意的工程取舍——如果直接让 Django 去调 spark-submit 或 SparkSession每次页面刷新都触发一次分布式计算响应时间会到秒级甚至分钟级对演示场景极不友好。预先算好结果、Django 只做读取是这类可视化系统的常见架构。5.2 StreamingHttpResponse 的 content_type 在导出场景的正确用法答辩时可能需要演示“导出排行榜 Excel”的功能这时要用到 Django 的 StreamingHttpResponse。核心参数 content_type 定义响应体的 MIME 类型content_disposition 定义浏览器如何呈现这个响应。常见错误是把两者写混比如 Excel 文件用了 text/html导致浏览器直接打开乱码。import csv from django.http import StreamingHttpResponse def export_brand_ranking(request): df pd.read_parquet(/user/hadoop/beauty/agg/brand_top10, enginepyarrow) def csv_rows(): yield [品牌, 评论数] for _, row in df.iterrows(): yield [row[brand_clean], str(row[cnt])] response StreamingHttpResponse( csv_rows(), content_typetext/csv ) response[Content-Disposition] attachment; filenamebrand_top10.csv return response逻辑说明这段代码用生成器逐行产生 CSV 内容StreamingHttpResponse 拿到生成器后逐个 chunk 发送给浏览器不用把整个文件加载到内存再一次性返回数据量大时这个区别很重要。Content-Disposition设置为 attachment 会触发浏览器下载行为filename 参数指定保存的文件名。如果只需要在页面上展示 CSV 的纯文本内容把 attachment 换成 inline 即可浏览器会直接渲染为文本。提示Django 生产环境部署最好不要用自带的 runserverwaitress nginx 是常见组合。waitress 是多线程 WSGI 服务器能顶住并发请求nginx 负责静态文件转发和反向代理。两者配合下可视化页面的加载速度会有明显提升。5.3 页面接入 ECharts 的模板写法前端部分直接引用 ECharts 的 CDN在 Django 模板里写一个 div 容器用 fetch 请求接口数据再 setOption 渲染图表。这里给出核心片段div idbrandChart stylewidth: 100%; height: 400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script fetch(/api/brand_top10/) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(brandChart)); chart.setOption({ title: { text: 美妆品牌讨论热度 Top10 }, tooltip: {}, xAxis: { type: category, data: data.data.map(d d.brand) }, yAxis: { type: value }, series: [{ type: bar, data: data.data.map(d d.cnt) }] }); }); /script这里要重点说明的一点是Django 模板渲染只是把页面骨架返回给浏览器图表数据完全通过异步请求获得。这样做的好处是接口和页面完全解耦后端调整聚合逻辑时前端模板无需改动。如果在答辩现场网络受限可以把 echarts.min.js 下载后放到 Django 的 static 目录里引用避免演示时因外网不通导致图表白屏。6. 三个答辩现场的验证技巧数据校验、性能预演、状态恢复到了演示验证阶段真正重要的不是功能又多又花哨而是每个环节的数据都能被当场验证。第一个技巧是随机抽查 HDFS 上的原始数据与 Spark 计算结果是否一致。从原始 JSON 里随机挑一条评论比如“这款口红的哑光质感很好”去 Django 页面的关键词统计模块里搜这个词如果能找到对应条目说明整条链路是贯通的。这一步能有效回应“你的数据是造的吧”这类问题也能暴露清洗逻辑中误删数据的隐患。第二个技巧是性能预演。答辩环境往往不是本机网络和磁盘性能都不可控。提前在 Django 页面加载前预建设置缓存——把接口查到的结果存到 Redis 里设置过期时间 10 分钟。这样演示时重复刷新页面不会触发重复的 Parquet 读取和 JSON 序列化响应能压缩到几十毫秒。一个工程师如果能在性能上多备一手展示出来的数据可以是同一条曲线但现场的流畅度会被评委记住。第三个技巧是状态恢复。演示现场经常出现断电、断网、接口报错等情况所以要准备一份手动启动文档Hadoop 需要先启动 HDFS 的 namenode 和 datanode 进程Spark 需要确认 standalone 集群的 master 和 worker 状态Django 服务要确认端口未被占用。用一条检查命令把三个服务状态一次性列出来比对jps | grep -E NameNode|DataNode|Master|Worker sudo netstat -tlnp | grep 8000如果输出里少了任何一项对照启动命令逐项恢复。这些细节平时不起眼但在演示场合往往是决定成败的最后一步。与其指望临场发挥不如把它们整理成一段可以盲打的启动脚本让整套“爬虫HadoopSparkDjango美妆大数据分析可视化系统”随时处于可演示状态。本文还有配套的精品资源点击获取