资讯动态

京东手机电商数据分析系统:基于Scrapy与可视化大屏的实战解析

发布时间:2026/9/15 22:28:50 来源:尧图企业网站定制
每年到毕业设计季都会有不少同学来问我“大数据方向的题目到底怎么做才能不烂大街”。今年被问得最多的就是这个“京东商城手机产品电商数据分析系统”。老实说这类题目的关键词拆开看都不难——大屏可视化、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。列表页翻页参数通常是page或s第一页和第二页的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.0AUTOTHROTTLE_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(hostlocalhost, port6379, db0) 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, dtypestr) # 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], errorscoerce) df df.dropna(subset[price]) # 4. 删除重复项 df df.drop_duplicates(subset[product_id], keeplast)注意顺序很重要。先解析品牌再做评论数转换再做价格过滤最后去重。因为去重要保留最新数据所以用keeplast保证重复商品记录的是最后一次抓到的价格。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。适配是可视化大屏最烦的问题之一。你在大屏上调试好的布局换一台分辨率不同的电脑就可能错位。常见做法有三种使用vw/vh单位做布局元素大小跟视口走用transform: scale()做整体缩放根据设计稿宽度动态计算缩放比例用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(maxsize32) 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~6DOWNLOAD_DELAY设为1秒左右基本不会对同机部署的API造成太大影响。5.3 定时调度与增量更新别让大屏变成一次性展示大屏数据需要持续更新定时任务就很重要。我用了两种方式一是Linux的crontab定时触发爬虫和清洗脚本# 每天凌晨2点整执行全量爬取 0 2 * * * cd /opt/phone-analysis /usr/bin/python3 scripts/run_spider.py logs/spider.log 21 # 每天凌晨3点30分执行数据清洗 30 3 * * * cd /opt/phone-analysis /usr/bin/python3 scripts/clean_data.py logs/clean.log 21二是在爬虫脚本里使用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 这套系统可以继续延伸的方向做完这个项目之后我的体会是它真正锻炼的不是爬虫技术而是“如何用数据产品思维组织一个完整系统”。如果后续要扩展可以往两个方向走。一是增加评论情感分析。在商品详情页抓取评论内容用情感分析模型给每条评论打上情感标签然后统计各品牌的好评率、差评率。这样大屏上就多了一个“口碑分析”模块分析深度一下子提升不少。二是做多平台数据对比。同样的手机型号抓取多个电商平台的价格数据做价格雷达图或比价折线图。这就不只是展示“京东这一个平台是什么样”而是真正的电商数据分析系统了。爬虫的设计也可以升级成“多源爬虫”把不同平台的采集逻辑抽象成插件通过配置驱动新增平台不需要改动核心代码。最后再分享一个小技巧不管你是做毕设还是公司项目一定要把每一步的中间结果都留下痕迹。爬虫日志、清洗前后的数据量对比、接口响应时间、大屏截图这些是你复盘和答辩时最有力的素材。技术方案可能会被质疑但“真实数据的完整处理过程”不会骗人。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价