Python爬虫实战:京东手机销售数据采集与可视化分析
发布时间:2026/9/26 13:20:33来源:尧图网络
京东手机品类的数据我盯了挺久。市面上的销量榜、价格分布、品牌份额基本是平台或媒体爱怎么写怎么写想拿到一份自己说了算的数还得自己动手。所以就有了这个“基于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用浏览器的真实UAReferer指向京东首页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/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.jd.com/, } def fetch_page(url): for attempt in range(3): try: resp requests.get(url, headersHEADERS, timeout10) 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, filenamejd_phone_raw.csv): with open(filename, a, newline, encodingutf-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手机encutf-8page{} 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 8GB128GB 燃橙色 5G手机 1.5K超清护眼硬核曲屏 5800mAh 超长续航1599.00200万Apple iPhone 15 (A3092) 128GB 蓝色 支持移动联通电信5G 双卡双待5399.0050万Redmi Note12 Pro 5G手机 12GB256GB暂无报价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排名全错。正确做法是识别“万”这个单位并乘上10000def 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(ascendingFalse).head(10) brand_stats.plot(kindbarh, figsize(10, 6), color#4C72B0) plt.title(京东手机品牌销量累计对比按评价总数近似) plt.xlabel(评价总数条) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(brand_sales.png, dpi150)价格带直方图我建议把分箱设置成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 MSLinux下得先安装中文字体。这个配置建议放在项目入口文件或配置文件里别在每个绘图函数里重复设置。CSV文件也得注意编码保存的时候用utf-8-sig而不是utf-8否则之后用Excel打开会看到中文乱码。用pandas写CSV时加一句df.to_csv(output.csv, indexFalse, encodingutf-8-sig)就行了。5.4 pandas类型“假干净”groupby排序前先转categorical最后分享一个我坑了很久的细节。pandas的groupby结果柱状图默认按品牌名称的字母表排序不是按销量排序。这就导致画出来的条形图“不是按大小排列的”看起来非常乱。解决方式是在画图前用sort_values()明确排序。还有因为“评价数”字段清洗后我转成了int但有一行数据因为“2000条评价”被解析成2000还行但“1.2万评价”解析成12000单位换算不同结果同是int却量级差了几百倍。这种“假干净”现象在数据里很隐嗨不会报错只会让最后的结果图出现一个离谱的异常点。所以我建议清洗完成后做一次字段值域检查最大值、最小值、分位数是不是在合理范围内。这一步虽然不直接产出图表但能避免你在错误数据上做一堆漂亮分析。对一个数据分析系统来说最宝贵的不是抓下来那一刻的原始数据而是清洗完之后那些“敢在自己报告里直接引用”的数字。这套基于Python的京东手机销售数据分析系统从抓取、存储、清洗到可视化走通的正是这条链路。我的感觉是数据分析项目最忌讳一步到位从几百条数据把流程跑顺再慢慢扩品类、扩字段比一上来就追求大而全要踏实得多。如果你也想做类似的项目不妨先从单一品牌、单一价格带的手机数据开始练的就是这套流程。
网站建设高端定制企业官网