需要先说明一点:这篇博文是围绕一个真实存在的毕业设计项目展开的完整复盘,写的都是我自己从选题、搭环境、写爬虫、灌数据、建模到做可视化的全流程经历,不是那种只有目录没有灵魂的“伪教程”。如果你正打算做类似的电商数据分析系统,或者纯粹想看看Selenium爬虫和Echarts可视化怎么串成一条完整链路,这篇应该能帮你省下不少踩坑时间。
1. 项目整体设计与技术选型逻辑
1.1 为什么选淘宝商品数据做毕设
很多同学做毕设的第一步就卡在“不知道做什么题目”。我当时选“淘宝商品数据分析可视化系统”,核心原因是这个题目能一次性覆盖计算机专业最常被问到的几个方向:爬虫、数据存储、数据分析和可视化。而且淘宝作为国内最大的电商平台,商品数据天然具备“量大、字段丰富、维度多”的特点,拿来做机器学习练手非常合适。
有同学会问:淘宝反爬那么严重,选这个题目不是给自己挖坑吗?我的回答是:选题时就要想清楚,毕业设计考察的不是你能否突破平台风控,而是你能否用工程化手段完整处理“数据采集到呈现”的全链路。Selenium模拟真实浏览器操作,本身就是一种合理的、符合平台规则的技术手段——模拟用户正常浏览,不搞恶意抓取、不搞高频请求。
从电商场景来看,淘宝商品数据里藏着非常典型的数据挖掘价值:
- 价格分布:不同类目商品的价格区间在哪,是否存在明显的“两极化”
- 销量特征:爆款商品的销量集中在哪个区间,和价格有没有相关性
- 店铺属性:皇冠店、天猫店、普通集市店的商品定价策略差异
- 商品标题文本:高频词是什么,能不能从中挖掘行业趋势
这些维度既能做描述性统计,又能喂给机器学习模型做预测和聚类,毕设深度完全够。
1.2 技术栈选型的真实考量
这个项目的技术栈拆解下来是:Selenium(爬虫采集) + MySQL(数据仓库) + Python数据分析全家桶(Pandas、NumPy、Scikit-learn) + Flask(后端接口) + Vue.js + Echarts(前端展示)。
很多人在技术选型上容易犯一个错误:追新。看到有同学用Hadoop、Spark就眼红,非要上个大数据框架。我当时的判断是:毕设的核心评判标准是“逻辑闭环+工程完整”,而不是技术栈有多高级。淘宝商品数据撑死几万条,单机MySQL完全够用,非要上分布式纯粹是给自己增加答辩压力。
选Selenium而不是Requests/Scrapy,原因是淘宝的页面是典型的异步加载+登录态校验,直接构造HTTP请求很容易被识别,而且数据接口的参数加密逻辑复杂。Selenium直接驱动真实浏览器,相当于模拟真人操作,稳定性最高。虽然效率低一点——每分钟能采集的商品数量大概在20到40个——但毕设场景不需要海量数据,质量比速度重要。
前端用Vue.js + Echarts的组合,是考虑到这两个技术在简历上出现的频率最高,而且彼此配合非常成熟。Echarts是百度开源的可视化库,对中国地图、词云、散点图等场景的支持比国外库更贴合中文电商数据场景。Vue.js负责组件化页面管理,配合ElementUI的表格和卡片组件,很快就能搭出一个专业感十足的仪表盘。
1.3 系统整体架构和各模块职责
整个系统我画了个非常清晰的分层:数据采集层、数据存储层、数据分析层、接口服务层、前端展示层。每一层只做自己的事,层与层之间通过标准化数据结构衔接。
数据采集层用Selenium操作Chrome浏览器,自动搜索关键词、滚动加载、提取商品卡片字段。这一层产出的是“原始HTML片段”,经过解析后转成结构化DataFrame。
数据存储层用了MySQL作为数据仓库。我建了三张核心表:商品信息表、店铺信息表和采集日志表。商品表和店铺表是主题表,日志表用来记录每次采集任务的耗时、状态和数量,方便排查问题。
数据分析层是整个系统的智慧大脑。Pandas做数据清洗和特征工程,Scikit-learn跑机器学习模型,具体做了三件事:价格预测(随机森林回归)、商品聚类(KMeans)、销量等级分类(逻辑回归)。每类模型输出的结果都转成JSON格式,通过Flask接口暴露给前端。
前端展示层用Vue.js渲染页面,通过Axios请求Flask接口,拿到JSON数据后用Echarts绘制图表。页面上有概览看板、类目分析、价格分布、词云、店铺排行等模块。
这样的架构还有一个额外好处:每一层都能单独写进毕业论文的“系统设计”章节,模板化程度很高,字数容易撑起来,而且每一层都有实物可演示。
2. 开发环境搭建与工程项目结构规划
2.1 环境配置和依赖版本注意事项
我先把完整的环境版本列出来,方便你照着复现:
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.9.x | 3.10以上部分库兼容性会出问题 |
| Selenium | 4.8.x | 4.x和3.x的API差异大 |
| Chrome | 对应Chromedriver版本 | 版本必须严格匹配 |
| MySQL | 8.0.x | 编码用utf8mb4 |
| Flask | 2.2.x | 轻量级API服务 |
| Pandas | 1.5.x | 数据处理主力 |
| Scikit-learn | 1.2.x | 机器学习库 |
| Vue.js | 2.7.x | 2.x生态最稳 |
| ElementUI | 2.15.x | 配合Vue2 |
| Echarts | 5.3.x | 5.x版本API更先进 |
这里最关键的是Chrome和Chromedriver的版本匹配。当时我踩过一次大坑:Chrome自动升级到了112版本,而Chromedriver还是108,Selenium跑起来直接报session not created异常。后来的解决办法是使用selenium-manager自动管理驱动版本,Selenium 4.6以上版本自带这个功能。
Python环境的建议是直接用Anaconda创建独立的虚拟环境,别和系统Python混在一起。我在conda里执行了如下命令:
conda create -n taobao_data python=3.9 conda activate taobao_data pip install selenium pandas numpy scikit-learn flask flask-cors pymysqlVue前端部分用npm管理依赖,Node版本建议用16.x LTS版本,太高或太低都会导致依赖安装报错:
npm install -g @vue/cli vue create taobao-visual # 选择Vue2模式 npm install element-ui echarts axios2.2 仓库目录结构和代码分层规范
项目完成后,我的目录结构长这样,你可以直接照抄这个模板:
taobao-analysis/ ├── crawler/ # 爬虫模块 │ ├── taobao_spider.py # Selenium采集主逻辑 │ ├── parser.py # HTML解析器 │ └── config.py # 搜索配置 ├── etl/ # 数据清洗存储 │ ├── clean_data.py # Pandas清洗脚本 │ └── db_utils.py # MySQL读写封装 ├── analysis/ # 机器学习分析 │ ├── price_predict.py # 价格预测模型 │ ├── cluster_analysis.py # 商品聚类 │ └── sentiment.py # 文本情感分析 ├── server/ # 后端API服务 │ ├── app.py # Flask应用 │ ├── api_router.py # 路由定义 │ └── json_response.py # 统一响应格式 ├── frontend/ # 前端工程 │ ├── src/ │ │ ├── views/ # 页面组件 │ │ ├── components/ # 通用组件 │ │ ├── router/ # 路由配置 │ │ └── api/ # Axios请求封装 │ └── vue.config.js └── data/ # 原始数据备份 ├── raw_html/ # 爬取的HTML快照 └── processed/ # 清洗后的CSV目录分开的最大好处是论文写“模块设计”章节时可以逐个模块截图、逐个模块讲,条理非常清晰。答辩的时候老师问你某个文件是干嘛的,你能立刻说出它在整个链路中的位置,这一点极其加分。
2.3 从零启动项目的关键步骤
动手写代码前,一定要先把MySQL的库表建好。这个项目的表结构我设计了很久,核心是商品表要预留足够的字段用于后续特征工程:
CREATE DATABASE taobao_data DEFAULT CHARACTER SET utf8mb4; CREATE TABLE product_info ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2), sales_count INT DEFAULT 0, shop_name VARCHAR(100), location VARCHAR(50), detail_url VARCHAR(500), category VARCHAR(50), crawl_date DATE, INDEX idx_category (category), INDEX idx_price (price) ) ENGINE=InnoDB; CREATE TABLE shop_info ( id INT PRIMARY KEY AUTO_INCREMENT, shop_name VARCHAR(100) NOT NULL, shop_level VARCHAR(20), good_rating DECIMAL(5,2), fan_count INT, product_count INT ) ENGINE=InnoDB;这样设计的理由是:category字段和crawl_date要频繁做分组统计,不加索引的话数据量大了之后查询会明显变慢;detail_url用VARCHAR(500)是因为淘宝的商品链接很长,留短了会被截断。
3. 核心模块实现与关键细节拆解
3.1 Selenium爬虫模块:动态页面采集的三个要点
爬虫是整个项目的第一步,也是最容易劝退人的一步。编写taobao_spider.py时,我采用了三个关键策略来保证采集的稳定性。
首先是登录态维护。淘宝搜索页不登录也能看数据,但翻页超过两三页就会弹出登录框。我的处理方法是用Selenium打开首页,手动扫码登录一次,然后往Chrome的User Data目录写入登录缓存。下次启动爬虫时直接加载这个用户目录,省去重复扫码的麻烦:
from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(): options = Options() options.add_argument('--user-data-dir=./chrome_profile') options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option('excludeSwitches', ['enable-automation']) driver = webdriver.Chrome(options=options) return driver其次是滑动加载和翻页控制。淘宝的搜索结果页是懒加载模式,滚动到页面底部才会加载下一批商品。如果直接requests请求,看到的只有前20个。Selenium的优势在于可以模拟滚动:
from selenium.webdriver.common.by import By import time def scroll_load(driver): last_height = driver.execute_script('return document.body.scrollHeight') for i in range(5): driver.execute_script('window.scrollTo(0, document.body.scrollHeight)') time.sleep(2) new_height = driver.execute_script('return document.body.scrollHeight') if new_height == last_height: break last_height = new_height这里的sleep(2)非常关键。太快滚动会导致页面加载跟不上,漏掉商品卡片;太慢又严重影响效率。实测2秒是最平衡的节奏。
最后是商品卡片解析。淘宝的商品列表页结构在PC端比较稳定,商品卡片有统一的CSS类名。我用CSS选择器定位商品节点,然后逐个提取标题、价格、销量、店铺等信息:
def parse_product_cards(driver): cards = driver.find_elements(By.CSS_SELECTOR, 'div.Content--contentInner--QVTcU0M > div') products = [] for card in cards: try: title = card.find_element(By.CSS_SELECTOR, 'span.Title--title--jZfHvLp').text price = card.find_element(By.CSS_SELECTOR, 'span.Price--priceText--V2sB7M1').text sales = card.find_element(By.CSS_SELECTOR, 'span.Price--realSales--F6Aa9vC').text shop = card.find_element(By.CSS_SELECTOR, 'div.ShopInfo--shopName--Nv6CjFf').text # 清洗字段后加入列表 products.append({ 'title': title, 'price': float(price.replace('¥', '')), 'sales': parse_sales(sales), 'shop': shop }) except Exception as e: # 单张卡片解析失败不影响整体 continue return products这里的异常处理太重要了。淘宝页面偶尔会有广告卡片、推荐卡片混入,导致某些节点定位不到。一开始我没加try-except,结果整个采集任务跑一半就崩溃。加了异常忽略后,单条数据丢失是可以接受的,整体采集任务能顺利跑完。
3.2 ETL与数据仓库构建:从脏数据到干净表结构
采集下来的原始数据非常脏,不处理直接进库的话,后面机器学习模型的效果会惨不忍睹。我在clean_data.py里做了一整套清洗流程,主要处理这几个问题:
价格字段的乱码和特殊符号。淘宝页面上的价格经常是“¥ 129.00”、“券后¥99”这种带前缀的字符串,还有用“--”代表暂无价格的情况。我用正则统一处理:
import re def clean_price(raw_text): if not raw_text or raw_text == '--': return None digits = re.findall(r'\d+\.?\d*', raw_text) if digits: return float(digits[0]) return None销量字段的“万”单位换算。淘宝销量经常写成“1000+人付款”、“2.5万+人付款”。如果直接转float会报错,必须做单位换算:
def parse_sales(sales_text, base_label='人付款'): if base_label in sales_text: sales_text = sales_text.replace(base_label, '') if '万' in sales_text: return int(float(sales_text.replace('万', '')) * 10000) return int(re.sub(r'\D', '', sales_text))重复数据去重。爬虫跑多轮后,同一件商品可能会被重复采集。我在写入MySQL前用DataFrame的drop_duplicates方法,以标题+店铺作为联合去重键:
df = pd.read_csv('raw_products.csv') df = df.drop_duplicates(subset=['title', 'shop_name'], keep='last')写入数据库的封装我是用pandas的to_sql方法,配合pymysql驱动,这样省去了手写INSERT语句的大量时间:
from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://root:password@localhost:3306/taobao_data') df.to_sql('product_info', engine, if_exists='append', index=False)3.3 机器学习建模:让数据说话的核心环节
机器学习部分是整个系统技术含量的集中体现。我给系统设计了三类分析任务,每类对应一个具体业务问题。
任务一:商品价格预测模型
解决的问题是:给定商品的特征(销量、店铺等级、所在地区、类目),预测合理的价格区间。这本质是个回归问题,我用随机森林回归器。特征工程上,我把店铺等级做了编码映射:“天猫”=3、“皇冠”=2、“钻石”=1、“心级”=0,然后把商品标题的长度也作为特征——标题越长的商品,通常包含更多的卖点关键词,价格也会相对高一些。
from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder def train_price_model(df): features = df[['sales_count', 'shop_level_encoded', 'title_length', 'location_encoded']] target = df['price'] X_train, X_test, y_train, y_test = train_test_split( features, target, test_size=0.2, random_state=42 ) model = RandomForestRegressor( n_estimators=200, max_depth=10, min_samples_leaf=3, random_state=42 ) model.fit(X_train, y_train) score = model.score(X_test, y_test) print(f'R² score: {score:.4f}') return model为什么选随机森林而不是线性回归?因为商品价格和特征之间的关系不是线性的。举个例子:销量从100涨到1000,价格可能只微涨;但从1000涨到10000,价格可能会因为爆款效应发生跳变。随机森林天然能挖掘这种非线性关系,而且对异常值更鲁棒。
任务二:商品聚类分析
聚类解决的业务问题是:不靠人工标注,自动把商品分成几类,看每一类的共性是什么。我用KMeans算法,选的特征是价格、销量、店铺等级。聚类数k的选择用了肘部法则——计算k从2到10的簇内误差平方和(SSE),选择曲线拐点处的k值。
from sklearn.cluster import KMeans import matplotlib.pyplot as plt def find_best_k(features): sse = [] for k in range(2, 11): km = KMeans(n_clusters=k, random_state=42, n_init=10) km.fit(features) sse.append(km.inertia_) # 取拐点作为聚簇数 best_k = sse.index(min(sse)) + 2 # 简化处理,实际要结合图表判断 return best_k实践中发现k=4时聚类效果最好,四类商品可以解读为:低价走量款、中端品质款、高端品牌款、长尾冷门款。这个分类结果本身就能写进论文的“实验分析”章节,很有说服力。
任务三:商品标题情感分析
严格来说,这个不算传统机器学习,但它是面试官最爱问的“NLP入门”任务。我用SnowNLP库对商品标题做情感倾向打分,得分在0到1之间,越接近1代表标题传递给买家的正面情绪越强烈。分析逻辑是提取标题中的促销词、品质词、热门词,综合计算情感分,再统计不同类目的平均情感分,就能判断哪些类目的商品标题更注重氛围烘托。
3.4 Echarts可视化与Vue.js前端整合
前端是整个项目最先被抓眼球的部分,也是答辩时老师第一眼看到的东西。我搭了四个核心可视化页面:数据概览看板、类目分析页、价格分析页、词云与排行榜。
数据概览看板放了五个核心KPI卡片(商品总数、平均价格、总销量、店铺数、采集日期),下面配一张Echarts折线图展示最近7天采集量的变化趋势。这张折线图的X轴标题在Echarts 5.x版本里需要额外配置,否则会出现刻度文字重叠:
xAxis: { type: 'category', data: this.dateList, axisLabel: { interval: 0, rotate: 30 } }类目分析页设计了柱状图+饼图的联动布局。柱状图展示各类目的商品数量,点击某个柱形后,饼图更新为该类目的价格区间分布。联动效果是Echarts自带的事件机制实现的:
barChart.on('click', (params) => { this.updatePieChart(params.name) })价格分析页是我最满意的部分,用散点图把“价格-销量”关系呈现出来。四类聚簇商品用不同颜色标记,鼠标悬停能看到商品标题。这个图直接把机器学习聚类的结果动态可视化,完美展示了“数据挖掘到可视化”的完整链路。
词云图是这里最亮眼的。淘宝商品标题天然适合做分词和词频统计。我用jieba分词库对标题分词,过滤掉“包邮”“新款”“正品”这类无意义词后,生成Top100高频词,再传给Echarts词云组件:
import jieba from collections import Counter def generate_word_freq(df, top=100): words = [] for title in df['title']: words.extend(jieba.lcut(title)) stopwords = ['包邮', '新款', '正品', '特价', '官方'] valid_words = [w for w in words if len(w) > 1 and w not in stopwords] return Counter(valid_words).most_common(top)前端Vue.js这边,我用了Vue的mounted钩子函数在页面加载时统一请求接口,通过this.$nextTick确保DOM渲染完成后再初始化Echarts实例,避免图表因为容器尺寸为0而显示空白:
mounted() { this.fetchData().then(() => { this.$nextTick(() => { this.initChart() }) }) }4. 实操踩坑实录与排查技巧汇总
4.1 爬虫运行时页面跳登录的终极解法
最大的坑绝对是登录态问题。我用Selenium打开淘宝首页,运行到第二页就弹出二维码登录框。尝试过添加cookie、修改user-agent、设置无头模式,都有一定概率被识别。
最后找到的靠谱方案是:用Chrome的debugging模式,先手动登录一次,再复用浏览器session。这样Selenium和手动浏览器共享同一个登录态,而且全程看起来就是一个真实用户在操作。
chrome --remote-debugging-port=9222 --user-data-dir=./chrome_profile启动爬虫时直接连接这个调试端口:
options.add_experimental_option('debuggerAddress', '127.0.0.1:9222') driver = webdriver.Chrome(options=options)这个方法实测能稳定跑完整个采集流程,而且不容易触发风控。
4.2 数据入库时的编码与类型陷阱
MySQL写入时报错最多的是中文乱码和字段类型不匹配。解决中文乱码,建库时必须显式指定utf8mb4字符集,同时写库时要把DataFrame的字符串列统一转成str类型,避免某些单元格是float导致的拼接异常。
字段类型上最大的坑是价格列。淘宝的价格偶尔会出现“None”或空字符串,直接转float会报ValueError。我统一做了兜底处理:
df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0)这样脏数据转成0,在SQL查询时用WHERE price > 0过滤掉即可,不影响整体分析。
4.3 Vue跨域请求Flask接口的解决方案
前后端分离架构下,前端在localhost:8080,后端在localhost:5000,浏览器会有跨域限制。我不建议在后端硬配置CORS,原因是对外暴露全部跨域权限有安全风险。我选择的方案是开发时用Vue CLI的代理转发:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } }前端所有请求都写成/api/products这种相对路径,由webpack-dev-server代理到Flask服务。答辩演示时用一个端口就够,不用解释跨域问题。
4.4 Chromedriver版本不匹配的快速修复
Selenium最经典报错是SessionNotCreatedException: This version of ChromeDriver only supports Chrome version XX。原因是Chrome浏览器经常自动更新,但Chromedriver还是旧版本。
快速修复方案是让Selenium自动下载匹配的driver。在Selenium 4.6以上版本中,只需要设置:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)webdriver-manager库会自动检测当前Chrome版本,下载对应的Chromedriver到本地缓存。这个库帮我省掉了大量手动折腾时间。
5. 答辩演示要点与项目扩展方向
5.1 答辩时老师的注意力分配
毕业设计答辩有黄金五分钟原则:前五分钟决定了老师对你的印象。我的答辩流程是这样设计的:
- 前30秒:直接展示可视化大屏,让老师看到炫酷的数据图表,吸引注意力
- 接下来1分钟:演示Selenium实时采集数据,强调这项技术能证明代码是真实可运行、不是摆样子的
- 然后2分钟:讲机器学习模型训练结果,用聚类散点图和价格预测的R²分数证明分析有深度
- 最后1分半:回答技术选型和架构问题
这里有一个非常实用的技巧:答辩前提前把爬虫任务配置好,如果需要现场演示采集,一定先把登录态缓存在chrome_profile里,要不然临场扫码会非常尴尬。
5.2 这个项目能扩展的方向
如果你的毕设还需要进一步进阶,我建议从三个维度入手:
数据规模扩展:把采集范围从搜索关键词扩展到位居Top榜的类目,增加采集的覆盖面。可以做增量采集,用采集日志表记录上次采集的页码,每天只采新增商品。
算法深度扩展:把随机森林换成XGBoost或者LightGBM,价格预测精度会有明显提升。聚类换成DBSCAN能自动识别异常商品。如果想做推荐系统,可以基于用户浏览历史构建协同过滤模型。
实时性扩展:后端加一个定时任务框架,比如APScheduler,每天定时采集和分析。前端通过WebSocket实现数据的动态刷新,这样系统就是一个持续运行的实时监控平台,而不仅仅是一次性分析。
5.3 过程中关于工程习惯的三点经验
做完这个项目,我对工程化代码的理解提升了一个档次。
第一,每个模块都要有日志。爬虫跑的时间长,中途出问题很难定位。我在每次采集循环里都加了一行logger.info(f'第{i}页采集完成,共{len(products)}件商品'),排查问题时看日志比看代码快得多。
第二,原始数据一定留备份。我每轮爬完都把HTML快照存一份,CSV原始文件也单独存一份。有次清洗脚本写错了,把价格列全改成0,还好有原始备份才能重新处理。
第三,数据库连接用完必须释放。用pandas的to_sql写库时,如果频繁建立连接而不关闭,MySQL会报Too many connections错误。我用上下文管理器自动关闭连接:
@contextmanager def db_connection(): conn = pymysql.connect(host='localhost', user='root', password='***') try: yield conn finally: conn.close()这套代码风格和工程习惯,在后续的实习面试里也帮了不小的忙。面试官对毕业设项目手搓出来的完整数据链路是认可度很高的,尤其是能讲清楚每层是怎么联动、遇到问题时怎么排查的候选人。
最后分享一个个人体会:毕设难的不是技术点本身,而是把每个技术点用一条完整的数据流串起来。记录了你从爬虫拿到原始HTML到最终画出可视化大屏的全过程,这种“完成了一个真实系统”的成就感,才是毕业设计最值钱的部分。你把这个流程吃透了,以后工作中任何数据相关的需求,你都能迅速定位到哪个环节需要优化。