从爬虫到可视化:美妆大数据分析系统全链路实践
2026/9/19 11:34:56 网站建设 项目流程

简介:Hadoop+爬虫+Spark美妆大数据分析可视化系统是一份完整的毕业设计论文,面向计算机、大数据、电子商务等专业本科毕业生,可用于解决毕设选题难、论文框架不清晰等问题,也为美妆行业从业者提供基于公开数据做市场分析的参考案例。资源为单个docx文件,压缩包大小3.85MB,内容包含中英文摘要、目录、绪论、开发环境与工具、技术路线、爬虫设计、数据清洗与存储、可视化分析等章节,论文结构完整,逻辑清晰。目前已有535人学习,在同类毕业设计资料中具有一定的参考热度。论文围绕Python、PyCharm、Scrapy实现美妆相关数据的爬取,结合Hadoop与Spark完成海量数据清洗和计算,使用MySQL进行结果存储,并通过柱状图、折线图、饼图等方式直观展示市场趋势、品牌竞争和消费者偏好。读者既能了解从数据采集、存储、分析到可视化的完整技术链路,也能获得毕业设计写作、系统实现与答辩准备的直接参考,适合用于论文借鉴、项目复现和答辩思路梳理。

1. 不止是爬虫加图表:美妆数据系统的真正工作量

选“潮流美妆大数据分析可视化”做课题的人,多半被“大数据”三个字吓住过。拆开看核心链路并不复杂:Scrapy 抓取、pandas 清洗、MySQL 存储、Spark 批处理、Django 渲染看板,一条线走完。

真正做过一遍会发现,爬虫只占三成工作量,剩下七成在字段设计与数据清洗。字段怎么定、脏数据怎么清、维度表怎么建,这些细节才是答辩时能讲出深度的部分,也决定了分析结果可不可信。

这篇按实际开发顺序拆解,适合正在做同类毕业设计的学生,也适合想快速搭数据分析 Demo 的开发者。命令和参数直接可抄,常见坑一并标注,照着走就能跑通整个链路。

2. 技术选型与数据链路:Scrapy、MySQL、Spark 的职责边界

2.1 B/S 架构与 Django 的选型理由

系统采用 B/S 模式,前台展示与数据处理分层。框架选 Django 而不是 Flask,主要看中三点:自带 Admin 后台,可以用极少量代码完成管理员对用户和美妆信息表的增删改查;ORM 屏蔽了原生 SQL,表结构变更时不需要大面积改查询语句;URLConf 的正则路由在写图表接口时非常灵活,一个路径对应一个视图,排查问题比 Flask 的集中式路由更直观。

版本搭配要留意兼容性。论文环境是 Python 3.6.4,低版本 Django 不支持 Python 3,官方推荐搭配是 Django 3.2.12。如果本机装的是 Python 3.10 以上,Django 建议直接上 4.2,但 3.2 的 LTS 版本对课设来说最稳,网上排错资料最多,踩坑成本最低。

提示:Django 3.2 在 Python 3.10 下会提示兼容性 warning,但不影响运行。想零警告就二选一:Python 降到 3.8,或者 Django 升到 4.2。

2.2 Scrapy 和 Requests 的差距在并发模型

采集层用 Scrapy 而不是 Requests,核心差距在并发模型。Requests 是同步阻塞,一个线程只能等一个请求返回,想提速就得自己开线程池;Scrapy 基于 Twisted 异步事件循环,单进程能维持几十个并发连接,抓美妆列表页这种密集型任务,速度差距在 5 到 10 倍。Scrapy 还自带去重、重试、限速和 Item Pipeline,这些在 Requests 方案里全要手写,写到后面错误处理很难覆盖完整。

新手常犯的错误是把 Scrapy 当 Requests 用,在 parse 里写 for 循环逐个调用 Request,把异步框架写成了同步。正确做法是 yield Request 交给调度器,让框架自己维护并发队列。另外 scrapy 的 Request 默认会去重,翻页 URL 带动态参数时需要注意指纹计算方式,否则页面特征变了也抓不到新数据。

