1. 项目的真实分量:它到底解决了什么问题
先说个我常遇到的场景:每隔一阵子就有学弟或者转行的朋友来问我,想找一个既能写在简历上、又能真正跑通全流程的 Python 项目。问的人多了我发现,大家的需求出奇一致——不想再要那种"爬个静态页面存 CSV"的玩具,但又怕一上来就搞高并发分布式把自己劝退。而这个标题里的项目,正好卡在了一个非常舒服的位置:单机可跑、技术栈全、有可视化闭环。
拆开看,这个平台做的事情其实是一条完整的数据链路:用selenium采集京东商品信息,把数据落到 MySQL,再通过Hadoop和Spark做分布式环境下的清洗与分析,最终交给Django做可视化页面展示。听起来环节很多,但每一环都是当前企业级数据项目里最常见的技术组件,而不是孤立的技术名词堆砌。
这个项目最适合三类人:正在做毕业设计或课程设计的学生、想从前端或纯 Python 脚本转向大数据方向的人、以及想在公司内部搭建一个"电商数据采集分析看板"但不想从零造轮子的开发。尤其是那些对大数据生态只停留在听过名字阶段的读者,跟着这个项目把 Hadoop 和 Spark 真实跑起来,比看一百篇"集群搭建教程"都管用。
我自己的体会是,这个项目的精髓不在于某个环节多高深,而在于它逼着你把爬虫、存储、计算、展示这四件事串起来。很多人在学校只写过 Jupyter 里的 Pandas,没见过数据从采集到出图表全流程是什么样;这个项目恰好补上了这个断层。
2. 为什么采集层选 Selenium 而不是 Requests
2.1 京东页面的动态渲染机制决定了技术选型
聊这个项目之前,先解决一个几乎所有人都会问的问题:京东商品页明明有接口,为什么不用 requests 直接请求?
答案是:京东的商详页和列表页,大量关键信息是通过 JavaScript 异步动态渲染的,尤其是价格、库存、促销信息。你用 requests 拿到的 HTML 源码里,很多盒子里是空的,或者只有一堆模板占位符。即便你找到了 xhr 接口,京东的风控会对高频请求做签名校验、滑块验证和 IP 限制,普通账号很难扛住。
Selenium 的思路完全不同:它直接驱动一个真实的浏览器内核(Chrome/Edge),等 JS 跑完再从 DOM 里取值。这等于绕过了"模拟请求"的复杂性,代价是速度慢、资源占用高,但换来的是一次采集的成功率和稳定性。对这类平台型采集项目来说,稳定压倒速度,这是第一条选型逻辑。
2.2 WebDriver 初始化与反检测配置
实践中最省心的启动配置我贴一下,都是在真实项目中验证过可直接用的:
from selenium import webdriver from selenium.webdriver.chrome.options import Options opts = Options() opts.add_argument("--headless=new") opts.add_argument("--no-sandbox") opts.add_argument("--disable-gpu") opts.add_argument("--window-size=1920,1080") opts.add_argument( "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" ) opts.add_experimental_option("excludeSwitches", ["enable-automation"]) opts.add_experimental_option("useAutomationExtension", False) driver = webdriver.Chrome(options=opts)其中excludeSwitches和useAutomationExtension是关键:它们用来去掉 WebDriver 的自动化标记。当然,这只能应对初级的检测,真正被风控盯上的时候还得配合代理池和随机延迟,这点后面单独说。
有一点必须提醒:--headless=new是新版 Chrome 的推荐写法,老旧的--headless参数在新版本里虽然兼容,但部分渲染行为有差异。如果你发现无头模式下拿不到某些动态数据,可以先切到有头模式调试一次,确认元素能正常加载再改回去。
2.3 滚动加载与显式等待是采集成功率的命门
京东的商品列表页采用滚动触底分页加载,你直接访问 URL 只能拿到第一屏数据。所以采集逻辑里必须模拟人类滚动,滚动一次后等待新元素加载,再继续滚,直到加载完全部商品。
这段逻辑我用的是最朴素也最可靠的方式:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def scroll_and_wait(driver, target_class, max_scroll=10): last_count = 0 for _ in range(max_scroll): # 滚动到页面底部 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") WebDriverWait(driver, 5).until( lambda d: len(d.find_elements(By.CLASS_NAME, target_class)) > last_count ) items = driver.find_elements(By.CLASS_NAME, target_class) last_count = len(items) time.sleep(random.uniform(0.8, 1.5)) return items注意这里用的是WebDriverWait而不是time.sleep硬等。等待条件写的是"商品元素数量增加",比固定睡几秒更高效——网络快的时候不浪费时间,网络慢的时候不会拿不完整。
2.4 字段提取与数据落地细节
页面加载出来后,提取字段用的是 CSS 选择器,比 XPath 更简洁:
title = item.find_element(By.CSS_SELECTOR, ".p-name em").text.strip() price = item.find_element(By.CSS_SELECTOR, ".p-price strong i").text.strip() shop = item.find_element(By.CSS_SELECTOR, ".p-shop a").text.strip()这里最容易踩的坑是:京东的部分字段在不同页面结构下选择器不同。比如某些商品的价格在i标签里,另一些则在span里。我建议在解析函数里写 try/except 兜底,匹配不到就置空,不要让单条数据异常拖垮整个采集任务。
数据落地我直接用的批量 INSERT,采集一批就写一批,而不是攒到最后一次性入库——因为浏览器进程一旦中途崩溃,你至少保住已经入库的数据。这里如果你的数据量预计在百万级以下,直接把 MySQL 作为存储终点完全没有问题。
3. Hadoop + Spark 在这条链路里的角色定位
3.1 MySQL 数据已经够用,为什么要引入大数据组件
这是项目里最容易被质疑的设计:数据量不大,MySQL 完全扛得住,引入 Hadoop 和 Spark 是不是为了凑技术栈?
从毕业设计的角度说,凑一点没关系,但从项目真实价值看,引入大数据组件有两个实际收益:
第一,处理逻辑与业务查询解耦。商品数据进了 MySQL 后,如果你要做复杂的聚合分析(比如按类目统计价格分布、按品牌统计销量份额、计算促销价和原价的优惠力度),直接用 SQL 写会很痛苦,而且会拖垮后续 Django 的在线查询性能。Spark 把预计算结果算好,Django 只查结果表,响应速度完全不是一个量级。
第二,清洗规则的统一管理。电商数据脏得很——同一件商品在不同列表页里的类目字段可能不一致,评价数有的是"10万+"有的是具体数字,价格里可能混入促销文案。用 Spark 写一套清洗逻辑,你把原始数据从 MySQL 同步到 HDFS,跑一次分布式任务,得到的是一张干净的标准表。以后想换别的数据分析框架处理,底子已经打好了。
3.2 数据同步:把 MySQL 数据送到 HDFS
这一步我建议直接用sqoop,或者更简单的方式——写一段 Python 脚本把 MySQL 数据导出为 CSV/Parquet 放到 HDFS。小项目不需要额外引入 sqoop,减少一个组件就少一个坑。
# 创建 HDFS 目录 hdfs dfs -mkdir -p /warehouse/jd/ods # 上传数据文件 hdfs dfs -put /opt/data/jd_item.csv /warehouse/jd/ods/如果你更喜欢用脚本导出,注意 CSV 的编码统一为 UTF-8,字段中包含换行符的商品描述要提前清洗,否则 Spark 读进来会错位。
3.3 数据仓库分层:不要一上来就暴力聚合
这个项目的仓库设计我建议按三层来:ODS、DWD、ADS。
- ODS(原始层):存同步过来的原始数据,不改变字段,只做格式统一。
- DWD(明细层):清洗、去重、类型转换、字段标准化。比如把"10万+"转成 100000,把价格字符串转成 DECIMAL。
- ADS(应用层):按业务需求做聚合,产出可供 Django 直接查询的统计结果表。
这样分层的意义在于:哪天你想要一个新的分析维度,不需要重跑整条链路,只要在 DWD 层基础上加一个聚合脚本就行。这是数据仓库和临时写脚本最大的区别。
3.4 Spark 核心处理逻辑与内存调优
Spark 任务的核心算子在代码里长这样(Scala 版本,比较直观):
val df = spark.read.option("header", "true") .csv("/warehouse/jd/ods/jd_item.csv") val clean = df .dropDuplicates("item_id", "datetime") .withColumn("price", regexp_replace(col("price"), "[^0-9.]", "").cast("decimal(10,2)")) .withColumn("comments", when(col("comments").contains("万"), regexp_replace(col("comments"), "万", "").cast("double") * 10000) .otherwise(col("comments").cast("double"))) clean.write.mode("overwrite").parquet("/warehouse/jd/dwd/jd_item_clean")然后是 ADS 层聚合,按类目算价格分位数、平均评价数、商品数等指标,输出到 MySQL。注意聚合结果回写 MySQL 用的是foreachPartition加批量插入,避免每条数据单独建连接。
Spark 在单机伪分布式模式下跑,最容易爆的是内存。我实测的经验是:spark.executor.memory=2g和spark.driver.memory=2g对百万级数据量足够,再大就考虑调大 executor 核数或放到真集群上去。如果你在本地跑频繁报 OOM,优先检查是不是并行度设置得太高,反而导致内存碎片化。
4. Django 可视化层的选型与接口设计
4.1 Django 在这里的角色:后端服务而非全栈框架
很多 Python 初学者以为 Django 可视化就等于 Django 模板语法渲染图表,这个理解偏了。在这个项目里,Django 的核心角色是提供数据接口服务——从 MySQL 读取 ADS 层的聚合结果,返回 JSON 给前端,由前端图表库负责绘制。
这背后是两个模块的职责分工:Django 负责数据 API、用户管理、权限控制;前端用 ECharts 负责图形。对比 Flask,Django 更适合这个项目的原因很简单:它自带 ORM、Admin 后台和用户认证,你不需要额外去配一堆第三方库,快速搭建管理页面的优势很明显。
4.2 项目结构规划与核心模型
建议在 Django 项目里建一个analysisapp,里面只做一件事:暴露统计接口。目录结构大致如下:
jd_project/ ├── manage.py ├── jd_project/ # 项目配置 ├── analysis/ # 分析应用 │ ├── models.py # 聚合结果模型 │ ├── views.py # 返回 JSON 的视图 │ └── urls.py └── templates/ # 可视化页面models.py里对应表的核心设计:
class CategoryStats(models.Model): category = models.CharField(max_length=64, verbose_name="商品类目") price_avg = models.DecimalField(max_digits=10, decimal_places=2) price_p50 = models.DecimalField(max_digits=10, decimal_places=2) item_count = models.IntegerField() comment_avg = models.DecimalField(max_digits=12, decimal_places=2) stat_date = models.DateField()注意,如果你的 ADS 表非常大或者来自 Spark 聚合,强烈建议不要通过 Django ORM 去同步建表。直接用 SQL 在 MySQL 里建好,Django 这边用managed = False的 Meta 选项映射即可。
4.3 API 与前端图表对接的完整逻辑
视图层我习惯用JsonResponse返回干净的 JSON,而不是让模板里嵌入大量 Python 变量:
def category_stats(request): rows = CategoryStats.objects.filter(stat_date=today) data = { "categories": [r.category for r in rows], "avg_price": [float(r.price_avg) for r in rows], "item_count": [r.item_count for r in rows], } return JsonResponse(data)前端页面就用原生 HTML + ECharts 的 CDN 文件,图表初始化时fetch这个接口拿数据。整个流程跑通之后,你会得到几个标准页面:类目价格分布柱状图、品牌销量排行条形图、评价数 Top 20 的表格,以及一个聚合概览卡片页。
这套设计里最省心的点是:前端不关心数据怎么算出来的,Django 不关心页面长什么样。以后你想把可视化换成 Vue 或者 React,后端接口一行都不用改。
5. 全流程复现:从空机器到看板上线
5.1 环境清单与版本配对
这一节我直接给出一份实测可行的版本组合,按这个走能省很多跨版本兼容的坑:
| 组件 | 推荐版本 | 关键备注 |
|---|---|---|
| Python | 3.9.x | 3.10 以上部分依赖需额外处理 |
| JDK | 1.8 | Hadoop 3.x 官方支持 8 |
| Hadoop | 3.3.6 | 单机伪分布式模式即可 |
| Spark | 3.3.x(对应 Hadoop 3) | 编译版本要与 Hadoop 匹配 |
| MySQL | 8.0 | 注意字符集选 utf8mb4 |
| Django | 4.2.x | 长期支持版本 |
| Selenium | 4.x | 配合 ChromeDriver 122+ |
版本问题往往是新手复现项目失败的第一个坎。比如 Hadoop 3.3 配 JDK 11 虽然能跑,但部分本地库会有兼容警告;Spark 3.2 配 Hadoop 2.7 也能凑合,但yarn模式下会有一堆隐性问题。直接用成熟的主流版本组合,别追新。
5.2 伪分布式 Hadoop 的核心配置要点
Hadoop 伪分布式搭建的核心就三件事:SSH 免密登录、core-site.xml和hdfs-site.xml配置、NameNode 格式化。
/etc/hadoop/core-site.xml 里最关键的一项:
<property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property>hdfs-site.xml 里把副本数设成 1(默认 3,但单机只有 1 个 DataNode)、块大小可以保持默认。
启动流程我建议严格按顺序走:
# 1. 格式化 NameNode(只在第一次执行) hdfs namenode -format # 2. 启动 HDFS start-dfs.sh # 3. 验证进程 jpsjps输出里应该看到NameNode、DataNode、SecondaryNameNode三个进程,缺哪个就去查对应日志。这里很容易遇到一个问题:格式化之后启动,DataNode 一直起不来。八成是dfs.namenode.name.dir路径下的历史数据和新集群的 clusterID 冲突,把hdfs-site.xml指定的目录删掉重新格式化就行。
5.3 Spark 提交任务与目录规范
Spark 跑起来之后,建议统一用spark-submit提交,而不是在spark-shell里写完就关掉。你自己写好的 jar 或者 Python 脚本打包后提交:
spark-submit \ --master local[*] \ --class com.example.JDPriceAnalysis \ /opt/jd-analysis.jar在 HDFS 的目录规范上,我吃过一个亏:一开始随便建了几个目录拼路径,后来清洗逻辑重写,发现历史目录又乱又难追溯。后来我强制自己按/warehouse/数据源/分层/业务主题来建目录,虽然初期麻烦,但后期调任何一段离线任务都很快定位。
5.4 串起全链路的调度方式
数据采集和 Spark 计算之间,一定要有个自动衔接的方式,而不是手动一个个跑脚本。小项目最简单的方案是crontab:
# 每天凌晨 2 点采集 0 2 * * * /usr/bin/python3 /opt/jd_project/scripts/crawl.py # 凌晨 4 点同步到 HDFS 并启动 Spark 计算 0 4 * * * /opt/jd_project/scripts/sync_and_compute.sh为什么不把采集和计算放在一个脚本里?因为采集经常因为晚高峰网络问题导致时长不可控,分开调度可以让数据采集失败时不连累已经完成的清洗步骤。等以后规模大了,再上 Airflow 或者 DolphinScheduler。
6. 在实际部署里踩过的几个深度坑
6.1 无头模式的"隐形"问题:元素可见但点击无效
Selenium 无头模式最诡异的一个问题:页面元素明明存在,find_element也能找到,但click()或send_keys()就是没反应。试了几种等待方式都不行,怀疑是无头模式下浏览器窗口的视图尺寸导致元素不可交互。
解决方法是先把窗口设置成足够大的尺寸(比如 1920x1080),再调用execute_script("arguments[0].scrollIntoView(true);", element)然后强制点击:
driver.execute_script("arguments[0].click();", element)以后再遇到无头模式元素"看不见"的问题,优先怀疑视口尺寸和滚动位置,这往往是坐标相交计算导致的交互失败。
6.2 Spark 读取 CSV 时的隐式 Schema 推断问题
Spark 读 CSV 会做自动类型推断,但真实数据的混合类型经常让推断结果失真。比如"评论数"这一列,95% 是数字,5% 是"10万+",Spark 可能把整列推断成string,后续聚合全错。
所以读 CSV 时我强烈建议显式指定 Schema,宁可长一点,也不要让 Spark 猜:
from pyspark.sql.types import StructType, StringType, LongType, DecimalType schema = StructType([ StructField("item_id", StringType(), True), StructField("title", StringType(), True), StructField("price", DecimalType(10, 2), True), StructField("comments", LongType(), True), ])显式 Schema 还有一个好处:数据解析阶段直接过滤掉脏数据,比到下游再清洗效率高得多。
6.3 MySQL 回写时的连接数爆炸
Spark 结果要用foreachPartition写 MySQL,但很多人的第一版是每条数据execute一次。百万行结果不出意外会把 MySQL 连接池打爆,报Too many connections错误。
正确姿势是在每个 partition 内只创建一次连接,批量执行 INSERT:
def write_partition(rows): conn = get_connection() cursor = conn.cursor() sql = "INSERT INTO ads_category_stats VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE ..." cursor.executemany(sql, [tuple(r) for r in rows]) conn.commit() cursor.close() conn.close() result.foreachPartition(write_partition)6.4 Django 静态文件在部署环境里 404
Django 开发环境跑得好好的,一上生产或者用 Nginx 部署,图片和 JS 全 404,这是DEBUG=False后静态文件服务路径不一致导致的。处理方法是按 Django 3.x+ 的标准做法,把静态文件统一收集到指定目录:
python manage.py collectstatic然后在 Nginx 配置里把/static/的请求指到对应的 static 根目录。这个坑几乎必踩,区别只是早晚。我的建议是项目从一开始就按生产模式配置静态文件路径,不要依赖 DEBUG 自带的静态服务。
6.5 爬虫风控进阶:从随机延迟到指纹对抗
如果只是课程设计,随机延迟就够了:
time.sleep(random.uniform(1.5, 3.5))但如果你发现爬了一会儿就开始要滑块验证,除了 IP 问题,更可能是浏览器指纹被采集了。Selenium 虽然顶着一个真实浏览器,但navigator.webdriver属性、Canvas 指纹、时区语言这些特征都会暴露自动化痕迹。网上有很多现成的 JavaScript 注入方案,可以在addScriptToEvaluateOnNewDocument阶段抹掉这些特征。
但我要泼一盆冷水:反爬对抗是军备竞赛,不要追求彻底绕过。在这个项目里,做好频率控制、数据完整性校验,才是可持续发展的思路。
7. 写在最后的扩展方向
如果你把这个平台跑通了,后续可以朝几个方向继续延伸。第一个是接入定时任务框架,用 Airflow 或 DolphinScheduler 把采集、清洗、计算、展示全部编排起来,这就和真实数仓平台的调度架构非常接近了。第二个是把 MySQL 换成 ClickHouse,分析性能会有一个数量级的提升,代码改动不大但能让你直观感受到 OLAP 和 OLTP 的区别。第三个是增加多平台数据源,比如把淘宝、拼多多都接入同一套清洗流水线,把你的数据仓库真正变成多源打通的中枢。
另外可以认真考虑一下异步采集。Selenium 慢是硬伤,但调研发现,京东商品详情页的数据很多其实可以通过接口拿到,你完全可以先用抓包定位这些接口,再用 requests + Selenium 降级兜底的方式提升整体吞吐。这样既保留了这个项目的可视化链路,又给采集层留了升级空间。
我在多次复现这类项目后的总感受是:技术栈多不等于工程复杂,真正难的是每一层之间的衔接细节。爬虫层要考虑稳定性,存储层要考虑编码和字段规范,计算层要考虑 Schema 和资源限制,展示层要考虑接口设计。只要一条链路里的每个环节你都踩过坑、知道为什么这么做,这个项目就真正属于你了。