京东手机电商数据分析系统:基于Scrapy与可视化大屏的实战解析
2026/9/15 22:28:29 网站建设 项目流程

每年到毕业设计季,都会有不少同学来问我“大数据方向的题目到底怎么做才能不烂大街”。今年被问得最多的,就是这个“京东商城手机产品电商数据分析系统”。老实说,这类题目的关键词拆开看都不难——大屏可视化、scrapy、大数据、数据分析,但真正要把整条链路跑通,从爬虫采集到数据清洗,再到存储建模、接口开发和可视化大屏,中间有不少隐藏的坑。

这篇文章就把我当时做这个项目的完整思路和落地过程整理出来。你拿到的输入可能只有“京东商城手机产品电商数据分析系统设计与实现,scrapy爬虫可视化”这么一行字,但千万别把它当成一个简单的爬虫Demo。它的核心价值在于:用手机商品数据做载体,跑通一条“数据采集—数据清洗—数据存储—指标计算—可视化展示”的大数据流水线。适合用来做毕业设计、课程项目,或者作为你接触大数据项目的第一块跳板。

1. 系统总体设计:从数据流角度拆解组件与链路

1.1 先别急着写爬虫,把“数据流”画出来

很多同学拿到这个题目,第一反应是写一个scrapy爬虫,把手机商品的标题、价格、评论数抓下来,然后丢给ECharts画几个图就完事。这样做的结果就是——答辩的时候老师问“你的大数据体现在哪”,你答不上来。

我的建议是,在设计阶段就把整个系统当成一个迷你数据中台来做。数据生命周期大概是这样的:

数据源(京东手机频道)→ 爬虫采集层(scrapy)→ 消息与去重缓冲层(Redis)→ 存储层(MySQL)→ 数据清洗与特征加工层(Pandas/脚本)→ 指标聚合层(SQL/API)→ 可视化展示层(ECharts大屏)

每一层之间用明确的数据接口解耦。爬虫只负责产出原始数据,清洗模块只负责读原始表、写干净表,后端API只负责从聚合表里取数,前端大屏只负责调API渲染。层与层之间不互相入侵,这样后期不管是换数据源,还是加图表,都只要改局部。

1.2 技术栈选型对比:为什么是scrapy而不是requests

先看一张技术选型对比表,这是我做选型时候的核心依据:

模块可选方案我选用的方案原因
数据采集requests + BeautifulSoup / scrapy / playwrightscrapy为主,playwright辅助需要分布式扩展、并发控制、去重、断点续爬,scrapy框架自带机制最完善
动态渲染selenium / playwright / 直接分析接口优先直接请求JSON接口,动态页面用playwright兜底接口直采效率高,不容易被JS渲染拖慢速度
数据缓存与去重MySQL临时表 / RedisRedis去重和请求队列需要高吞吐低延迟,Redis天然适合
数据存储MySQL / MongoDB / HiveMySQL数据量在百万级以内,MySQL足够,且方便做统计SQL
后端可视化接口Flask / FastAPI / SpringBootFlask轻量,和大屏前端联调快,也能直接读MySQL
可视化ECharts / Highcharts / TableauECharts可视化大屏生态成熟,组件多,社区案例丰富

这里重点说一下为什么不是requests。requests写小爬虫确实简洁,但你要自己处理并发、重试、URL去重、请求调度、日志管理,代码会越写越臃肿。scrapy把这些都抽象成了框架机制:Scrapy Engine控制数据流,Scheduler管理请求队列,Downloader处理下载,Item Pipeline处理数据,扩展点非常清晰。如果要做分布式爬虫,也只需要把Scheduler和Dupefilter替换成基于Redis的实现,requests要做到这一步需要大量造轮子。

1.3 大数据集群部署策略可以先放一放,但要预留扩展

这个项目标题带“大数据”,很多人会纠结要不要上Hadoop、Spark集群。我的经验是:如果你只是为了处理几万条手机商品数据,硬上HDFS和Spark是过度设计,反而增加部署复杂度。但可以在系统架构说明里预留扩展点,比如把清洗后的数据导出为Hive表结构,方便后续做更复杂的离线分析。

