京东手机品类的数据,我盯了挺久。市面上的销量榜、价格分布、品牌份额,基本是平台或媒体爱怎么写怎么写,想拿到一份自己说了算的数,还得自己动手。所以就有了这个“基于Python的京东手机销售数据分析系统”:把京东手机类目的在售商品信息抓下来,清洗成可以分析的表格,再用图表把价格带、销量、品牌结构这些维度跑出来。整套东西不复杂,却是典型的一条龙数据分析练习,走完能摸清数据采集、清洗、分析、可视化的整套闭环。
这篇文章把项目完整拆开讲,适合刚学完Python语法、想做个实战项目的人,也适合已经有爬虫基础、想把数据做得像样一点的朋友。我不会只贴代码,更重要的是把每一步取舍和踩坑讲清楚——为什么这么抓数据、为什么这么洗字段、图标怎么选才不误导人,这些才是真正值钱的东西。
1. 项目核心设计与技术选型
1.1 先把需求拆清楚,别急着写代码
很多人在拿到这种项目时,第一反应就是打开IDE,装个requests库,开始对着网页“硬爬”。真正做之前,我建议老老实实把需求拆成几个可验证的产出物,否则做着做着就变成写了一堆没人看懂的脚本。
这个系统的核心诉求是什么?拆开来看是这三件事:
- 能拿到一定数量的京东手机商品记录,字段至少包括标题、价格、评价数/销量、店铺类型、品牌这些维度。
- 把原始数据清洗成“能直接喂给pandas做统计”的干净表格,尤其是价格和销量这种最容易出脏数据的字段。
- 输出几个关键结论:品牌市场份额、手机价格带分布、销量TOP商品、价格与销量的关系,用图表说话。
我没有设计复杂的用户登录、权限管理、定时任务,因为单机分析场景用不上。这也算一个经验:分析型项目最忌讳过度设计,能用CSV存数据就别急着上数据库,能用脚本跑就别上框架。我最终方案就是Python脚本 + CSV中间文件 + Jupyter做分析和可视化,简单直接,一个人半天就能跑通全流程。
1.2 技术栈怎么选:为什么是Python这一套
选技术栈时,我认真对比过几个方案:Node.js、Java、或者直接用Excel手工统计。最后依然选Python,理由很实际:
- 爬取部分,requests库足够轻量,配合lxml的XPath选择器,解析HTML效率高;就算遇到复杂页面,还能用playwright做无头浏览器兜底。
- 清洗分析部分,pandas几乎是这个场景下的最优解,处理字段类型、去重、分组聚合,三五行代码搞定,换成Java写要绕不少路。
- 可视化部分,一份图用matplotlib出静态图,一份用pyecharts出可交互HTML,覆盖正式汇报和日常快速看数两个场景。
热词里很多人关心“Python环境怎么配置”,这里提一句:Windows下装Python一定勾选“Add Python to PATH”,然后pip源换成国内的镜像源,不然装pandas那几十兆依赖能让你怀疑人生。编辑器方面,VSCode装好Python扩展后,直接在设置里指定解释器路径,F5调试脚本很顺。别在这个环节卡太久,环境和工具都是为跑通业务服务的。
1.3 数据体量预估和接口设计
京东手机类目大概有大几千个SPU(标准化产品单元),但我的分析重点是“消费者能直观看到的在售手机”,不是所有SKU。比如同款手机的不同颜色、不同内存版本,我更关心型号维度的销量和价格,所以数据量控制在千级以内,完全不需要分布式或队列。
接口设计上,我把整个系统拆成四个函数模块:
fetch_page(url):负责网络请求和异常重试。parse_page(html):负责从HTML解析出商品字段。clean_data(raw_df):负责数据清洗和标准化。analyze_and_plot(clean_df):负责统计分析加出图。
这样分层的最大好处是,某个环节字段变了,只改一个函数,不会牵一发而动全身。后面爬虫反爬也好、字段变更也好,我都是在对应模块里做小手术,整体没动过。
2. 京东手机销售数据从哪来:抓取设计与反爬对抗
2.1 页面数据通道与请求伪装
京东商品列表页的数据加载方式,本质上分为两种通道:服务端直出的HTML和前端异步接口的JSON。手机类目的列表页,主要商品信息还是服务端渲染的,所以直接分析HTML就能拿到。但这里有个关键坑:页面里的价格字段经常是动态加载的,直接出现在HTML上的价格可能为空或者显示“暂无报价”。
我的处理方式是:先请求一个基础列表页,把标题、链接、评价数、店铺归属这些静态字段拿到;然后再根据每个商品的链接,请求单独的详情接口拿真实价格。京东部分场景会把价格塞在一个内嵌的JSON变量里,形如window._JD_Price_ = ...,用正则提取比解析HTML更容易。
请求伪装的部分,核心是三件套: User-Agent、Cookie、Referer。UA用浏览器的真实UA,Referer指向京东首页,Cookie则是重中之重。京东对未登录状态的Cookie也能访问商品信息,但请求过于频繁后会弹出滑块验证。我在实际测试中的做法是:
- 固定一个浏览器参数组合,避免每次请求UA都变,反而更容易被识别。
- 设置请求间隔,随机在1到3秒之间,避免固定频率的“机器味”。
- 每次请求带上一个标准的浏览器头,包括Accept、Accept-Language、Connection这些。
2.2 反爬限制与备选数据方案:别硬刚
我实测下来,纯requests快速连续抓京东,大概在几十个请求之后就会开始出现异常:要么返回一个空壳页面,要么直接弹出验证码。这类平台的反爬策略属于“温和但坚定”的类型,不会立刻断你,而是慢慢让你拿不到干净数据。
这种情况下很多人会硬刚,去搞打码平台、搞IP池,我不建议这么干。一方面成本高,另一方面容易把账号搭进去。这个项目的目标训练数据分析能力,不是跟风控较劲。我的实际解决思路是:
- 第一次抓取时,把请求频率降下来,分几批跑,每跑完一批数据保存成CSV,即使中途被触发验证,也不至于全盘皆输。
- 被验证码拦截后,先停半小时,手动过一下验证码,恢复会话状态,再从断点继续。
- 爬不到的缺失数据,用公开的第三方电商历史数据或自行手动整理补充。比如一些评测网站会公开某段时间的手机销量榜单,这种数据拿来补充和分析,完全够用。
我后来甚至做了个“数据输入层”的抽象:把爬虫和批量文件导入都视为同一套数据源。也就是说,我的load_data()函数既支持读CSV文件,也支持走爬虫接口。这种方式特别适合想复现项目的朋友——你甚至不需要真去跑爬虫,只需要手头有一份类似的电商数据就能继续后续的分析。数据分析真正的核心永远在清洗和分析,数据来源的优先级反而是其次。
2.3 抓取代码实现:以最小可用为准
下面这段抓取代码,是我早期稳定版本的精简呈现,完整跑一轮大概能抓300到500条商品记录:
import requests import random import time import csv from lxml import html HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://www.jd.com/', } def fetch_page(url): for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text except Exception as e: print(f"请求失败,尝试第{attempt + 1}次:{e}") time.sleep(2) return None def parse_page(html_text): tree = html.fromstring(html_text) items = [] # 商品列表的公共容器选择器按实际页面结构调整,这里以经典版结构为例 for li in tree.xpath('//ul[contains(@class,"gl-warp")]/li'): title = ''.join(li.xpath('.//div[contains(@class,"p-name")]/a/em//text()')).strip() link = li.xpath('.//div[contains(@class,"p-img")]/a/@href') price = li.xpath('.//div[contains(@class,"p-price")]//i//text()') comment = li.xpath('.//div[contains(@class,"p-commit")]/a//text()') items.append({ 'title': title, 'link': 'https:' + link[0] if link else '', 'price': ''.join(price).strip(), 'comment': ''.join(comment).strip(), }) return items def save_csv(rows, filename='jd_phone_raw.csv'): with open(filename, 'a', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=['title', 'link', 'price', 'comment']) if f.tell() == 0: writer.writeheader() writer.writerows(rows) if __name__ == '__main__': base_url = 'https://search.jd.com/Search?keyword=手机&enc=utf-8&page={}' for page in range(1, 15): html_text = fetch_page(base_url.format(page * 2 - 1)) if html_text: items = parse_page(html_text) if items: save_csv(items) print(f"第{page}页保存{len(items)}条") # 随机延时,避免频率过高 time.sleep(random.uniform(1, 3))这里有个小细节我特意留着:page * 2 - 1。京东PC搜索页翻页规律是奇数页返回真实内容,偶数页多数是空壳或者重复内容,不处理这个会白白多请求一倍。这些经验短时间在网上找不到,都是实测试出来的。
3. 数据清洗与标准化:一部手机的两条命
3.1 脏数据从哪来:眼见不一定为实
爬下来的数据,远看像模像样,细看全是坑。原始数据大概是这个画风:
| 标题 | 价格 | 评价数 |
|---|---|---|
| 【热销】荣耀X50 8GB+128GB 燃橙色 5G手机 1.5K超清护眼硬核曲屏 5800mAh 超长续航 | 1599.00 | 200万+ |
| Apple iPhone 15 (A3092) 128GB 蓝色 支持移动联通电信5G 双卡双待 | ¥5399.00 | 50万+ |
| Redmi Note12 Pro 5G手机 12GB+256GB | 暂无报价 | 10万+ |
这种数据直接喂给pandas是没法用的。“价格”列混着纯数字、带“¥”的字符串、还有“暂无报价”,pandas会把它识别成字符串而不是数值类型,排序、求均值都会出大问题。“评价数”就更别提了,“200万+”是文本格式,不是数字200,不处理没法参与计算。
清洗的目的,就是把这些自然语言和半结构化文本,全部转成干净的数值字段。
3.2 价格和销量的清洗套路
清洗价格字段,我的思路是先把所有非数字字符剔除,字符串统一转float;空值和“暂无报价”统一记为NaN,后续分析时看占比决定是删除还是补0。
import pandas as pd import re def clean_price(price_str): if not isinstance(price_str, str): return price_str price_str = price_str.replace('¥', '').replace('¥', '').strip() if price_str == '暂无报价': return None # 有些价格区间显示为 1599.00-1699.00,取低价 price_str = price_str.split('-')[0] try: return float(price_str) except ValueError: return None df['price_clean'] = df['price'].apply(clean_price)销量文本转换成数字,这块我一开始吃了亏。刚开始用简单的正则提取数字,结果“2.3万+”提取出来是2.3,参与计算时把总量当成2.3,排名全错。正确做法是识别“万”这个单位并乘上10000:
def parse_comment(text): if not isinstance(text, str): return 0 num_search = re.search(r'([\d.]+)(万)?', text.replace('+', '')) if num_search: num = float(num_search.group(1)) if num_search.group(2) == '万': num = int(num * 10000) return num return 0 df['comment_num'] = df['comment'].apply(parse_comment)这里我单独提醒一句:处理完数值必须跑一次df.dtypes看字段类型,别只看数据长什么样。如果清洗后字段类型还是object,前面的操作很可能没生效。我经常遇到有人问我“为什么groupby出来的分组还是字符串排序”,一查就是类型没转好。
3.3 品牌与系列归类:别让“红米”和“小米”分家
分析手机市场,品牌维度是逃不开的。但原始标题里的品牌表述非常分裂:同一品牌可能有多种叫法,比如“小米”“Xiaomi”“Redmi”“红米”,还有“小米Redmi”这种组合出现在标题里。如果不做统一,饼图里会出现一堆碎片品牌,根本没法看。
我的做法是写一个关键字映射函数,按品牌别名优先级匹配,把标题统一映射到主品牌。尤其注意,华为和荣耀虽然曾经同属一个体系,但现在是独立品牌,做分析时必须分开。映射逻辑大致如下:
brand_map = [ ('苹果', 'Apple', 'iphone', 'iPhone'], ('华为', 'HUAWEI', '华为'], ('荣耀', 'HONOR', '荣耀'], ('小米', 'Xiaomi', 'Redmi', '红米'], ('OPPO', 'OPPO', 'realme'], ('vivo', 'VIVO', 'iQOO'], ('三星', 'SAMSUNG', 'Galaxy'], ('魅族', 'MEIZU'], ] def map_brand(title): for brand, aliases in brand_map: for alias in aliases: if alias.lower() in title.lower(): return brand return '其他'这个函数看起来很简单,但它解决了数据里最核心的一个“口径统一”问题。分析报告里的饼图,理应只出现用户认知中的主流品牌,而不是五花八门的别名。顺带一提,我在实际清洗中还加了一个“子品牌拆分”的字段,比如小米和Redmi、vivo和iQOO分开统计,这样能够看到子系列的差异,但这些都建立在主品牌映射完成的基础上。
3.4 去重和缺失值处理:宁缺毋滥
爬虫采集过程中,同一个商品会因为翻页列表和详情页关联而出现两条记录,或者同一手机的不同颜色也算成了不同记录。去重逻辑上,我选择以“标题规范化后的文本”作为主键,去掉空格和特殊符号,重复的保留第一条;如果是同一型号不同颜色,则在后续分析时按型号聚合,不影响总体结论。
缺失值的处理策略分两种:价格缺失的行,如果占比小于5%,直接删除;占比比较高的,可以考虑用该品牌该价格带的均值填充,但分析时要在报告里备注。评销量的缺失值则统一补0,表示没有评论数据,不参与均值计算。
4. 分析思路与可视化落地:数据到结论的最后一步
4.1 销量和价格的基本盘:先跑描述性统计再出图
清洗完之后,别急着画花哨的图,先跑一遍描述性统计,让自己心里有数:
print(df[['price_clean', 'comment_num']].describe())这一步非常重要,能快速暴露数据质量问题:如果价格最小值是0,大概率清洗时出了bug;如果销量最大值比其他值高出几个数量级,可能是“万+单位”解析没做好,把数值当成原始数字了。我实际跑下来,手机均价段集中在1200到6000之间,中位数比均值低2000左右,说明头部旗舰机拉高了整体均值,市场呈明显的长尾分布。
有了底数后,再开始出核心图表。我的基本组合是四张图:品牌销量饼图、价格带直方图、单品销量TOP20条形图、价格与销量的散点图。
4.2 四张核心图表怎么画最说明问题
画图这里,我也是两类工具都试过。matplotlib适合出静态报告,png直接插入文档,风格偏学术;pyecharts则适合自己反复拖拽看数据,交互体验好,特别适合展示给不懂技术的人看。
品牌销量的饼图,我后来换成了条形图。原因是手机品牌数量不多,饼图在品牌大于5个的时候阅读负担很大,条形图按销量排序后反而一眼就能看出格局。下面是最终用的matplotlib脚本的核心片段:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文乱码 plt.rcParams['axes.unicode_minus'] = False brand_stats = df.groupby('brand')['comment_num'].sum().sort_values(ascending=False).head(10) brand_stats.plot(kind='barh', figsize=(10, 6), color='#4C72B0') plt.title('京东手机品牌销量累计对比(按评价总数近似)') plt.xlabel('评价总数(条)') plt.gca().invert_yaxis() plt.tight_layout() plt.savefig('brand_sales.png', dpi=150)价格带直方图,我建议把分箱设置成1000元一段,横轴从0到12000,这样看到的市场结构最清楚。实际跑出来的结果符合预期:整个市场销量大头集中在1000到3000元,6000元以上区间呈现断崖式下跌,说明消费者对手机的价格敏感度非常高,高端市场容量有限。
4.3 做分析时容易犯的“自嗨式”陷阱
可视化是最容易做出“看似专业实则误导”的工作。我在这里踩过三个具体的坑:
- 把评论数直接等同于销量。京东商品页显示的大多是评价总数,不是期间销量。“200万+”只能说明历史累计评价多,不能说明本月卖得好。我所有图表的标题都清晰标注“按评价总数近似”,绝对不写“销量”字样。
- 饼图里有太多小品牌,导致视觉上所谓的“其他”占了很大一片。我处理的方式是:低于2%的归入“其他”,排行榜只展示前10,这样既保留了信息,又不让图糊成一团。
- 用颜色过度装饰,什么渐变、3D效果都上。图表设计的第一原则是冗余度最小,信息密度最大,老老实实用统一的配色,反而最有说服力。
散点图分析价格和评论数的关系时,我看到了一个有意思的现象:价格和评论数并不是单调递减,而是在2000到3000元价位存在一个“甜点区”,评论密度反而很高。这也符合消费直觉:中端机是大众换机的主流选择。这类洞察,不跑数据很难发现,也是这个系统真正有价值的产出。
5. 常见问题与排查技巧实录
5.1 请求返回空壳页面:先检查Cookie和访问频率
事不过三,爬虫被限制的最直接表现就是页面返回的HTML缺胳膊少腿:有框架结构,没有商品列表。排查步骤,我总结成一个固定顺序:
- 浏览器先手动访问一次相同的搜索页,确认当前页面结构是否正常。
- 用curl或requests单独请求,打印前500个字符看返回内容。
- 如果返回的是包含验证码关键词的页面,说明触发了风控,要降低频率。
- 检查Cookie是否过期,重新复制浏览器Cookie到配置里。
- 检查页面翻页参数是否正确,比如奇偶页的问题。
这个排查顺序在绝大多数情况下有效,因为80%的问题都出在前两步。真到第四步和第五步,说明大概率是策略性原因,不是代码bug。遇到这种情况就不要头铁,切到备选数据源。
5.2 解析结果为空或者字段对不上:别和动态渲染硬刚
京东的列表页字段选择器,过一段时间就会调整。如果你的XPath在Chrome调试面板里谋取正常,但requests取下来解析却是空的,十有八九是页面采用了懒加载视图。比如商品列表的前几页是在HTML里,翻到后面就用JSON接口加载。
排查思路是先打开浏览器的“查看网页源代码”,对比一下里面是否包含商品字段,如果源码里有而requests却没有,就是编码或者Response解码问题;如果源码里都没有,那就是动态渲染,必须切换playwright或者直接用接口请求。
我这里补充一个小经验:用正则re.search在HTML里直接搜索“手机”品牌关键词,如果能搜到,页面HTML里就有商品数据;搜不到,就别在解析上浪费时间,去看网络请求里的XHR接口。这个判断方法比看选择器快得多。
5.3 图表中文乱码:一次配置,全项目生效
matplotlib默认字体不支持中文,画图时标题全是方框。Windows下解法很简单,设置中文字体即可:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = FalsemacOS下把字体换成'PingFang SC'或'Arial Unicode MS',Linux下得先安装中文字体。这个配置建议放在项目入口文件或配置文件里,别在每个绘图函数里重复设置。
CSV文件也得注意编码:保存的时候用utf-8-sig而不是utf-8,否则之后用Excel打开会看到中文乱码。用pandas写CSV时加一句df.to_csv('output.csv', index=False, encoding='utf-8-sig')就行了。
5.4 pandas类型“假干净”:groupby排序前先转categorical
最后分享一个我坑了很久的细节。pandas的groupby结果柱状图,默认按品牌名称的字母表排序,不是按销量排序。这就导致画出来的条形图“不是按大小排列的”,看起来非常乱。解决方式是在画图前用sort_values()明确排序。
还有,因为“评价数”字段清洗后我转成了int,但有一行数据因为“2000+条评价”被解析成2000还行,但“1.2万+评价”解析成12000,单位换算不同,结果同是int却量级差了几百倍。这种“假干净”现象在数据里很隐嗨,不会报错,只会让最后的结果图出现一个离谱的异常点。所以我建议清洗完成后做一次字段值域检查:最大值、最小值、分位数是不是在合理范围内。这一步虽然不直接产出图表,但能避免你在错误数据上做一堆漂亮分析。
对一个数据分析系统来说,最宝贵的不是抓下来那一刻的原始数据,而是清洗完之后那些“敢在自己报告里直接引用”的数字。这套基于Python的京东手机销售数据分析系统,从抓取、存储、清洗到可视化,走通的正是这条链路。我的感觉是,数据分析项目最忌讳一步到位,从几百条数据把流程跑顺,再慢慢扩品类、扩字段,比一上来就追求大而全要踏实得多。如果你也想做类似的项目,不妨先从单一品牌、单一价格带的手机数据开始,练的就是这套流程。