1. 选题阶段就想清楚的事:这个项目为什么能吃下整个技术栈
如果你正卡在毕业设计选题上,大概率会遇到两种情况:要么题目太小,写不满论文、做不出系统截图;要么题目太大,一个人搞不定分布式集群、扛不住性能调优。而“Python+Flask+Selenium+Spark+Hadoop+Echarts”这种组合,恰好是工科院校里最能打的“广谱型”选题——它把数据采集、Web开发、大数据处理、可视化展示四块内容全部串在一条业务链上,每一层都有技术深度可写,又不需要你在某一块钻研到专家级。
先说清楚这套系统到底在做什么。它的核心流程是:用Selenium模拟真实浏览器操作,去爬取电商平台的商品信息(标题、价格、销量、店铺、评论数等)存入MySQL;然后通过Hadoop的HDFS做分布式存储,用Spark做离线数据分析(比如价格区间分布、销量排名、关键词词频);最后用Flask对外提供Web服务,把Spark跑出来的统计结果交给Echarts渲染成可视化大屏。
这套链路最聪明的地方在于“数据流是闭环且可验收的”。答辩时你能演示的不只是某个页面,而是“从URL输入到图表输出”的完整过程,每个环节都能被追问出细节。横向对比其他常见毕设:做普通爬虫系统的,大部分用Scrapy或Requests,虽然也能跑,但和“大数据”四个字完全不沾边;做Hadoop案例的,多数人就是拿现成数据集跑个WordCount,缺乏自采数据的完整故事线;做Web可视化的,又往往只是展示静态CSV。你这个选题恰好把三个方向的痛点一次性解决了:爬虫提供了新鲜数据,大数据平台提供了处理逻辑,Web前端提供了展示出口。
我接触过的毕设选题里,这类项目最大的优势就是“容错率高”。如果你Spark集群搭建失败,仍然可以退回到单机版跑通整个流程;如果Selenium被反爬拦截,也可以降级到Requests加解析库;哪怕Echarts图表出不来,Flask至少能返回JSON数据用于答辩演示。多一层技术栈就多一层保障,这在答辩现场是非常关键的。
不过也得提醒你一句,这个项目体量比普通毕设大不少。从零开始做,采集模块一个周、数据存储与预处理一个周、Spark分析一个周、Flask后端加前端页面一个周,加上联调写论文,满打满算五到六周比较稳妥。如果学校还有中期检查,至少要在第三周结束前把爬虫和数据库跑通,否则后面Spark分析部分就没有数据可用,整个系统就像没了水源的河。
如果你确认自己愿意接受这个开发量,下面我从四个核心模块逐步拆解具体实现方案。先说明一句,我讲的都是经过验证的通用做法,具体到你的环境和版本可能会有些差异,原理是相同的。
2. 爬虫采集层:Selenium为什么是毕设场景的正确选择
2.1 选Selenium而不是Scrapy/Requests的底层逻辑
很多同学第一反应是“爬虫不都应该用Scrapy吗”,这个思路在真实工业界没错,但在毕设这套系统里反而会给你添麻烦。原因在于你爬的电商商品列表页,至少会有两类动态加载:第一类是滚动加载,页面往下拉才会触发后续商品数据;第二类是异步请求,价格、库存、销量可能通过Ajax接口返回后在浏览器里替换DOM节点。Requests加解析拿到的只是最初始的HTML骨架,里面根本没有完整商品数据。
Selenium解决问题的思路是“直接接管浏览器”——它驱动真实的Chrome或Firefox实例,页面怎么渲染它就怎么看到数据,人类能滚动的它也能滚动,人类能点击的它也能点击。哪怕页面里的数据是JavaScript动态拼出来的,最终DOM里的内容也会被Selenium完整获取。这就是它在这种场景下不可替代的优势。
不过代价也显而易见:速度慢,一个浏览器实例加载完整页面可能需要2到5秒。但毕设场景里爬个几千条商品数据完全够用,你不需要追求百万级吞吐,稳定性和可解释性才是重点。答辩时老师问你“为什么不用Scrapy”,你可以答:“因为目标站点数据通过JavaScript动态渲染,Selenium能完整模拟浏览器环境,规避动态内容抓取不全的问题”,这个回答就非常专业。
2.2 爬虫模块的完整代码骨架与关键配置
Python环境建议直接用3.8以上版本,安装selenium库的同时一定要装与浏览器版本匹配的驱动。Chrome的话,下载chromedriver时先查一下本机Chrome版本号,主版本不一致启动浏览器必报错,这是新手最常见的第一个坑。
以淘宝为例,商品搜索页的关键实现逻辑大概如下:
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.options import Options import pymysql import time import random # 设置无头浏览器参数, 后台运行不弹窗 options = Options() options.add_argument("--headless") options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") driver = webdriver.Chrome(options=options) wait = WebDriverWait(driver, 10) def scroll_page(times=5): # 模拟向下滚动, 触发懒加载, 每次滚到底部等待图片和数据刷新 for i in range(times): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(random.uniform(1.5, 3)) def parse_items(): # 通过CSS选择器定位商品卡片容器, 字段采集需要注意获取真实渲染后的文本内容 cards = driver.find_elements(By.CSS_SELECTOR, "div[data-view-id*='product']") result = [] for card in cards: try: title = card.find_element(By.CSS_SELECTOR, "a[title]").get_attribute("title") price = card.find_element(By.CSS_SELECTOR, "div[class*='price']").text shop = card.find_element(By.CSS_SELECTOR, "div[class*='shop']").text result.append((title, price, shop)) except Exception: continue return result这段代码里有三个个人认为值得注意的细节:
第一,--disable-blink-features=AutomationControlled这个参数能去掉webdriver的自动化标记,让目标站点更不容易识别你是爬虫。虽然现在很多大平台还有更严格的风控,但至少能延后被封的时间。
第二,采集字段别直接拿元素的text属性,尤其是标题和价格。标题类字段用get_attribute("title")更稳,因为很多标题实际放在标签的title属性里而非文本节点中。
第三,滚动加载是电商列表页最常见的数据加载方式,控制滚动次数和时间间隔时模拟人的操作节奏,避免“秒滚到底”这种非人类行为。随机sleep一下比固定sleep效果好得多。
2.3 反爬应对与采集合规的边界感
说句实在话,淘宝这类大型电商平台的反爬强度是远超你想象的。滑块验证、行为检测、IP频率限制轮番上阵,你在毕设中遇到滑块时,Selenium虽然能做模拟拖拽(比如用ActionChains按住滑块拖动),但成功率并不高,而且极其耗时。我在自己的实践中得出的经验是:毕设项目做功能验证,选择一些反爬策略相对宽松的电商站点就够了,比如某些垂直品类电商或进口商品平台,数据结构类似但爬取难度低一个量级。没必要死磕淘宝。
而且在写论文时,这一块反而是加分项。你把“反爬应对策略”作为一章,讲清楚IP代理池、请求频率控制、User-Agent轮换、行为模拟这四道防线,再配合你工程里实际实现的频率限制,老师会觉得你考虑到了工程落地层面的问题。
关于合规性必须多说两句:目前国内对爬虫相关的法律边界已经很清晰了——只抓公开数据、不做商业化二次转卖、不绕过反爬机制中的技术保护措施(如破解登录验证码),这三条底线守住,毕设项目就不会有问题。如果你的项目后期要公开源码,记得把目标站点数据脱敏,不要保留真实的店铺名和商品ID。
3. Flask应用层:别把爬虫写进视图函数里
3.1 为什么需要Flask这层“壳”
有些同学会问:采集和分析都跑完了,直接把结果生成一个HTML不就行了吗,为什么非要上Flask?这个问题问到点子上了。假如你只要交一篇论文,确实用静态HTML加Echarts就够了;但如果你需要演示“用户访问系统、系统实时展示数据分析结果”的交互过程,就必须有一个Web服务端来承载数据接口和页面渲染。
Flask在毕设项目里几乎是标准答案,理由也很实在:框架轻,入门成本低,一个app.py就能撑起整个项目;内置Jinja2模板引擎,和Echarts配合做数据展示非常顺手;同时Python生态让你可以把爬虫、Spark分析脚本全都import进来,不用像Java体系那样拆成多个工程。
3.2 一个清晰的Flask项目结构
我在做这类项目时习惯把项目拆成以下几个部分:
project/ ├── app.py # Flask主程序, 路由和接口 ├── config.py # 数据库与全局配置 ├── crawler/ │ ├── spider.py # Selenium采集逻辑 │ └── store.py # 数据入库模块 ├── analysis/ │ ├── spark_job.py # PySpark分析任务 │ └── result_reader.py # 从MySQL读取聚合结果 ├── templates/ │ ├── index.html # 可视化大屏 │ └── dashboard.html # 数据分析页面 └── static/ ├── js/ └── css/需要特别注意的是“不要让爬虫阻塞Flask”。很多人写出的第一个版本是:视图函数里调用爬虫函数,用户点了“开始采集”按钮后页面就整个卡住,等爬虫跑完才返回。这在演示时是灾难级的体验,爬几十页要几分钟,浏览器早就超时了。
正确做法有两种。第一种是启动一个后台线程执行爬虫任务,视图函数立刻返回“采集已启动”,然后前端通过/api/status轮询进度。第二种是用apscheduler库定时触发爬虫任务,这样系统就具备了定时更新的能力。按我的经验,毕设阶段用后台线程足够了,还能少封装一层调度逻辑。
3.3 数据表设计与接口设计示例
MySQL表设计不需要太复杂,一张商品表加一张汇总表就能撑起整个业务。商品表负责明细数据,给Spark分析做输入;汇总表存分析结果,给Echarts直接读取。以下面这个结构为例:
CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) COMMENT '商品标题', price DECIMAL(10,2) COMMENT '商品价格', sales INT COMMENT '销量', shop VARCHAR(120) COMMENT '店铺', category VARCHAR(60) COMMENT '商品类目', crawl_time DATETIME COMMENT '采集时间' ); CREATE TABLE analysis_result ( id INT AUTO_INCREMENT PRIMARY KEY, metric VARCHAR(50) COMMENT '指标名称', value VARCHAR(255) COMMENT '指标值', extra VARCHAR(255) COMMENT '关联项', create_time DATETIME COMMENT '生成时间' );这套设计的考虑是:Spark分析完数据后,把结果统一写回analysis_result,前端接口只需要按metric查询即可,不用为每一张图表单独建表。这是一种“以指标为中心”的宽表设计,对毕设项目来说是最省事的方案。
接口层面定义四个就好:
GET /首页,可视化大屏页面渲染GET /api/products返回商品明细列表GET /api/summary返回价格区间分布、销量Top10、关键词词频等汇总数据POST /api/crawl触发采集任务
前端Echarts在页面加载后调用/api/summary拿数据,一个fetch就能搞定。
4. Spark与Hadoop在毕设里的真实角色:伪分布式不算偷懒
4.1 Hadoop在这里承担什么功能
很多同学对“用了Hadoop”有一种执念,以为不搭一个三节点集群就不算大数据项目。实际上,从论文评审的角度看,你完整复现了伪分布式环境、把数据成功写入HDFS、用Spark读取分布式文件进行计算,已经足以证明你掌握了大数据平台的工作原理。毕竟毕设考察的是知识体系,而不是生产环境的运维能力。
Hadoop在咱们这个项目里的作用分两块:第一,数据落地——爬虫采集到的商品数据定期导出为JSON或CSV文件,通过hadoop fs -put命令上传到HDFS的/user/hadoop/product_data/目录;第二,作为Spark的数据源——Spark作业通过spark.read.json("hdfs://localhost:9000/user/hadoop/product_data/")读取分布式文件做后续分析。这一条链路就能展示出完整的大数据存储和计算流程。
4.2 Hadoop伪分布式搭建的核心要点
搭建过程网上教程很多,我提炼几条最容易翻车的经验。
Java环境一定要用JDK8配合Hadoop 2.x版本,这个组合最兼容最稳定;如果你用了JDK11甚至更高版本,很容易遇到javax.security无法解析之类的诡异报错。配置文件core-site.xml、hdfs-site.xml、mapred-site.xml和yarn-site.xml四个都得改。其中core-site.xml里必须配置NameNode的地址,hdfs-site.xml里把副本数dfs.replication设为1,因为伪分布式只有一个DataNode,你写3副本不仅浪费空间,启动时还会一直报块副本不足的警告。
启动前一步很多人漏掉:首次启动HDFS必须执行hdfs namenode -format格式化。这一步如果没做,NameNode和DataNode的clusterID对不上,DataNode会一直报错无法加入集群。格式化成功后,start-dfs.sh和start-yarn.sh分别启动存储和计算模块,用jps命令看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager五个进程才算搭建成功。
4.3 PySpark进行数据分析的实用写法
Spark分析模块建议用PySpark而不是Scala,原因很直接:你的爬虫和Flask都用Python,语言统一能降低切换成本。PySpark的安装包需要和Spark版本严格对应,否则会出现Python解释器与JVM通信报错,这个匹配检查一定要先做。
核心分析任务我一般拆成三类:词频统计、价格区间分布、销量排行。词频统计针对商品标题进行中文分词,需要引入jieba库配合Spark的flatMap操作;价格区间分布通过Bucketizer将价格分桶(比如0-50、50-100、100-200、200以上四档)后做groupBy;销量排行直接orderBy("sales", ascending=False).limit(10)。整体代码如下:
from pyspark.sql import SparkSession import jieba from pyspark.sql.functions import col, explode, split, desc spark = SparkSession.builder \ .appName("ProductAnalysis") \ .config("spark.executor.memory", "2g") \ .getOrCreate() # 从HDFS读取商品数据 df = spark.read.json("hdfs://localhost:9000/user/hadoop/product_data/") # 词频分析: 对标题分词后explode成多行再聚合 words = df.select(explode(split(df.title, " ")).alias("word")) \ .groupBy("word") \ .count() \ .orderBy(desc("count")) \ .limit(20) # 价格分析: 按区间聚合 df.createOrReplaceTempView("products") price_stat = spark.sql(""" SELECT CASE WHEN price < 50 THEN '0-50' WHEN price < 100 THEN '50-100' WHEN price < 200 THEN '100-200' ELSE '200以上' END AS price_range, COUNT(*) AS cnt FROM products GROUP BY price_range """)个人经验是,在毕设项目中数据分析结果不要再写回HDFS,而是直接通过spark.sql的聚合结果写入MySQL。用df.write.format("jdbc")可以完成这一步,这样Flask查询MySQL拿结果,逻辑链路更干净;否则你还要写一个HDFS文件读取模块,徒增复杂度。
5. Echarts可视化的数据流设计:从DataFrame到前端图表
5.1 前后端数据结构要对齐,表字段名别神游
整条链路里最磨人的事情,排第一的是爬虫反爬,排第二的就是前端图表数据格式和后端返回字段对不上。Echarts需要的格式通常是[{name: "0-50", value: 123}, ...]或[[0, 123], [1, 456]],而Spark分析后的DataFrame字段名是price_range、cnt,如果不做转换直接返回,前端拿到的就是个带下划线字段名的JSON,图表渲染出来全空。
我的做法是后端在接口层统一做一次数据整形,把数据库字段映射成前端看得懂的键名:
@app.route("/api/summary") def summary(): cursor = db.cursor() cursor.execute("SELECT metric, value, extra FROM analysis_result") rows = cursor.fetchall() result = {} for metric, value, extra in rows: if metric == "price_distribution": result.setdefault("priceRange", []).append({ "name": extra, "value": int(value) }) elif metric == "sales_top10": result.setdefault("salesTop", []).append({ "name": extra, "value": int(value) }) return result这种改造放在接口层做的好处是,爬虫、分析层的数据结构保持原始风格,只有HTTP边界上的数据才做视图模型适配,各模块互不干扰,排查问题时也更清晰。
5.2 大屏页面的布局与图表配置思路
可视化页面用Echarts做一块“大屏”,展示的商品分析里,我实践下来效果最好的是六块图的布局:顶部放标题和采集数据统计卡片,中间左侧放价格区间分布饼图,中间右侧放销量Top10柱状图,下方放关键词词频词云或热力图。视觉上错落有致,答辩投影时也看得清楚。
Echarts初始化时有一个非常常见的坑——图表容器高度为0或父容器用了百分比高度但父级没有指定高度,导致页面加载后图表不渲染。解决办法是给容器设置固定高度,比如div style="height: 400px",或者在setOption前用window.innerHeight折算。
另一个很容易被忽视的点是:页面加载时从后端拉取数据,图表应该先初始化再更新。不要等在fetch回调里才初始化,否则容器在数据返回前可能还没完成布局。代码里用init完成后先showLoading,数据返回后setOption并hideLoading,这样即便Spark分析结果生成慢,用户视觉上也能感受到系统在“工作”。
5.3 数据联动刷新:定时任务让图表自己走起来
如果你想让演示效果更惊艳一点,可以在前端加一个定时刷新:每60秒调用一次/api/summary,有变化就更新图表。配合后台的apscheduler定时爬虫任务,整个系统就变成了一个“有生命”的数据分析平台——数据不断积累,图表不断变化,答辩时不用手动刷新也能看到动态效果。
from apscheduler.schedulers.background import BackgroundScheduler def crawl_job(): # 调用爬虫模块执行增量采集 crawl_main() scheduler = BackgroundScheduler() scheduler.add_job(crawl_job, "interval", hours=6) scheduler.start()这个定时任务最好放在Flask启动的if __name__ == "__main__":块之前执行初始化,并在应用上下文中完成。
6. 串联验收:整个系统跑通的标准操作顺序
到了联调阶段,一定要按顺序来,否则出了问题都不知道是哪一层断的。我建议的验收顺序是这样的:
6.1 第一步:Hadoop环境自检
启动HDFS和YARN后,用浏览器访问http://localhost:9870打开NameNode管理页,确认“Live Nodes”列表里有你的DataNode,并且磁盘空间不为0。再用命令行创建一个测试目录:
hdfs dfs -mkdir -p /user/hadoop/product_data hdfs dfs -ls /user/hadoop/如果不报错,说明HDFS存储层可用。这个自检过程不要跳过,因为Spark读HDFS如果失败,绝大多数原因是前面的HDFS本身没启动正常,而不是Spark程序写错了。
6.2 第二步:爬虫单跑,数据入库
单独运行crawler/spider.py,观察有没有报错,多少条数据成功写入。用一条SQL确认数据质量:
SELECT COUNT(*) FROM product; SELECT MIN(price), MAX(price), AVG(price) FROM product;先确认数据非空、价格字段没有明显异常(比如出现0.01或999999这类脏数据),再做下一步。数据清洗是大数据项目里绕不开的一环,你可以在这里把价格为0的记录剔除,把空标题过滤掉。
6.3 第三步:导出HDFS文件并跑Spark
将MySQL数据导出成JSON文件,放到HDFS目录中,然后提交Spark分析任务:
spark-submit --master local[2] analysis/spark_job.py不要用--master yarn,毕设机器资源不够,而且配置YARN模式的提交参数非常麻烦。本地模式local[2]表示用两个线程模拟分布式计算,演示和验证完全足够。看日志里有没有报错,然后查MySQL里analysis_result是否有新数据写入。
6.4 第四步:启动Flask并查看页面
python app.py启动Flask,浏览器打开http://localhost:5000,看图表是否正确渲染。如果图表空白,按F12打开控制台,先看Network里/api/summary接口是否返回200、返回的数据格式是否符合预期,再从Network切换到Console看有没有JavaScript报错。这一套排查路径几乎能解决所有前端显示问题。
6.5 第五步:全链路压测演示流程
最后一定要走一遍完整演示稿:启动Hadoop → 启动Flask → 页面展示已有数据分析结果 → 手动触发一次爬虫 → 等采集完成 → 跑一次Spark分析 → 刷新页面看到数据变化。整个流程走顺了,答辩时你就有了一个连贯的叙事线,老师顺着你的演示走下来,会很自然地认可项目的完整度。
7. 这个项目最容易翻车的几个坑
最后集中说几个我在帮同学排查这个项目时反复见到的问题,每个都是血泪教训换来的。
第一个坑:Chromedriver版本和Chrome版本对不上。Selenium启动时直接抛SessionNotCreatedException,这一类错误在毕设答辩前一周集中爆发。解决办法不是重新下载一个驱动就完事,而是先查清Chrome版本,再登录chromedriver镜像站找到对应的驱动版本号,下载后放进Python环境的Scripts目录或直接用webdriver.Chrome(executable_path="指定路径")。
第二个坑:Spark读取JSON时日期和数字类型推断出错。例如价格字段在MySQL里是DECIMAL(10,2),导出JSON后会变成1.23这样的小数,但标题、价格混合在一起时Spark可能把价格列推断成字符串。建议在Spark读取时显式指定schema:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DoubleType schema = StructType([ StructField("title", StringType(), True), StructField("price", DoubleType(), True), StructField("sales", IntegerType(), True), StructField("shop", StringType(), True) ]) df = spark.read.schema(schema).json("hdfs://...")第三个坑:Flask端口被占用。特别是演示前你开了很多终端窗口,python app.py时报Address already in use。这是小问题,但特别影响心态。直接换端口跑:python app.py --port 5001,或者lsof -i:5000找到占用进程然后kill掉。
第四个坑:Echarts图表数据是有了,但中文标签显示乱码。一般不会出现在现代浏览器里,但如果页面没有声明<meta charset="utf-8">就会触发。把模板文件改成UTF-8编码保存,并且以<!DOCTYPE html>开头。
第五个坑:Selenium跑久了内存暴涨。无头Chrome虽然看不到,但它一样吃内存,长时间运行会导致整个系统卡死。解决方法是每隔一段时间driver.quit()重开一个浏览器实例,或者限制一个采集任务的页面数量上限。在毕设演示场景里,重开实例是最稳妥的方案。
关于项目源码的组织方式,我个人建议整个项目做成一个公开的Git仓库,README里写清楚技术栈、环境搭建步骤、启动命令。原因有两个:一是你自己写论文时需要引用代码清单,有Git记录方便回溯;二是如果指导老师要看代码,给他一个克隆地址比发压缩包专业得多。
最后说一点个人体会。这套系统最大的价值不在于某项技术玩得多深,而在于你亲手打通了一条完整的数据管道——从世界的某个网页出发,经过浏览器模拟的“手指”点击与滑动,把数据收进本地数据库;再经过大数据引擎的分布式计算,提炼成一个个可以回答问题的数字;最后转换成你眼前屏幕上跳动的图表。这种“数据从无到有再到价值”的完整感,是任何单点技术训练都给不了的经验。如果你能把这条管道里的每一步都讲清楚为什么这么做、出了故障怎么排查,那这场毕业答辩对你来说,其实已经不只是过关的问题,而是真正把知识点串成了一张网。