2.3 Hadoop 与 Spark 在链路里的真实位置

很多课设在 Hadoop 和 Spark 上只是装个环境跑 wordcount,这是本末倒置。这套系统里它们的定位是:Hadoop 提供 HDFS 做原始数据的备份存储,Spark 对 MySQL 导出的明细数据做批量聚合,计算品牌销量份额、价格带分布、评分趋势这些指标。单机环境下 Hadoop 用伪分布式即可,Spark 跑 local 模式,不需要搭真正意义的 spark 集群。

MapReduce 不是不能用,但同一份聚合逻辑,MapReduce 要写几十行 Java,Spark 用 DataFrame API 十几行搞定,而且中间结果在内存里流转,迭代计算快一到两个数量级。这也是选择 Spark 而不是纯 MapReduce 的理由。技术栈完整分工如下:

环节工具职责数据形态
采集Scrapy抓取美妆信息、评价、销量JSON / CSV
清洗pandas去重、格式统一、异常值处理DataFrame
存储MySQL持久化明细数据关系表
计算Hadoop + Spark原始数据备份、批量聚合RDD / DataFrame
展示Django + ECharts接口与图表渲染JSON 到图表

这套分工的边界很清晰:Scrapy 只负责把页面变成结构化数据,不关心数据怎么消费;Spark 只做计算,不关心数据从哪来;Django 只做展示,不承担任何清洗逻辑。边界一旦模糊,比如在爬虫里写 SQL join,或者在 Django 里做全表聚合,系统很快就会变成改一处崩三处的状态。

3. Scrapy 爬虫落地:从 Item 定义到 Pipeline 入库

3.1 工程初始化与 Item 字段设计

爬虫工程在 PyCharm 的 Terminal 里用命令行创建,不要手动建目录:

scrapy startproject beauty_crawler cd beauty_crawler scrapy genspider beauty_spider example.com

创建后目录结构为:spiders 目录放爬虫主逻辑,items.py 定义字段,pipelines.py 做清洗入库,settings.py 控制并发和下载延迟。美妆信息的核心字段定义如下:

# items.py import scrapy class BeautyItem(scrapy.Item): title = scrapy.Field() # 商品标题 brand = scrapy.Field() # 品牌,后续分组维度 category = scrapy.Field() # 品类:口红/粉底/眼影 price = scrapy.Field() # 价格,入库前转 float sales = scrapy.Field() # 销量,用于排序分析 rating = scrapy.Field() # 评分,保留一位小数 comment_count = scrapy.Field() # 评价数,判断热度 author = scrapy.Field() # 发布者,部分来源是笔记 source_url = scrapy.Field() # 详情页 URL,唯一去重 crawl_time = scrapy.Field() # 抓取时间,默认当前时间

字段设计的原则是:先想清楚要分析什么,再定抓什么。如果后面要做“价格区间 × 品类”的交叉分析,price 和 category 就必须拆成独立字段,不能塞进一个描述文本里。source_url 强烈建议保留,既用来做 URL 去重,也方便回溯原始页面。scrapy.Field()本身不限制类型,类型校验放在 Pipeline 里做比放在 Item 里更灵活。

3.2 列表页解析与翻页实现

解析用 XPath 比正则稳定。页面结构变化时 XPath 只需要改路径,正则要重写匹配逻辑。一个列表页的典型写法:

# spiders/beauty_spider.py import scrapy from beauty_crawler.items import BeautyItem class BeautySpider(scrapy.Spider): name = "beauty_spider" start_urls = ["https://example.com/beauty"] def parse(self, response): # 选中所有商品节点,逐条提取字段 for node in response.xpath('//div[contains(@class,"product-item")]'): item = BeautyItem() item["title"] = node.xpath('.//a[@class="title"]/text()').get() item["brand"] = node.xpath('.//span[@class="brand"]/text()').get() item["price"] = node.xpath('.//span[@class="price"]/text()').get() item["sales"] = node.xpath('.//span[@class="sales"]/text()').get() item["source_url"] = node.xpath('.//a/@href').get() yield item # 翻页:下一页链接继续回调 parse next_page = response.xpath('//a[@class="next"]/@href').get() if next_page: yield scrapy.Request( url=response.urljoin(next_page), callback=self.parse )

