1. 从“想看看京东手机行情”到完整分析系统,我经历了什么
先说结论:这个项目不是某个大厂的数据中台,也不是什么高深的算法模型,它就是一个能用、能跑、能出图的京东手机销售数据分析系统。最初的需求特别朴素——年底想换手机,想在京东上看看各个价位段的手机到底谁卖得好、谁的评价最真实、哪些型号是“刷单重灾区”。结果越查越深,最后干脆用Python做了一套从采集、清洗、分析到可视化的完整流程。
如果你也是刚接触Python数据分析不久,或者正在找练手项目,这个系统的价值在于:它不依赖任何付费数据源,完全基于京东网页公开的商品信息、销量标记和评论摘要,就能构建一个多维度的市场观察视角。标题里的“销售数据分析”听起来很宏大,但实际上拆开就四件事:爬数据、洗数据、算指标、画图表。
很多人一上来就想着用Scrapy框架搭分布式爬虫,或者直接上Hadoop那一套,我觉得完全没必要。京东手机品类虽然SKU多,但单个类目列表页加详情页的规模也就几万条,用requests加pandas就完全能扛住。真正的难点不在数据量,而在数据质量——手机商品的参数格式五花八门,同一个“8GB+256GB”的表述可能出现在标题、参数表、广告词三个地方,格式还不一样,这部分的处理工作占了整个项目的大头。
整套系统我划分成了五个模块:数据采集、数据清洗、特征工程、分析引擎、可视化报表。每个模块独立运行,中间用数据库对接。模块化的好处是,哪一块出了问题可以单独修,不用整个推倒重来。下面我把每个模块的完整思路和踩过的坑都展开讲,你可以直接照着复现。
2. 数据从哪儿来:采集方案设计,以及必须守住的合规边界
采集模块是整个系统的基础,数据源头如果出了问题,后面所有分析都是空中楼阁。我先说清楚我的抓取思路,再说合规问题,这一点非常关键。
2.1 采集目标与页面结构拆解
京东手机销售数据分布在两类页面上:列表页和详情页。列表页是搜索“手机”后出现的商品列表,一页几十个商品,包含商品名称、价格、标签(比如“自营”“京东超市”)、以及累计评价数——注意,京东网页版不直接展示销量数字,而是用“累计评价”这个数字替代,这在行业内是公认的销量近似指标。
详情页则是每个商品单独的主页面,包含完整的参数规格、促销信息、品牌官方数据、有时还有商品短视频的标题描述。我的采集策略是:先抓列表页拿到商品ID和基础价格,再用商品ID拼接详情页URL,抓取详细参数和评价摘要。
这里有个很实用的细节:京东商品ID是纯数字,详情页URL是https://item.jd.com/{商品ID}.html,列表页URL则是https://search.jd.com/Search?keyword=手机&enc=utf-8。我建议你直接用keyword=手机这个参数,而不是去点分类导航,因为分类导航页的页面结构更复杂,爬取成本更高。
2.2 请求头设置与反爬应对
京东的反爬策略这几年升级了好几次。早期只需要伪装User-Agent就能畅通无阻,现在基本是一套组合拳:User-Agent、Cookie、请求频率、访问行为模式,缺一不可。
最稳妥的方案是模拟真实用户访问序列。我封装了一个请求类,逻辑是这样的:
- 每个请求之间随机休眠2到5秒,不能固定间隔。
- 维护一个User-Agent池,每次请求随机抽取一个。
- 请求头带上完整的Referer、Accept-Language、Accept-Encoding。
- 首次请求先访问首页获取Cookie,再带着Cookie访问列表页。
用这种方式,我实测抓取了大约3000个手机商品的基础信息,没有被封IP。如果你是第一次上手,还可以用更温和的方式:直接下载京东商品列表页的HTML文件到本地,然后用BeautifulSoup离线解析,完全不碰在线请求。这种方式适合用来跑通后面所有的清洗和分析流程,不会有任何合规风险。
2.3 合规红线:什么能抓,什么不能抓
这里我必须强调,写爬虫和做数据分析一样,前提是守住边界。京东的商品名称、价格、公开参数这些信息,属于商品公开信息,用于个人学习研究是合理的。但有四条红线我建议你绝对不要碰:
- 不抓用户手机号、收货地址、订单详情等个人隐私数据。
- 不对京东的服务器发起高频并发请求,不做影响正常运营的抓取。
- 不把抓取的数据用于商业转售或大规模商业分析。
- 不绕过登录验证去抓取需要身份认证才能查看的内容。
在采集模块的代码里,我特意加了限速逻辑和异常退出机制,就是为了确保整个过程温和且可控。做技术的底气来自自律,不是来自破解能力。
2.4 增量采集与数据落库
数据落库我选的是SQLite,原因很简单:零配置、单文件、Python内置支持。手机商品数据不过几万条,SQLite的读写性能完全够用,没必要上MySQL。
建表时我是按“商品维度”和“评价维度”分开设计的。商品主表存商品ID、标题、价格、品牌、店铺类型、累计评价数;评价明细表存评价文本摘要、评分、评价时间。两张表通过商品ID关联。这样设计的好处是,后面做分析时可以根据需要灵活join,不用为了某个分析维度提前定制表结构。
增量采集的策略是每天跑一次,根据商品ID去重,只更新价格和累计评价数有变化的商品。这套机制跑了一个月,我手上攒了4个时间切片的手机数据集,为后面的趋势分析打下了基础。
3. 数据清洗的硬骨头:手机参数格式混乱的预处理实战
采集到的原始数据不能直接用,这是我在这套系统里教训最深的一环。原始字段大约有60多个,真正能用的不到一半,而且每个字段都有独一无二的“脏法”。我花了大概三天时间做清洗和特征工程,后面所有分析模型的准确性都建立在这次清洗的基础上。
3.1 价格字段的“文字陷阱”
京东的价格显示有一个特点,商品标题下方显示的是“京东价”,但有时候会叠加促销信息,比如“满3000减200”“plus会员价”之类的。但我抓的是静态HTML,这些动态信息不会出现在源码里,价格字段相对干净。
真正脏的是价格范围。比如一个商品有多个SKU(存储版本不同价格不同),列表页可能会显示“¥3999起”或者“¥4599.00”,前者就带了文字尾巴。清洗策略是用正则表达式提取数字部分:re.findall(r'\d+\.?\d*', price_str),如果匹配到多个数字,就取最小值作为起售价。这里要注意,有些价格带千分位逗号,要先去掉逗号再做提取。
3.2 内存版本的统一:从“8GB+128GB”到标准数字
手机参数表里最难的字段是内存版本。看一下我收集到的几种写法:
- 8GB+128GB
- 8+128G
- 8G运存+128G存储
- 8GB 128GB
- 8GB(运行内存)/128GB(机身存储)
如果不加清洗,这些会被当成完全不同的类别,聚成一堆乱七八糟的分组。我的解决方案是两步走:
第一步,分别提取运行内存和存储内存,正则匹配(\d+)\s*G,把第一个匹配结果当作运行内存,第二个当作存储内存。 第二步,把GB统一转换为GB数值,比如“12G”转成12。
这样清洗下来,所有手机都能落到“运行内存xGB、存储内存yGB”的标准维度上。这一步做完,内存对价格的影响建模才能成立。
3.3 品牌字段的自动归类
品牌字段不是每个商品都规规矩矩地放在参数表里的,很多手机标题里就写了品牌,但格式不统一:“Apple 苹果 iPhone 15”“小米 Xiaomi 14”“vivo iQOO 12”。单靠标题分词容易出错。
我的做法是维护一个品牌关键词映射表,包含主流手机品牌及其常见别名,然后用str.contains()逐一匹配标题和参数表中的品牌字段。匹配不到的归为“其他”。实测下来,这个规则方法的F1值比直接跑一次文本聚类还好,而且代码只有不到30行。
3.4 评论数据的去重与基础清洗
评价摘要文本里有大量重复词、广告词、无意义短评。我做了两个清洗动作:长度小于10个字符的评论直接丢弃;用difflib.SequenceMatcher的相似度阈值0.9去除高度重复的评论。剩下的有效评论文本用于情感分析。
清洗模块的代码结构我建议这样组织:
def clean_price(raw_price): # 去掉千分位逗号,提取首个数字 cleaned = re.sub(r',', '', str(raw_price)) numbers = re.findall(r'\d+\.?\d*', cleaned) return float(numbers[0]) if numbers else None def clean_memory(raw_ram, raw_storage): # 分别提取运行内存和存储内存 ram_match = re.search(r'(\d+)\s*G', str(raw_ram)) storage_match = re.search(r'(\d+)\s*G', str(raw_storage)) ram = int(ram_match.group(1)) if ram_match else None storage = int(storage_match.group(1)) if storage_match else None return ram, storage def classify_brand(title, param_brand): # 品牌关键词映射匹配 brand_map = { '苹果': ['apple', '苹果', 'iphone'], '华为': ['huawei', '华为', 'honor'], '小米': ['xiaomi', '小米', 'redmi', '红米'], 'vivo': ['vivo', 'iqoo'], 'oppo': ['oppo', '一加', 'oneplus'], '荣耀': ['honor', '荣耀'] } combined_text = f"{title} {param_brand}".lower() for brand, keywords in brand_map.items(): for kw in keywords: if kw in combined_text: return brand return '其他'4. 分析层:价格、配置和评价三个维度能挖出哪些结论
数据清洗完以后,分析工作就变得顺理成章了。我建了一套“价格-配置-评价”的三角分析模型,核心问题是:京东手机市场的价格带分布如何?配置如何影响定价?评价数据能反映哪些真实使用感受?
4.1 价格带分布与品牌段位图
把清洗后的价格字段按区间切分,我设定了几档:1000元以下、1000-2000元、2000-3000元、3000-5000元、5000元以上。用pd.cut()函数实现,然后按品牌交叉统计。
让我比较意外的结论是:在2000-3000元这个价格区间,竞争最激烈,覆盖的品牌数最多;而在5000元以上区间,苹果的份额几乎是一骑绝尘,安卓阵营里只有华为的旗舰系列能撑住场面。这种“金字塔结构”不是拍脑袋想出来的,是真实数据呈现出来的。
交叉表分析用一行代码就能出结果:
price_band = pd.cut(df['price'], bins=[0, 1000, 2000, 3000, 5000, np.inf], labels=['千元以下', '1000-2000', '2000-3000', '3000-5000', '5000以上']) brand_price = df.groupby([price_band, 'brand'], observed=False).size().unstack(fill_value=0)4.2 配置与价格的回归关系
手机配置里最重要的三个连续变量是运行内存、存储内存和电池容量。把它们作为特征,对价格做线性回归,能直观看到“每增加1GB运行内存,价格大约上涨多少”。
这里我遇到一个典型陷阱:运行内存和价格不是线性关系,低端机8GB和12GB的差价不大,但高端机的内存升级溢价明显更高。直接跑普通线性回归,残差图会呈现喇叭形。我换成了对数变换,把价格取log后再回归,效果才合理。
如果你想复现,可以用statsmodels库,它会输出完整的回归摘要,包括每个特征的系数、标准误和置信区间,比自己手算方便很多:
import statsmodels.api as sm features = df[['ram_gb', 'storage_gb', 'battery_mah']].fillna(0) features = sm.add_constant(features) model = sm.OLS(np.log(df['price']), features).fit() print(model.summary())4.3 评价情感分析与“刷单”识别
评价文本的情感分析技术上有很多路线,从最简单的基于情感词典,到BERT微调。我这套系统的定位是轻量级分析,所以用了SnowNLP库——它内置了中文情感倾向分析功能,调用.sentiments属性就能得到一个0到1的情感得分。
清洗后的评论数据我按商品聚合成平均情感分,再和累计评价数做个散点图。这里能看出一个有意思的规律:某个商品如果累计评价数特别高、但平均情感分显著低于同类,大概率是促销跑量型产品,评论里真实使用体验占比低;反之,评价数中等但情感分高的,往往是口碑型产品。
如果你觉得SnowNLP的准确率不够,可以把每个商品的正负评论抽样出来,用关键词匹配做二次校验。我在实践中的经验是,对于“流畅”“续航好”“拍照清晰”这类高频正向词,直接关键词匹配的准确率反而不低,而且可解释性更强。
4.4 时间切片与促销节点趋势
这可能是整套系统里最有“数据分析感”的部分。因为采集模块是增量跑的,我积累了一个月内多个时间点的价格快照。把这些快照合并成宽表后,就能看每个商品的降价曲线、涨价曲线,以及整类目的价格中位数变化。
我对比了双十一前的两周和日常周的价格中位数,发现大量商品的“促销价”其实是先涨价再打折的结果,真正价格平稳的商品反而没参与所谓的大促。这类结论如果不用时间切片数据,根本无法验证。你的数据集里如果暂时没有时间维度,可以先跳过这个分析,等增量采集积累几周后再回头看,效果会很好。
5. 可视化层:用Pyecharts做出能直接汇报的数据看板
分析结果最终要向人表达,可视化模块承担这个职责。我选择了Pyecharts,理由有三个:生成的是HTML文件,可以在浏览器里直接交互查看;图表类型丰富,地图、漏斗、词云都有现成组件;输出格式干净,方便截图放到汇报ppt里。
5.1 第一张图:品牌价格带热力图
热力图非常适合展示“品牌x价格区间”的交叉关系。横轴是价格带,纵轴是品牌,方块颜色深浅代表商品数量。用Pyecharts的HeatMap组件实现,注意数据要转成[x, y, value]的三元组格式。
出这这张图我花了最长的时间在做数据透视表的行列排序上。默认排序是按品牌名拼音,没法体现数据层级。我用sort_values()按商品总数从高到低排序,再把价格带按从低到高排列,视觉效果会直观很多。
5.2 第二张图:内存配置气泡图
气泡图用来同时展示三个维度:x轴是存储内存,y轴是运行内存,气泡大小代表商品数量,颜色代表平均价格。Pyecharts的Scatter图表加上visualMap颜色映射就能实现。
气泡图最适合回答的问题是:“哪个配置段是手机厂商的主力出货区?”我在图里清晰地看到,12GB+256GB和12GB+512GB是两个密集区域,而16GB+1TB这种顶配组合气泡明显偏小。这说明厂商发力的重点,和消费者的真实选购行为是吻合的。
5.3 第三张图:评价情感得分排名条形图
条形图用于展示不同品牌在“平均情感得分”上的差异。我给情感得分加了一个参考线0.5,大于0.5表示正面评价为主,小于0.5表示负面评价居多。
用Pyecharts画条形图本身不复杂,但要注意一点:品牌平均值是聚合后的结果,同一品牌内部不同价位段手机的情感差异非常大。我建议你在条形图上叠加每个品牌的得分区间(用误差棒或者同时画散点),否则会掩盖真实情况。我后来改成了“箱线图+均值点”的组合,信息量丰富了一个层级。
5.4 可视化模块的工程化封装
每个图表我都封装成了一个独立的函数,输入是清洗后的DataFrame,输出是HTML文件路径。这样做既方便调试单个图表,也方便后续整合到一个汇总页面里。
def render_brand_price_heatmap(df, output_path): # 数据透视 price_band = pd.cut(df['price'], bins=price_bins, labels=price_labels) pivot = df.groupby([price_band, 'brand'], observed=False).size().unstack(fill_value=0) # 构造三元组数据 heat_data = [] for j, brand in enumerate(pivot.columns): for i, band in enumerate(pivot.index): heat_data.append([j, i, int(pivot.loc[band, brand])]) # 初始化图表 c = HeatMap() c.add('商品数', pivot.columns.tolist(), pivot.index.astype(str).tolist(), heat_data, label_opts=opts.LabelOpts(is_show=False)) c.set_global_opts( title_opts=opts.TitleOpts(title='京东手机品牌-价格带热力图'), visualmap_opts=opts.VisualMapOpts(max_=100), ) c.render(output_path)6. 部署与运行:整个系统的一键启动思路
数据分析系统的价值在于可重复运行,而不是一次性脚本。我设计了一键启动入口和模块化配置,这样每天新增数据后跑一遍就能更新所有报表。
6.1 项目目录结构
我的目录组织方式如下:
jd_phone_analysis/ ├── config.py # 全局配置:URL常量、数据库路径、价格区间 ├── collector/ # 采集模块 │ ├── fetcher.py # 请求封装 │ ├── parser.py # HTML解析 │ └── storage.py # 数据落库 ├── cleaner/ # 清洗模块 │ ├── price.py # 价格清洗 │ ├── memory.py # 内存版本清洗 │ └── brand.py # 品牌归类 ├── analyzer/ # 分析模块 │ ├── price_analysis.py │ ├── sentiment.py │ └── regression.py ├── visualizer/ # 可视化模块 │ ├── charts.py │ └── dashboard.py └── main.py # 一键入口每个模块的职责单一,依赖关系清晰。main.py里用argparse控制运行阶段,支持--stage collect、--stage clean、--stage analyze分别执行。
6.2 SQLite数据库的读取调优
SQLite虽然轻量,但读取时还是有几个性能注意事项。一个典型的坑是:商品表中的累计评价数是字符串类型,直接排序会按字典序排,导致“9999”排在“10000”后面。我的解决方案是在建表时就用INTEGER类型存储数字字段,或者在SQL查询里加CAST(comment_count AS INTEGER)。
另一个建议是给常用查询字段建索引,特别是brand和price:
CREATE INDEX idx_brand ON products(brand); CREATE INDEX idx_price ON products(price);加了索引后,按品牌筛选的速度能提升一个数量级,虽然数据量不大,但每次跑分析都等几秒钟,积累下来体验差距很大。
6.3 main.py的一键运行逻辑
一键启动的代码非常朴素:
def main(): parser = argparse.ArgumentParser(description='京东手机销售数据分析系统') parser.add_argument('--stage', default='all', choices=['collect', 'clean', 'analyze', 'visualize', 'all']) args = parser.parse_args() if args.stage in ('collect', 'all'): run_collect() if args.stage in ('clean', 'all'): run_clean() if args.stage in ('analyze', 'all'): run_analyze() if args.stage in ('visualize', 'all'): run_visualize()如果说有什么经验教训,那就是不要把每个阶段都直接“一把梭”跑完。数据量大了以后,采集阶段耗时长,清洗阶段中途失败要重新跑,最好分开执行,便于定位问题。
7. 复盘:这套系统最值钱的地方,和最值得改进的部分
一个月跑下来,这套系统带给我最大的收获不是一箩筐图表,而是建立了一种“数据驱动观察”的思维方式。市面上所有关于手机销量的文章,很多都是厂商通稿和渠道传闻,只有自己从数据里挖出来的结论,才经得起追问。
7.1 最值钱的三件事
第一,完整的电商数据采集和分析流程经验。从列表页到详情页再到清洗入库,这条链路在任何一个电商分析项目中都能复用,不只是京东、不只是手机。
第二,对“销量数据”这个概念的祛魅。京东的“累计评价数”不等于销量,但它是目前公开可得的、相对可信的近似指标。理解了数据代理指标和真实指标的关系,对后续做任何商业分析都有帮助。
第三,配置参数对价格的半对数回归模型。这个模型的解释能力非常强,输出结果可以直接用来估算“一台手机的成本天花板”和“品牌溢价空间”,这对消费决策是有实际参考价值的。
7.2 值得改进的部分
当前系统最大的短板是情感分析准确率。SnowNLP的预训练模型偏向通用领域,对手机评测文本中的专业词汇(如“性能调度”“发热控制”“屏幕边框”)理解不够。后续计划引入一个基于手机评测语料微调的轻量文本分类模型,或者直接用几家大厂的API做跨平台对比校验。
另一个改进方向是时间维度的自动化报告。目前的趋势分析需要手动设定对比周期,下一步可以做成定时调度任务,每周自动生成一份价格波动简报,发送到邮箱。这个功能在商业环境里的价值会更高,因为价格趋势一旦形成时间序列,就能做简单的预测和异常检测。
7.3 给新手的实操建议
如果你是第一次做这类完整的数据分析项目,我的建议是:先别追求完美。就用离线的HTML文件,写一百行清洗代码,画三张图,跑通整个流程。等你理解了“脏数据会在哪个环节出现”“图表到底在表达什么关系”之后,再回来补采集模块、增量更新、自动化部署都来得及。
我自己也是从最原始的Excel手动统计起步的,后来才一步步把每个环节工具化。判断一个数据分析项目是否成功的标准,从来不是用了多少技术栈,而是它能不能持续地回答你真正的疑问。这套京东手机数据分析系统,帮我回答了很多实际问题,希望你也能从复现它的过程中,找到属于自己的分析视角。