真正的商业级大数据链路一般是:

采集端(分布式爬虫)→ 消息队列(Kafka/RocketMQ)→ 计算引擎(Spark/Flink)→ 数据仓库(Hive/Iceberg)→ 可视化

这个项目可以设计成“轻量版大数据管线”,用Redis模拟消息队列,用Pandas模拟离线计算,用MySQL模拟数据仓库。答辩时你能说清楚“如果数据量增长,哪一层可以换成什么组件”,比硬装一个假集群要加分得多。

2. Scrapy采集模块的工程化实现:页面解析、翻页与动态内容

2.1 商品字段定义:爬之前先想清楚分析需要什么

爬虫不是“看到什么抓什么”,而是“分析需要什么才抓什么”。我做需求分析时,先列了可视化大屏需要展示的指标,再反推字段:

  • 商品标题:需要从中提取品牌、型号、卖点
  • 价格:分析价格区间分布、各品牌均价
  • 评论数:衡量商品热度
  • 店铺类型(自营/第三方):判断平台生态结构
  • 商品链接:作为唯一标识,方便去重
  • 品牌:从标题中拆出来,做品牌份额分析
  • 上架时间/促销信息:辅助判断产品迭代节奏

对应的items.py设计大概长这样:

import scrapy class PhoneProductItem(scrapy.Item): product_id = scrapy.Field() # 商品ID,从URL或页面中提取 title = scrapy.Field() # 商品标题 brand = scrapy.Field() # 品牌,后面清洗时统一 price = scrapy.Field() # 当前价格 original_price = scrapy.Field() # 划线价/原价 comment_count = scrapy.Field() # 评论数 shop_name = scrapy.Field() # 店铺名称 shop_type = scrapy.Field() # 自营 or 第三方 detail_url = scrapy.Field() # 商品详情页链接 crawl_time = scrapy.Field() # 抓取时间

字段设计要注意一点:不要只存“展示值”。比如价格在页面上显示成“¥3999.00”,你要么存数值类型,要么在管道里转成浮点数。评论数可能带“万”后缀,比如“5.6万”,清洗时也得统一处理。这些脏数据问题如果不在字段设计阶段提前考虑,后面清洗会非常痛苦。

2.2 页面解析与翻页策略:先找到列表页的数据规律

手机频道列表页通常是一个商品一个商品块,商品标题、价格、评论数都在列表页可以直接拿到。用scrapy的Selector解析时,建议先用Scrapy Shell调试,避免反复启动爬虫试错。在PyCharm里跑scrapy shell也是常见的效率操作:

scrapy shell "https://channel.jd.com/手机-XXXXXXXX.html"

进去之后用response.css()或者response.xpath()逐步定位商品块。我一般优先用CSS选择器,语法更简洁;遇到嵌套结构再用XPath。列表页翻页参数通常是pages,第一页和第二页的URL规律比较明显,构造URL列表然后循环请求就行。

但要注意:列表页的某些商品价格可能是通过AJAX异步加载的,列表页初始HTML里只能拿到“暂无报价”。这时候有两个方案:一是进详情页取价格,二是直接分析页面里的JavaScript接口。优先选第二个,因为详情页的访问成本更高,还更容易触发限制。

2.3 并发与限速的平衡:爬虫并发设计到底哪个好

这是PyCharm里写scrapy教程时最容易忽略的问题。很多新手喜欢一上来就把CONCURRENT_REQUESTS调到32甚至64,结果没跑几分钟就被限制访问,整个IP被暂时封掉。

我当时在settings.py里的配置是:

# 并发请求数,一开始别太高 CONCURRENT_REQUESTS = 8 # 同一域名下的并发请求数 CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 下载延迟,动态调整,默认0.5秒 DOWNLOAD_DELAY = 0.5 # 启用自动限速扩展 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 1.0 AUTOTHROTTLE_MAX_DELAY = 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0