两个关键点。第一,.//表示从当前节点往下找,get()返回第一个匹配值,取不到时返回 None 而不是抛异常,解析容错性更好。第二,翻页用response.urljoin()拼接相对路径,比手写字符串拼接安全,能自动处理协议相对地址//host/path这类情况。

3.3 清洗 Pipeline 与增量入库

Pipeline 是 Scrapy 里做数据清洗的位置。每个 Item 从 spider 出来后按优先级依次经过所有 Pipeline,常见做法是第一个做字段清洗,第二个做入库:

# pipelines.py import pymysql from itemadapter import ItemAdapter class CleanPipeline: def process_item(self, item, spider): adapter = ItemAdapter(item) # 价格清理:去掉货币符号和空格,转 float price = str(adapter.get("price", "")).replace("¥", "").strip() adapter["price"] = float(price) if price else 0.0 # 空标题直接丢弃 if not adapter.get("title"): raise DropItem("empty title, dropped") return item class MysqlPipeline: def open_spider(self, spider): self.conn = pymysql.connect( host="127.0.0.1", user="root", password="123456", database="beauty_db", charset="utf8mb4" ) self.cursor = self.conn.cursor() def process_item(self, item, spider): sql = """INSERT INTO beauty_info (title, brand, category, price, sales, rating, comment_count, source_url, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE sales = VALUES(sales)""" self.cursor.execute(sql, ( item["title"], item["brand"], item["category"], item["price"], item["sales"], item["rating"], item["comment_count"], item["source_url"], item["crawl_time"] )) self.conn.commit() return item def close_spider(self, spider): self.cursor.close() self.conn.close()

入库 SQL 里的ON DUPLICATE KEY UPDATE是增量更新的关键:source_url 建唯一索引后,重复抓取同一商品不会插入新行,而是更新销量和评价数。这样每天定时跑一次爬虫,就能积累美妆商品的时间序列数据,后续做趋势分析才有素材。

3.4 并发与反爬参数设置

settings.py 里按目标站点承受能力设置并发,不要盲目调大:

# settings.py CONCURRENT_REQUESTS = 16 # 全局并发数,32 以上容易被封 IP DOWNLOAD_DELAY = 0.5 # 每请求间隔,单位秒 ROBOTSTXT_OBEY = False # 课设环境按需关闭,生产环境需合规 DEFAULT_REQUEST_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://example.com/" }

CONCURRENT_REQUESTSDOWNLOAD_DELAY是反比关系。出现 403 或频繁验证码时,先把并发降到 4、延迟提到 2,观察一段时间再逐步回调。DOWNLOAD_DELAY对同一域名生效,多域名抓取时可以适当调低。

4. 数据清洗与 MySQL 表结构:分析质量的决定因素

4.1 pandas 清洗的标准操作

爬虫落库的数据不能直接分析,常见问题包括:品牌写法不一致、价格为 0 的异常行、评分超出区间、时间格式不统一。清洗脚本单独放一个文件,用 pandas 读库处理后写回:

# clean_data.py import pandas as pd import pymysql conn = pymysql.connect(host="127.0.0.1", user="root", password="123456", database="beauty_db", charset="utf8mb4") df = pd.read_sql("SELECT * FROM beauty_info", conn) # 1. 品牌名标准化:统一大小写,去除首尾空格 df["brand"] = df["brand"].str.strip().str.title() # 2. 过滤异常价格:价格必须大于 0 且小于 5000 df = df[(df["price"] > 0) & (df["price"] < 5000)] # 3. 评分越界处理:超过 5 分按 5 分截断 df.loc[df["rating"] > 5, "rating"] = 5.0 # 4. 时间字段统一为日期格式 df["crawl_time"] = pd.to_datetime(df["crawl_time"]).dt.date # 5. 按商品维度去重,保留最新一条 df = df.sort_values("crawl_time").drop_duplicates( subset=["title", "brand"], keep="last") # 写回清洗后的数据 df.to_sql("beauty_info_clean", conn, if_exists="replace", index=False) conn.close()

