基于Selenium+Hadoop+Spark的京东电商数据采集与分析可视化平台
1. 项目的真实分量它到底解决了什么问题先说个我常遇到的场景每隔一阵子就有学弟或者转行的朋友来问我想找一个既能写在简历上、又能真正跑通全流程的 Python 项目。问的人多了我发现大家的需求出奇一致——不想再要那种爬个静态页面存 CSV的玩具但又怕一上来就搞高并发分布式把自己劝退。而这个标题里的项目正好卡在了一个非常舒服的位置单机可跑、技术栈全、有可视化闭环。拆开看这个平台做的事情其实是一条完整的数据链路用selenium采集京东商品信息把数据落到 MySQL再通过Hadoop和Spark做分布式环境下的清洗与分析最终交给Django做可视化页面展示。听起来环节很多但每一环都是当前企业级数据项目里最常见的技术组件而不是孤立的技术名词堆砌。这个项目最适合三类人正在做毕业设计或课程设计的学生、想从前端或纯 Python 脚本转向大数据方向的人、以及想在公司内部搭建一个电商数据采集分析看板但不想从零造轮子的开发。尤其是那些对大数据生态只停留在听过名字阶段的读者跟着这个项目把 Hadoop 和 Spark 真实跑起来比看一百篇集群搭建教程都管用。我自己的体会是这个项目的精髓不在于某个环节多高深而在于它逼着你把爬虫、存储、计算、展示这四件事串起来。很多人在学校只写过 Jupyter 里的 Pandas没见过数据从采集到出图表全流程是什么样这个项目恰好补上了这个断层。2. 为什么采集层选 Selenium 而不是 Requests2.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(--headlessnew) opts.add_argument(--no-sandbox) opts.add_argument(--disable-gpu) opts.add_argument(--window-size1920,1080) opts.add_argument( user-agentMozilla/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(optionsopts)其中excludeSwitches和useAutomationExtension是关键它们用来去掉 WebDriver 的自动化标记。当然这只能应对初级的检测真正被风控盯上的时候还得配合代理池和随机延迟这点后面单独说。有一点必须提醒--headlessnew是新版 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_scroll10): 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.memory2g和spark.driver.memory2g对百万级数据量足够再大就考虑调大 executor 核数或放到真集群上去。如果你在本地跑频繁报 OOM优先检查是不是并行度设置得太高反而导致内存碎片化。4. Django 可视化层的选型与接口设计4.1 Django 在这里的角色后端服务而非全栈框架很多 Python 初学者以为 Django 可视化就等于 Django 模板语法渲染图表这个理解偏了。在这个项目里Django 的核心角色是提供数据接口服务——从 MySQL 读取 ADS 层的聚合结果返回 JSON 给前端由前端图表库负责绘制。这背后是两个模块的职责分工Django 负责数据 API、用户管理、权限控制前端用 ECharts 负责图形。对比 FlaskDjango 更适合这个项目的原因很简单它自带 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_length64, verbose_name商品类目) price_avg models.DecimalField(max_digits10, decimal_places2) price_p50 models.DecimalField(max_digits10, decimal_places2) item_count models.IntegerField() comment_avg models.DecimalField(max_digits12, decimal_places2) 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_datetoday) 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 环境清单与版本配对这一节我直接给出一份实测可行的版本组合按这个走能省很多跨版本兼容的坑组件推荐版本关键备注Python3.9.x3.10 以上部分依赖需额外处理JDK1.8Hadoop 3.x 官方支持 8Hadoop3.3.6单机伪分布式模式即可Spark3.3.x对应 Hadoop 3编译版本要与 Hadoop 匹配MySQL8.0注意字符集选 utf8mb4Django4.2.x长期支持版本Selenium4.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 namefs.defaultFS/name valuehdfs://localhost:9000/value /propertyhdfs-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 内只创建一次连接批量执行 INSERTdef 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 静态文件在部署环境里 404Django 开发环境跑得好好的一上生产或者用 Nginx 部署图片和 JS 全 404这是DEBUGFalse后静态文件服务路径不一致导致的。处理方法是按 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 和资源限制展示层要考虑接口设计。只要一条链路里的每个环节你都踩过坑、知道为什么这么做这个项目就真正属于你了。