AUTOTHROTTLE_ENABLED这个开关很关键,它会根据目标服务器的响应时间动态调整延迟,服务器响应变慢时自动拉长间隔,响应快时适当提速。这比“固定延迟”要智能得多。但注意,自动限速和DOWNLOAD_DELAY同时配置时会互相影响,新手容易懵,我的做法是:调试期固定延迟,稳定跑批时开自动限速。

并发到底调多大合适?我的经验是:看单次请求的平均耗时和单页数据量。如果平均响应1秒,8个并发理论上每秒可以处理8个请求;但目标服务器往往不希望你每秒打这么多。所以更合理的指标是“每分钟请求数”,控制在20~30个以内比较稳妥。你要做的是在采集速度和目标站点负载之间找一个平衡,而不是追求单机极限。

2.4 动态页面与iframe数据:scrapy如何配合playwright

京东商城的部分信息是动态加载的,有些字段藏在iframe里,或者通过JavaScript异步请求才能拿到。这类需求单靠scrapy原生请求搞不定。

两种常见解法:

  • scrapy-playwright中间件,在scrapy里直接驱动浏览器渲染
  • 用playwright单独写采集脚本,把数据写入数据库,再走scrapy做增量

scrapy-playwright的集成方式不算复杂,在settings.py里开启:

DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } PLAYWRIGHT_LAUNCH_OPTIONS = { "headless": True, }

然后在Request里加meta={"playwright": True},这个请求就会走浏览器渲染。但这里有个性能陷阱:浏览器渲染比普通HTTP请求慢十倍以上,所以只有普通请求拿不到数据时才用playwright,不要全站开启。

遇到iframe的话,还要先切换到iframe上下文里取内容。playwright提供了frame_locator来处理,能精确定位到内部元素,比Selenium的switch_to_frame体验好很多。

2.5 数据与URL去重:Redis在爬虫里的实际角色

scrapy自带的去重是基于内存的RFPDupeFilter,单机跑没问题,但爬虫中断重启后,已经爬过的URL会重复请求,非常浪费。我的做法是把去重集合放到Redis里,用商品ID或URL的哈希作为集合成员。

Redis在爬虫里的作用不只是去重,还可以做:

  • 待爬取URL队列:多个爬虫进程同时从Redis里取URL,自然实现分布式爬虫
  • 爬虫状态记录:记录已经爬了多少条、失败了多少条
  • 中间数据缓存:清洗模块和采集模块解耦,爬虫写完Redis,清洗脚本再从Redis拉

实现Redis去重可以在dupefilter里替换,也可以自己在Pipeline里判重。基础版本就是在Pipeline里用SISMEMBER判断:

import hashlib import redis r = redis.Redis(host="localhost", port=6379, db=0) def generate_fingerprint(url): return hashlib.md5(url.encode("utf-8")).hexdigest() def is_duplicate(url): fp = generate_fingerprint(url) if r.sismember("phone_urls_done", fp): return True r.sadd("phone_urls_done", fp) return False

这段逻辑在单机版里看起来有点“杀鸡用牛刀”,但如果后期要扩展到分布式爬虫,这一层就是现成的。用Redis可视化客户端看一眼集合成员数量,比写一堆日志直观得多。

3. 数据清洗与存储建模:从原始HTML文本到可用数据仓库

3.1 爬下来的数据到底有多脏

爬虫Pipeline里拿到的数据,不是说直接INSERT进MySQL就完事了。真正常见的脏数据包括:

  • 品牌字段不统一:“Apple 苹果”、“苹果”、“Apple iPhone”都有,需要映射成统一品牌名
  • 评论数带单位:“5.6万”需要转成56000
  • 价格为“暂无报价”或者空字符串,需要过滤或置空
  • 标题里带有一堆促销标签:“【直降300】Apple iPhone 15 (A3092) 128GB 黑色支持移动联通电信5G 双卡双待手机”,品牌和型号要单独拆
  • 重复商品:同一个商品在不同的列表页出现多次,需要通过product_id去重