drop_duplicates(subset=[...], keep="last")是清洗里最关键的一步:先按抓取时间排序,再按标题加品牌去重,保证每个商品只保留最新抓取状态,避免同一条数据被抓两次造成销量翻倍。to_sqlif_exists="replace"适合课设阶段反复跑清洗任务;生产环境应该改成增量更新,只处理新增时间段的数据。

注意:pandas 的to_sql对 DECIMAL 字段会有精度问题,insert 后建议抽查几条price,如果出现 float 尾差,入库字段统一改成 DECIMAL(10,2) 并由 SQL 侧负责转换。

4.2 表结构与索引设计

清洗后的数据落到独立表,原始表保留不动方便回溯。核心表设计如下:

字段类型约束说明
idINTPRIMARY KEY AUTO_INCREMENT主键
titleVARCHAR(200)NOT NULL商品标题
brandVARCHAR(50)INDEX品牌,分组维度
categoryVARCHAR(50)INDEX品类,分组维度
priceDECIMAL(10,2)成交价
salesINT销量
ratingDECIMAL(2,1)评分
comment_countINT评价数
source_urlVARCHAR(500)UNIQUE INDEX原文链接,防重复
crawl_timeDATEINDEX抓取日期

三个索引位置是设计重点:brand 和 category 是分析时最常用的分组维度,必须建索引;crawl_time 建索引是因为后面做时间序列分析时要按日期范围过滤,没有索引的话千万行级别的全表扫描会直接拖垮查询。source_url 的唯一索引对应爬虫入库时的ON DUPLICATE KEY UPDATE,两者必须配套使用。

4.3 用 SQL 验证数据分布

清洗完先不急着上 Spark,MySQL 里基础聚合先跑一遍,确认数据分布合理:

-- 按品类统计销量 Top 10 SELECT category, SUM(sales) AS total_sales FROM beauty_info_clean GROUP BY category ORDER BY total_sales DESC LIMIT 10; -- 品牌价格带分布:每 100 元一档 SELECT brand, FLOOR(price / 100) * 100 AS price_band, COUNT(*) AS product_cnt FROM beauty_info_clean GROUP BY brand, FLOOR(price / 100) * 100 ORDER BY brand, price_band; -- 近 30 天抓取量趋势 SELECT crawl_time, COUNT(*) AS daily_cnt FROM beauty_info_clean WHERE crawl_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY crawl_time ORDER BY crawl_time;

这三个查询分别回答“什么品类卖得好”“品牌定位在什么价格段”“采集是否连续”。最后一个尤其重要:如果某天 daily_cnt 掉到接近 0,说明当天爬虫被反爬拦截了,需要回去看抓取日志,而不是继续往下做分析。数据采集断层会让后续的趋势分析出现假性下跌,这个要提前排除。

5. Spark 批处理:在 Hadoop 上算品牌竞争度与消费偏好

5.1 JDBC 读取与分区参数

单机课设不需要 Spark 集群,local 模式加 JDBC 直读 MySQL 就够:

# spark_analysis.py from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("BeautyAnalysis") \ .master("local[2]") \ .config("spark.sql.shuffle.partitions", "4") \ .getOrCreate() df = spark.read \ .format("jdbc") \ .option("url", "jdbc:mysql://127.0.0.1:3306/beauty_db") \ .option("dbtable", "beauty_info_clean") \ .option("user", "root") \ .option("password", "123456") \ .option("driver", "com.mysql.jdbc.Driver") \ .load()

master("local[2]")表示本地双核运行,不提交集群。spark.sql.shuffle.partitions=4是关键调参:默认 200 个分区在单机上纯属浪费资源,每个分区都要起 task,改成 4 个能明显减少 shuffle 文件数和内存压力。读取大表时还可以加partitionColumnlowerBoundupperBound做并行读取,让多个 task 各读一段主键范围。

5.2 品牌竞争度与消费偏好指标

用 DataFrame API 做聚合,写法接近 SQL 但后续接机器学习更方便:

