淘宝商品爬虫,看着简单,但要是把 Python、Flask、Selenium、Spark、Hadoop 和 ECharts 串成一个完整的毕设项目,那工作量就不是抓几个商品标题那么简单了。当时我拿这个题目做毕业设计,前前后后折腾了两个月,踩过的坑比爬到的商品还多。今天把这套系统的设计思路、关键代码和实操教训整理出来,给后面选这个方向的同学一个能落地的参考。
这套系统解决的问题很实际:淘宝页面大量数据是 JS 动态渲染的,直接用 requests 拿不到商品列表;拿到数据之后,几万条商品记录又没法靠 Excel 处理;最后要做可视化报表,还得有一套 Web 页面展示分析结果。所以技术选型就是围绕这三个痛点展开的,不是故意堆大厂名词。
1. 项目整体架构与设计思路
1.1 为什么是 Python + Flask + Selenium + Spark + Hadoop
先说爬虫采集端。淘宝的商品列表、搜索结果、详情页都是异步加载的,尤其登录后的首页,完全是 React 框架渲染出来的 DOM。requests 能拿到的是页面框架的 HTML,里面的商品数据全靠 XHR 接口返回。要想拿到真实数据,最直接的办法就是用 Selenium 驱动一个真实浏览器,等页面渲染完了再解析。虽然慢,但稳定性高,适合课程设计和毕设这种对抓取速度不敏感的场合。
然后是数据处理层。如果只是抓几十上百条数据,用 pandas 处理就够了。但毕设题目既然写了 Spark、Hadoop,那就得把架构撑起来。Hadoop 负责 HDFS 存储原始采集结果,Spark 负责清洗和统计分析。这样做的好处有两点:一是分区存储和海量数据处理的能力真实存在,不是摆设;二是答辩的时候,面试官问“你为什么要用大数据框架”,你可以从数据量、计算效率、容错机制三个角度说得清楚。
Flask 在这里的角色是 Web 展示层。它轻量、灵活,不需要像 Django 那样绑死 ORM 和 Admin,适合快速开发数据可视化后台。ECharts 则是纯前端图表库,通过 Ajax 从 Flask 接口拉 JSON 数据,渲染成折线图、柱状图、词云、地图。
1.2 系统模块划分
我一开始就按功能把系统拆成四个模块,每个模块独立开发,最后再串联:
- 采集模块:Selenium 驱动浏览器,负责登录、搜索、翻页、提取商品名称、价格、销量、店铺、评论数等字段,输出原始 JSON 或 CSV。
- 存储模块:先把原始数据落到本地文件,再上传到 HDFS 指定目录,保留原始数据副本。
- 计算模块:Spark 读取 HDFS 数据,做清洗、去重、分词、分类统计,把结果写回 MySQL 或者输出为汇总 JSON 文件。
- 展示模块:Flask 提供 REST API,ECharts 读取接口数据渲染可视化页面。
模块之间的数据流是先采集 -> 落盘 -> HDFS -> Spark -> MySQL/JSON -> Flask API -> ECharts。每个环节之间的数据格式要提前约定好,比如商品价格字段统一保留两位小数,销量字段是整数型,店铺名要清洗掉换行符和空格。不然 Spark 处理的时候,类型转换报错能让人崩溃。
1.3 分布式不是必须的,但能让项目完整
做这种项目时有个误区:以为用了 Hadoop 和 Spark 就必须搭一个大集群。其实伪分布式模式完全够用。Hadoop 配置 NameNode、DataNode 在同一个节点上,Spark 用 local[*] 模式跑,既能体现分布式计算的思想,又不会因为集群资源不足导致任务失败。真正需要分布式的是工作上千万级别的数据,毕设抓几万条数据,伪分布式就是一套很好的演示环境。
我在设计里还额外加了一个“原始数据副本”机制:采集到的 JSON 文件会同时落在本地和 HDFS 两个目录。这个设计在答辩时很加分,因为你可以明确解释“本地是快照备份,HDFS 是数据仓库源头”,也方便后续数据重算。
2. 环境准备与基础搭建
2.1 Python 版本和虚拟环境
我用的 Python 3.8,解释器版本对 Selenium 和 Spark 的兼容性影响不小。建议不要用太新的 Python 3.12,某些第三方库可能还没适配。虚拟环境一定要建,用 conda 或 venv 都行,避免系统环境被搞乱。我当时创建了一个专门的环境:
conda create -n taobao_env python=3.8 conda activate taobao_env pip install flask selenium pymysql pyspark这里有个经验:pyspark 的安装版本要和本地安装的 Spark 主版本一致,不然运行时经常报 SparkContext 初始化异常。如果你的 Spark 是 3.0 或 3.2,pyspark 就用对应的版本,不要默认装最新。
2.2 Selenium 驱动配置
Selenium 本身的安装很简单,坑主要在浏览器驱动。Chrome 版本和 chromedriver 版本必须严格对应,差一个小版本都可能导致“session not created”错误。我以前老手动下载驱动,后来发现用 WebDriver Manager 自动管理更方便:
from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(ChromeDriverManager().install())如果你在内网环境不能自动下载,那就手动去 Chrome for Testing 网站下载对应版本驱动,然后指定 executable_path。别用最新版驱动的“recommended”版本,一定看浏览器主版本号。
2.3 Hadoop 伪分布式与 Spark 本地模式
Hadoop 搭建这里不详细展开,只说几个关键点。配置 core-site.xml 里的 fs.defaultFS 为 hdfs://localhost:9000,hdfs-site.xml 里设置副本数 replication=1 即可。启动前要 ssh localhost 免密登录。很多人在这一步卡住,其实用 ssh-keygen 生成公钥,然后 cat id_rsa.pub >> authorized_keys 就能解决。
Spark 本地模式就简单得多,直接下载预编译的二进制包,解压后设置 SPARK_HOME 和 PATH。启动时会默认使用 local[*],SparkSession 会自动探测可用核数。如果你的笔记本内存只有 8G,建议在 spark-defaults.conf 里加上 spark.driver.memory 2g,别把整个内存都吃掉。
2.4 Flask 项目结构
Flask 的目录结构我习惯这样划分:
taobao_system/ ├── app.py # Flask 入口 ├── crawler/ │ └── taobao_spider.py # Selenium 采集 ├── data/ │ ├── raw/ # 原始 JSON │ └── result/ # 分析结果 JSON ├── scripts/ # Hadoop/Spark 提交脚本 ├── static/ │ └── echarts.min.js └── templates/ └── index.htmlFlask 这层不需要太复杂的蓝图,因为接口数量不多。我当时只写了两个接口:/api/overview 返回总商品数、价格区间分布、销量 TOP10;/api/search 支持关键词过滤。这些接口直接读取 result 目录下 Spark 生成的 JSON 文件,避免每次请求都去查 MySQL。
3. 淘宝商品采集模块:Selenium 实战
3.1 为什么要用 Selenium 而不是 requests
淘宝页面经过多次改版,商品列表完全依赖异步接口。我测试过直接用 requests 调用 API,接口的签名加密参数(比如 sign)相当复杂。而 Selenium 相当于一个真实用户,浏览器自动执行 JS,天然解决了动态渲染的问题。代价就是慢,单页商品抓取需要两秒,带上滚动加载可能要五秒。
如果你不想用 Selenium,也可以研究 Playwright 或 pyppeteer,它们的 API 更现代。但 Selenium 的社区资料更多,遇到问题也好搜。
3.2 登录和 Cookie 处理
淘宝搜索页不需要登录也能看数据,但价格、销量有时需要登录态。爬虫系统里我加了一个可选登录功能:用 Selenium 打开登录二维码页面,等用户扫码后把 Cookie 持久化保存到文件。下次采集直接加载 Cookie,避免重复扫码。
import os, json, time from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument('--disable-blink-features=AutomationControlled') driver = webdriver.Chrome(options=options) driver.get('https://login.taobao.com/') time.sleep(20) # 等待扫码 cookies = driver.get_cookies() with open('taobao_cookies.json', 'w') as f: json.dump(cookies, f)加载 Cookie 时要注意域名限制,Selenium 只有在访问对应域名时才能添加 cookie,所以要先打开淘宝首页再 add_cookie。
3.3 页面解析和滚动加载
商品列表是瀑布流布局,一次只加载一屏,必须循环滚动页面才能加载更多。我写了一个滚动函数:
def scroll_page(driver, max_scroll=5): for _ in range(max_scroll): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(2)滚动太快容易被反爬机制识别,所以我每滚动一次就随机睡眠 1.5 到 3 秒。然后通过 CSS 选择器提取商品卡片。淘宝商品 card 的类名在不同版本里会变,最稳妥的方式是先写 XPath 解析在 Chrome DevTools 里验证,再写进代码。
商品名称、价格、销量、店铺名的 XPath 大致类似:
title = driver.find_element('.//div[contains(@class, "title")]/span').text price = driver.find_element('.//div[contains(@class, "price")]/strong').text sales = driver.find_element('.//div[contains(@class, "deal")]').text但注意淘宝经常改类名,一旦找不到元素就抛异常。我统一封装了一个 safe_find 方法,找不到返回空字符串,而不是让整个脚本崩溃。
3.4 反爬应对和合规提醒
这里说几个实测有效的思路:设置 user-agent 和 disable-blink-features,开启 headless 模式(但 headless 容易被检测),随机睡眠,减少采集频率。淘宝的反爬主要集中在 IP 频率和账号行为上,如果你要做到核心功能,建议每次采集间隔 5 秒以上,单次采集数量控制在 500 条以内,避免账号异常。
合规上要提醒一句:爬取数据用于学习和毕设没问题,但不要公开传播采集到的数据,更不要用于商业盈利。淘宝的商品数据受版权保护,老实的做法是只展示自己的分析结果,而不是打包导出原始数据库。
3.5 断点续爬与数据清洗
采集过程中网络中断、浏览器卡死是常事。我写了一个“已完成页码记录”机制,把当前爬到的页码写入 progress.txt,重启后从这个页码继续。数据清洗放在采集端只做基础处理:去掉空白字符、统一币种符号、把“5000+人付款”转换为数字、把“包邮”等促销文字拆出来。
价格字段里的“¥”要删掉,促销价“¥9.9”和原价“¥39.9”要分开存储,这样后续统计价格区间才准确。销量字段如果是“1.2万+”,要乘以 10000。
4. 数据存储与 Spark 分析
4.1 原始数据落地
采集后的数据先保存为 JSON Lines 格式,每行一个商品记录。不要用普通 JSON 数组,因为 Spark 读取多行 JSON 时更高效。示例行:
{"title": "春季新款休闲鞋", "price": 129.0, "sales": 3400, "shop": "XX旗舰店", "city": "杭州"}然后通过 HDFS 命令上传:
hdfs dfs -put /data/raw/*.json /user/taobao/raw/4.2 Spark 清洗和统计
Spark 脚本用 pyspark 写,核心步骤是读 JSON、去重、过滤无效数据、统计价格区间、统计销量 TOP10、计算平均价格。
from pyspark.sql import SparkSession from pyspark.sql.functions import col, count, sum, avg, when spark = SparkSession.builder.appName("TaobaoAnalysis").getOrCreate() df = spark.read.json("hdfs://localhost:9000/user/taobao/raw/*.json") df = df.dropDuplicates(["title"]) df = df.filter(col("price").isNotNull() & (col("price") > 0)) df = df.filter(~col("title").contains("广告")) # 价格区间 df.registerTempTable("goods") price_level = spark.sql(""" SELECT CASE WHEN price < 50 THEN '0-50' WHEN price >= 50 AND price < 100 THEN '50-100' WHEN price >= 100 AND price < 200 THEN '100-200' ELSE '200+' END AS price_range, COUNT(*) AS cnt FROM goods GROUP BY price_range """) res = price_level.toJSON().collect() with open("/data/result/price_range.json", "w") as f: for line in res: f.write(line + "\n")这里建议把结果写回 HDFS 而不是本地文件系统,这样后续计算节点都能访问。我为了演示方便同时也输出了一份到本地,Flask 读取更快。
4.3 Hadoop 在这里到底干了什么
有些人会觉得数据量不大,Hadoop 很鸡肋。但在系统架构里,Hadoop 至少承担三个真实角色:
- 数据仓库:原始 JSON 文件统一存到 HDFS,有目录权限管理,方便多个任务共享。
- 分布式文件系统:Spark 读取 HDFS 时是分布式的,可以把文件分成多个 partition 并行处理,体验大数据计算的完整调用链。
- 容错机制:NameNode 和 DataNode 的副本机制保证了原始数据不丢失。
就算毕设只抓了一万条数据,只要你能讲清楚“如果数据增长到百万条,这套架构如何扩展”,这个设计的价值就体现了。
4.4 统计分析指标设计
我做的是商品消费趋势分析,包括:
- 价格区间分布(柱状图)
- 销量 TOP10 商品(横向柱状图)
- 不同价格段商品平均销量(折线图)
- 店铺所在地城市分布(地图)
- 商品标题关键词词频(词云图)
这些指标都是 Spark 先聚合好,再输出为 JSON。这样 ECharts 画图的时候不用再做复杂计算,前端只负责渲染。
5. Flask Web 展示与 ECharts 可视化
5.1 Flask 接口怎么写
Flask 接口尽量保持简单,返回 JSON 就行。我固定了数据文件的路径,启动时先加载一次到内存,防止每次请求都读文件:
from flask import Flask, jsonify app = Flask(__name__) def load_data(): with open('/data/result/price_range.json', 'r') as f: return [json.loads(line) for line in f] @app.route('/api/overview') def overview(): return jsonify(price_range=load_data(), top_sales=load_top(), city_stats=load_city()) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)注意 debug 模式下会启动两个进程,可能造成端口占用。我一开始也没注意,后来发现 debug=True 时 Flask 会自动重启,导致浏览器请求一半会断,改成 debug=False 就稳定了。
5.2 ECharts 前端图表
ECharts 我从官网下载了 npm 包里的 echarts.min.js 放到 static 目录,然后写一个 HTML 模板。图表区域用几个 div 布局,每个 div 的 id 分别给 chart1、chart2、chart3。初始化时通过 fetch 请求接口,拿到数据后 setOption。
fetch('/api/overview') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('price_chart')); chart.setOption({ xAxis: { type: 'category', data: data.price_range.map(d => d.price_range) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.price_range.map(d => d.cnt), itemStyle: { color: '#5470c6' } }] }); });有几个容易踩的坑:图表容器必须有明确宽度和高度,否则 echarts.init 报错;图表在页面初始化时如果数据还没回来,会渲染成空白,所以要先加载 JSON 再设 option;词云图的类型 series 是 wordCloud,需要额外引入 echarts-wordcloud 插件,我最后为了简化没做词云,改成了 TOP20 关键词表格。
5.3 前后端分离还是模板渲染
我选择的是 Flask 模板渲染 + 原生 JS,不额外用 Vue 或 React。原因很简单,毕设时间有限,模板渲染够用。传参方式:render_template('index.html', data=initial_data),这样页面一打开就有数据,后续再主动刷新。
但前端只靠模板渲染有一个问题,Spark 分析结果更新后,页面要手动刷新才能看到最新数据。所以我加了一个“重新分析”按钮,点击后调用 /api/rerun,这个接口会调用 subprocess 重新执行 Spark 脚本。这里要小心别阻塞 Flask 主线程,我用的是 subprocess.Popen 后台运行,前端轮询状态。
5.4 部署和访问控制
Flask 默认只能本机访问,要让局域网访问必须设置 host='0.0.0.0'。正式演示时我用的是 9000 端口,因为 Hadoop NameNode 用的是 9870,Spark UI 是 4040,避免冲突。另外我加了简单的 token 验证,请求时带 header: Authorization: Bearer xxx,防止别人直接访问你的接口。
6. 常见问题与排查技巧实录
这一节全是实战记录,按问题类型列个速查表。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| Selenium 报 session not created | Chrome 与 chromedriver 版本不匹配 | 下载对应版本驱动,或用 WebDriver Manager 自动匹配 |
| 找不到商品元素 | 淘宝页面 AJAX 未加载完 | 用 WebDriverWait 等待元素出现,增加轮询间隔 |
| Selenium 被检测出爬虫 | webdriver 特征明显 | 禁用 AutomationControlled 特征,加 user-agent |
| Spark 内存不足 OOM | 分配给 driver/executor 内存过小 | 设置 spark.driver.memory=2g,spark.executor.memory=1g |
| HDFS 文件无法删除 | 文件被 Spark 任务占用 | 先 stop SparkSession 再删,或等待释放 |
| Flask 端口占用 | debug=True 双重进程 或 残留进程 | 用 pkill -f app.py 清理,改为 debug=False |
| 中文乱码 | 终端编码或文件编码不统一 | JSON 读取时指定 encoding='utf-8',HDFS 数据文件统一 UTF-8 |
| ECharts 图表不显示 | 容器高度为 0 或数据格式不对 | 检查 div 有没有 height,控制台看 fetch 返回的数据结构 |
| 采集过程中断 | 网络波动或 Chromedriver 崩溃 | 写断点记录,定时重启驱动 |
| MySQL 连接数过多 | 每次请求新建连接 | 用连接池或直接读 JSON 文件 |
下面展开讲几个我最头疼的问题。
6.1 Selenium 元素定位:淘宝页面改版太频繁了
第一次写采集脚本时,我把所有元素都用 XPath 硬编码。第二天运行就发现价格定位失败,因为淘宝把类名从 Price--xxx 改成了 Price--yyy。后来我改成“多重定位”策略:先尝试 CSS,再尝试 XPath,再尝试 JS 执行脚本直接获取文本。这样即使改版,也能多维持一段时间。
真实原因是淘宝前端做了模块化更新,类名会自动加 hash 后缀,这是为了缓存优化。所以最可靠的方式是定位父级容器,然后在容器内用 text 属性过滤。比如先找所有包含“价格”文本的 span,再取它的兄弟节点。虽然丑,但稳定性明显提高。
6.2 Spark 读取本地 JSON 与 HDFS 的区别
一开始我直接用 spark.read.json("file:///data/raw/"),发现效率很低,而且每次都要同步到集群。后来改为上传 HDFS 后读取。这里有个性能优化点:如果 JSON 文件是小文件(几十 KB),Spark 会分配很多任务,反而慢。我先用 hdfs dfs -text 将所有小文件合并成一个文本文件再处理。
合并命令:
hdfs dfs -cat /user/taobao/raw/*.json | hdfs dfs -put - /user/taobao/merged.json这样 Spark 只读一个文件,任务数少一大半,清洗速度明显提升。如果你用 Hive,可以把这些小文件直接 load 到一张外部表,自动合并分区。
6.3 Flask 异步刷新分析结果
我在做“重新分析”按钮时,遇到一个经典问题:点击后请求 Flask,Flask 里通过 subprocess 跑 Spark 脚本,但页面一直转圈。这是因为同步等待导致 Flask 阻塞。改成 Popen 后立即返回“已开始”,再写一个 /api/status 接口轮询脚本状态。
import subprocess proc = subprocess.Popen([ 'spark-submit', '--master', 'local[*]', 'scripts/analyze.py' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)状态进程保存在全局变量里,注意多进程环境可能不共享,最好用 Redis 或文件锁记录状态。毕设规模下,写一个 status.txt 文件也能用。
6.4 MongoDB 还是 MySQL
我的系统里用到了一个小型 MySQL 来存储最终分析结果,原因是为了做商品搜索功能。但后来发现 MySQL 对 JSON 类型的支持不如直接读文件方便。如果你只是想展示,直接读 JSON 文件就够了。如果想加搜索,SQL 更适合:LIKE 查询标题没问题。但要注意中文全文检索不如分词器,建议用简单 LIKE 或者接入 Elasticsearch,但那就是另一个项目了。
7. 从毕业设计到工程化:扩展建议
7.1 采集框架升级
Selenium 适合演示,但效率低。如果你想扩展成真正能长期运行的采集系统,建议换成 Scrapy + Playwright for Python。Scrapy 负责并发调度和管道,Playwright 负责处理动态内容。这样采集速度提升十倍以上,而且自带限速、去重、增量爬取。我后来做实际项目时就是这么迁移的,把 Selenium 那套代码里的数据提取函数单独抽出来,复用率很高。
7.2 增加消息队列和调度
如果有多个采集任务,可以使用 Celery + Redis。采集任务发到队列,Spark 分析任务放在 Celery worker 里定时执行。每次采集完自动触发分析,分析完再推送结果到前端。这个架构可以说明你对异步任务的掌握,加分项明显。
7.3 可视化增强:交互式图表
ECharts 加上 dataZoom 组件后,用户可以在前端缩放查看某个价格区间的细分。我后来给销量 TOP10 加了点击下钻功能:点击某个商品,显示该店铺的标题关键词分布。实现不复杂,主要是 Flask 接口增加一个 route,接收商品名称,返回该商品的评论情感倾向统计。这需要爬主评论区,又是另一个模块了,毕设如果时间够可以试试。
7.4 用 Docker 打包整个环境
如果你不想在答辩现场配环境,可以把 Hadoop、Spark、Flask 分别做成 Docker 镜像。用 docker-compose 一键启动。注意 Hadoop NameNode 需要暴露 9870 端口,Spark Master 暴露 7077 和 8080,Flask 暴露 9000。数据卷挂载到宿主机,避免容器重启数据丢失。这个方案我试过,对现场演示很有帮助。
7.5 代码结构优化建议
所有配置项(数据库密码、Cookie 路径、HDFS 目录)都放在 config.py 里,不要在代码里硬编码。写一个 DataPreprocessor 类,负责清洗字段。写一个 AnalysisResult 类,负责把 Spark 结果转成 ECharts 需要的数据结构。这样答辩时讲代码,模块边界很清楚,别人一看就觉得有工程意识。
最后再分享一个小技巧:采集数据时,记得在每条记录里加一个 timestamp 字段。后期做时间维度分析、展示“今日新增商品数”时,这个字段就能派上大用场。我自己因为当初没加,后来重新跑了一遍采集任务才补上,白白浪费了一个下午。
这个系统做完,收获最大的一点是:不要被“大数据”三个字吓住。把一条数据流从头串到尾,搞清楚每一步为什么存在,比单纯用一个高大上的组件有用得多。就算你的 Spark 跑在 local 模式,Hadoop 只是伪分布式,只要架构清晰、逻辑闭环,它就是一个合格的大数据实战项目。希望这篇分享能帮后面做类似题目的同学少走点弯路。