这一块最能体现数据分析系统的价值。如果数据质量不行,后面画出来的大屏就是“垃圾进,垃圾出”,指标全是错的。

3.2 用Pandas做清洗流程:规则要可复现

我习惯把清洗逻辑单独写成一个Python脚本,输入是爬虫产出的原始表,输出是清洗后的干净表。这样做的好处是,爬虫抓完数据后可以反复跑清洗,不用重新爬。

核心步骤代码大概这样:

import pandas as pd import re df = pd.read_csv("raw_phones.csv", dtype=str) # 1. 统一品牌 brand_map = { "Apple": "苹果", "苹果": "苹果", "华为": "华为", "HUAWEI": "华为", "小米": "小米", "Xiaomi": "小米", "OPPO": "OPPO", "vivo": "vivo", "荣耀": "荣耀", "Honor": "荣耀" } def parse_brand(title): for key, val in brand_map.items(): if key.lower() in title.lower(): return val return "其他" df["brand"] = df["title"].apply(parse_brand) # 2. 评论数转换:"5.6万" -> 56000 def parse_comment_count(text): if not text or text == "暂无评论": return 0 text = text.replace("+", "").strip() if "万" in text: return int(float(text.replace("万", "")) * 10000) if "亿" in text: return int(float(text.replace("亿", "")) * 100000000) return int(text) df["comment_count"] = df["comment_count"].apply(parse_comment_count) # 3. 价格清洗 df["price"] = pd.to_numeric(df["price"], errors="coerce") df = df.dropna(subset=["price"]) # 4. 删除重复项 df = df.drop_duplicates(subset=["product_id"], keep="last")

注意顺序很重要。先解析品牌,再做评论数转换,再做价格过滤,最后去重。因为去重要保留最新数据,所以用keep="last",保证重复商品记录的是最后一次抓到的价格。

3.3 MySQL表结构设计:维度表和事实表分开

表结构设计上,我参考了一点数据仓库的思路:商品维度和价格事实分开,因为同一款手机在不同时间点的价格不一样,分析“价格走势”时需要事实表的支持。

商品维度表:

CREATE TABLE dim_phone ( product_id VARCHAR(64) PRIMARY KEY, title VARCHAR(512), brand VARCHAR(64), model VARCHAR(128), shop_name VARCHAR(128), shop_type VARCHAR(32), detail_url VARCHAR(512) );

价格与评论事实表:

CREATE TABLE fact_phone_daily ( id INT AUTO_INCREMENT PRIMARY KEY, product_id VARCHAR(64), price DECIMAL(10,2), original_price DECIMAL(10,2), comment_count INT, crawl_time DATETIME, KEY idx_product_id (product_id), KEY idx_crawl_time (crawl_time) );

这种设计的好处是:分析“当前市场均价”时,取每个product_id最新一条的crawl_time数据;分析“价格随时间变化”时,直接查事实表。和直接堆一张大宽表相比,数据冗余更少,统计速度也更快。

3.4 用SQL做指标聚合:可视化接口的数据基础

大屏上的每一个数字,后面对应一条聚合SQL。比如:

  • 总商品数:SELECT COUNT(*) FROM dim_phone;
  • 价格带分布:SELECT ROUND(price/1000)*1000 AS price_bucket, COUNT(*) FROM fact_phone_daily WHERE crawl_time=(SELECT MAX(crawl_time) FROM fact_phone_daily) GROUP BY price_bucket;
  • 品牌分布:SELECT brand, COUNT(*) FROM dim_phone GROUP BY brand ORDER BY count DESC;

这个阶段建议把“当前数据”和“历史数据”分开。大屏默认展示的是“最近一次全量采集”的快照,历史趋势再看事实表。如果每次大屏刷新都实时跑全量聚合,MySQL压力会很大。

4. 可视化大屏设计:指标体系和图表怎么对应

4.1 先定指标再画图:大屏不是图表的堆砌