from pyspark.sql import functions as F # 品牌竞争度:销量份额 + 品类覆盖数 brand_stats = df.groupBy("brand").agg( F.sum("sales").alias("total_sales"), F.countDistinct("category").alias("category_cnt"), F.avg("rating").alias("avg_rating") ).orderBy(F.desc("total_sales")) # 价格带 × 品类交叉分析 price_cat = df.withColumn( "price_band", (F.col("price") / 100).cast("int") * 100 ).groupBy("price_band", "category").count() # 消费偏好:评论数大于 100 的商品里按评分排序 top_rated = df.filter(F.col("comment_count") > 100) \ .orderBy(F.desc("rating"), F.desc("comment_count")) \ .limit(20)

countDistinct("category")算品牌覆盖的品类数,能区分“专精型品牌”和“全品类品牌”,这是论文里品牌竞争度分析的核心指标。filter里限制评论数大于 100,是为了过滤掉刷分的冷门商品,只看有真实用户基础的评分,避免分析结果被异常值带偏。

5.3 结果写回 MySQL 与 HDFS 备份

分析结果用两种方式落盘:指标表写回 MySQL 供 Django 读取,原始明细备份到 HDFS:

# 写回 MySQL,供 Django 看板使用 brand_stats.write \ .format("jdbc") \ .option("url", "jdbc:mysql://127.0.0.1:3306/beauty_db") \ .option("dbtable", "agg_brand_stats") \ .option("user", "root") \ .option("password", "123456") \ .option("truncate", "true") \ .mode("overwrite") \ .save() # 原始数据备份到 HDFS df.write.mode("overwrite") \ .parquet("hdfs://localhost:9000/user/beauty/clean_data")

写回 MySQL 时.option("truncate", "true")配合.mode("overwrite"),先清表再写入,避免重复跑任务时数据翻倍。HDFS 备份用 parquet 格式而不是 CSV,列式存储压缩率高,后续增量分析可以直接用 Spark 读 parquet,省掉一次 JDBC 全量拉取。

提示:Spark 写 MySQL 时遇到ClassNotFound com.mysql.jdbc.Driver,是驱动 jar 没进 CLASSPATH。把 mysql-connector-java 的 jar 放到$SPARK_HOME/jars目录,重启 SparkSession 即可。

6. Django 看板渲染与排错实操

6.1 JSON 接口与 ECharts 双轴图

Spark 算好的聚合结果写回 MySQL 后,Django 只负责查表。视图里用游标执行只读 SQL,避免 ORM 在聚合查询时生成多层子查询:

from django.http import JsonResponse from django.db import connection def brand_chart(request): with connection.cursor() as cursor: cursor.execute(""" SELECT brand, total_sales, avg_rating FROM agg_brand_stats ORDER BY total_sales DESC LIMIT 10 """) rows = cursor.fetchall() return JsonResponse({"code": 0, "data": [ {"brand": r[0], "sales": float(r[1]), "rating": float(r[2])} for r in rows ]})

前端模板引入 ECharts,fetch 后 setOption。画销量柱状图叠加评分折线图时,评分 y 轴必须设max: 5,否则折线被压缩在图表底部,趋势完全看不出来。图表容器需要显式声明高度,父级隐藏时初始化会导致渲染宽度为 0,图表空白。

6.2 三个方向的排错顺序

看板打开慢,按接口、数据库、计算层三步排查。接口慢就看 Django 日志和浏览器 Network 面板的耗时分布;数据库慢就开 MySQL 慢查询日志,用EXPLAIN看聚合 SQL 是否走了索引;Spark 任务慢则在 local 模式下调spark.driver.memory,而不是盲目加分区数。

爬虫内存持续上涨,通常是并发设置过高或 Item 里累积了大字段。用scrapy parse --spider=beauty_spider URL单条调试,看请求耗时分布,比反复改代码重启快。全部跑通后把爬虫和 Spark 任务交给 cron 定时调度,Django 只读聚合表,三层解耦之后任一层出问题都不会拖垮整条链路,这也是这套系统最值得保留的架构习惯。

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

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

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

立即咨询