可视化大屏最容易踩的坑,是为了填满屏幕而堆图表。真正有价值的是“用图表回答业务问题”。我这个项目围绕手机电商数据的核心分析场景,定了四类指标:

分析维度业务问题可视化形式
市场总览监测了多少款手机?总评论量多少?顶部KPI数字卡
品牌竞争哪个品牌上架产品最多?平均价格谁高谁低?柱状图 + 条形图
价格结构手机价格集中在哪个区间?价格带分布柱状图/饼图
商品热度哪些机型评论数最多?Top10商品列表 + 横向柱状图

如果你还抓了商品好评率、店铺类型,可以再加一个“自营vs第三方占比”的环形图。不要超过六七个核心图表,否则大屏看起来热闹,信息密度反而下降。

4.2 大屏布局与适配:参考真实大屏项目的做法

大屏布局我采用的是经典“总—分”结构:顶部一行放总览KPI,左中右三列分别放品牌分布、价格分布、热度榜单。中间C位放一个核心图表,比如品牌平均价格对比,或者商品评论数Top15。

适配是可视化大屏最烦的问题之一。你在大屏上调试好的布局,换一台分辨率不同的电脑就可能错位。常见做法有三种:

  1. 使用vw/vh单位做布局,元素大小跟视口走
  2. transform: scale()做整体缩放,根据设计稿宽度动态计算缩放比例
  3. 用ECharts自带的ResizeObserver监听容器尺寸变化

我实际用的是第二种,也是大屏项目里最稳的方案。设计稿按1920x1080来做,然后:

const scaleX = window.innerWidth / 1920; const scaleY = window.innerHeight / 1080; document.querySelector('#screen').style.transform = `scale(${scaleX}, ${scaleY})`;

这种方式能保证布局比例不变,但不是自适应,而是等比缩放。缺点是屏幕比例不一致时会出现留白,不过大屏场景下可以接受。

4.3 ECharts图表初始化与数据接入

以品牌分布柱状图为例,图表初始化和数据加载的核心逻辑是:

const chart = echarts.init(document.getElementById('brandChart')); fetch('/api/brand_distribution') .then(res => res.json()) .then(data => { chart.setOption({ tooltip: {}, xAxis: { type: 'category', data: data.brands }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.counts, barWidth: 20, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#00c6ff' }, { offset: 1, color: '#0072ff' } ]) } }] }); });

这里要说一个经验:接口返回的字段名和图表数据字段名最好在设计时统一,比如后端返回counts,前端就直接用counts。不要在接口层做字段名转换,前后端不要各改一遍,否则联调的时候很容易踩坑。

4.4 大屏自动刷新与数据缓存策略

电商数据是时效性数据,大屏不能只展示一次。我用了两个定时器:前端每5分钟刷新一次数据,后端接口层每10分钟查一次数据库并缓存结果。后端缓存可以用Redis,也可以直接在Flask里加functools.lru_cache

简单版本:

import time from functools import lru_cache @lru_cache(maxsize=32) def get_brand_distribution_cache(): # 查询MySQL return result def get_brand_distribution(): # 10分钟缓存过期 cache_key = int(time.time() / 600) return get_brand_distribution_cache(cache_key)

这种方式能显著降低MySQL的查询压力,因为大屏页面即使有几十个人同时看,后端也只会每10分钟真正查一次库。

5. 部署落地与性能调优:从PyCharm跑通到服务器稳定运行

5.1 部署结构:一台服务器怎么装下所有服务

很多同学的代码在PyCharm里能跑通,一部署到Linux服务器就各种问题。这个项目的部署其实不复杂,单台服务器就能搞定。我当时的部署结构是:

  • Python 3.10 + scrapy爬虫脚本(用crontab定时触发)
  • Redis 6.x(去重和缓存)
  • MySQL 8.x(数据存储)
  • Flask + Gunicorn(后端API服务)
  • Nginx(静态大屏托管 + API反向代理)

如果你的服务器内存只有2G,建议把冗余服务做减法:Redis可以不开AOF持久化,MySQL只保留一个实例,Flask用2个worker就够了。别一上来就部署什么大数据集群,先把单机版跑稳。

5.2 并发参数调整:爬虫、数据库、后端API之间的资源博弈

在服务器上部署之后,会遇到一个本地开发没有的问题:资源竞争。爬虫跑批量采集时,CPU和带宽占用很高,导致后端API响应变慢,大屏加载卡顿。解决思路是分时段:

  • 凌晨2点到6点:跑全量爬虫和清洗任务
  • 白天:只做增量更新和API查询

如果一定要同时跑,就在scrapy的settings里把并发调低,数据库连接池调大一点,给后端API留资源。爬虫这块的实际调参经验是:单核服务器上,CONCURRENT_REQUESTS设为4~6,DOWNLOAD_DELAY设为1秒左右,基本不会对同机部署的API造成太大影响。

5.3 定时调度与增量更新:别让大屏变成一次性展示

大屏数据需要持续更新,定时任务就很重要。我用了两种方式:

一是Linux的crontab定时触发爬虫和清洗脚本:

# 每天凌晨2点整执行全量爬取 0 2 * * * cd /opt/phone-analysis && /usr/bin/python3 scripts/run_spider.py >> logs/spider.log 2>&1 # 每天凌晨3点30分执行数据清洗 30 3 * * * cd /opt/phone-analysis && /usr/bin/python3 scripts/clean_data.py >> logs/clean.log 2>&1

二是在爬虫脚本里使用start_urls动态拼接页码,实现增量抓取。手机商品的更新频率不像新闻那么快,每天跑一次全量列表页即可。如果考虑更细,可以重点抓评论数变化较大的商品,只更新热点商品的价格。

5.4 我实际踩过的几个典型问题

第一个是中文乱码。scrapy默认的响应编码判断有时候不准,导致抓下来的中文变成乱码。解决方式是手动指定:

response = requests.get(url) response.encoding = 'utf-8'

如果还有乱码,再用response.text.encode('latin1').decode('utf-8')这种“编码修复大法”。

第二个是502和连接超时。这是目标服务器对单IP访问频率的限制。我当时加了重试机制和代理池,但代理池如果来自免费渠道,稳定性很差,反而增加脏数据。我的方案是老老实实降低抓取速度,只保留少量高可用代理,宁可慢一点,也不要三天两头出问题。

第三个是Redis数据堆积。如果爬虫崩溃了,Redis队列里会堆积大量未处理的URL,重启后直接开爬会重复消耗请求资源。我加了一个启动前检查,如果待爬队列数量超过阈值,先清空队列再重建任务。这个逻辑不复杂,但能省下不少麻烦。

第四个是可视化大屏适配问题。前面说了用transform: scale(),但要注意ECharts在缩放后,点击事件坐标会偏移。解决办法是监听window.resize事件,重新调用chart.resize()

5.5 这套系统可以继续延伸的方向

做完这个项目之后,我的体会是:它真正锻炼的不是爬虫技术,而是“如何用数据产品思维组织一个完整系统”。如果后续要扩展,可以往两个方向走。

一是增加评论情感分析。在商品详情页抓取评论内容,用情感分析模型给每条评论打上情感标签,然后统计各品牌的好评率、差评率。这样大屏上就多了一个“口碑分析”模块,分析深度一下子提升不少。

二是做多平台数据对比。同样的手机型号,抓取多个电商平台的价格数据,做价格雷达图或比价折线图。这就不只是展示“京东这一个平台是什么样”,而是真正的电商数据分析系统了。爬虫的设计也可以升级成“多源爬虫”,把不同平台的采集逻辑抽象成插件,通过配置驱动新增平台,不需要改动核心代码。

最后再分享一个小技巧:不管你是做毕设还是公司项目,一定要把每一步的中间结果都留下痕迹。爬虫日志、清洗前后的数据量对比、接口响应时间、大屏截图,这些是你复盘和答辩时最有力的素材。技术方案可能会被质疑,但“真实数据的完整处理过程”不会骗人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询