ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Python的京东手机销售数据分析系统:从爬虫采集到可视化报告

基于Python的京东手机销售数据分析系统:从爬虫采集到可视化报告 我最初做这个项目纯粹是因为自己买手机时被搞烦了。想在京东上看一款手机到底值不值翻了几十页商品列表对比参数、看评价、查历史价格一晚上就耗进去了。后来给家里人挑手机又是同一套流程我就在想能不能把这些活儿全丢给 Python让它自己抓数据、自己算、自己出报告于是就有了这套基于 Python 的京东手机销售数据分析系统。这套系统能做的事简单概括就是三个环节自动采集京东手机品类的商品数据把价格、评价、品牌、型号等字段清洗后落库再按照预设的分析模型产出价格分布、品牌格局、性价比评分等结论最后用可视化图表呈现出来。适合想从零入手学习 Python 数据采集与分析的读者也适合有选品、比价、市场调研需求的朋友参考。日常被吐槽“Python 学了不知道能干嘛”的人看完这篇应该能改观不少。1. 为什么拿京东手机品类开刀这个切入口的数据价值想清楚“分析什么”比“怎么分析”更重要。我之所以选京东手机这个垂直品类有几个层面的考虑不完全是拍脑袋。1.1 手机品类的分析价值天然适合练手手机是标准化的标品同一款型号在不同店铺、不同促销节点下价格差异和评价分布都很明显这种数据特性非常适合做结构化分析。相比之下服装、食品这类非标品光规格就有一堆变体清洗的工程量会指数级上升对新手不太友好。手机品类的字段也非常规整商品标题、品牌、型号、价格、好评率、评价数、店铺类型自营/第三方这些字段基本都能从页面或接口里拿到。字段规整意味着数据建模和后续的可视化不必在清洗阶段消耗太多精力你可以把更多时间花在分析逻辑本身。对于数据集市来说字段质量决定了分析深度的天花板手机品类正好是那种“底子好”的数据源。1.2 平台选京东的对比优势当时我也考虑过其他平台最后留下京东是因为它在数据获取上有几个实际优势维度京东的特点对分析系统的价值价格体系促销价、到手价、plus价分层可以观察价格弹性和促销力度评价体系好评率明确评价样本量大口碑量化有依据自营与第三方店铺类型清晰物流节点区分明显可以对比渠道信任度对销量的影响商品结构搜索页默认排序有“销量优先”销量热度能直接参与建模京东的“商品编号 评论数 好评率”这套数据结构事实上有种公开数据仓库的味道。比起某些平台把关键数据藏在登录后的脚本里京东的静态数据已经足够支撑一套完整的分析系统这也是我最终确定方案的关键因素。当然必须强调一个问题无论采集哪个平台的数据都要尊重目标网站的规则控制请求频率只把抓取行为限制在个人学习研究范围内。别把你的服务器 IP 变成对方的重点关照对象对大家都好。2. 数据获取链路从商品列表到详情页的完整采集方案这一节是整个系统里最容易翻车的地方也是工作量最大的部分。我最初只写了不到 50 行 requests 代码就以为搞定了结果第一天跑下来发现抓回来的数据缺了一半全是“暂无报价”和空评论数。核心原因是没理清京东页面的数据加载机制。2.1 先理清页面结构列表页、详情页、价格接口各管各的事京东手机品类的数据分散在三个层级搜索列表页承载商品ID、标题、主图、评价数、店铺名。这是采集入口负责“找到有哪些商品”。商品详情页承载更完整的品牌、型号、参数、好评率、价格区间。这一层用于补充列表页拿不到的属性。价格接口实际成交价往往不是详情页 HTML 里的静态文本而是通过独立的异步接口返回。直接抓 HTML 会出现“暂无报价”。所以正确的链路是先抓列表页拿到商品ID列表再根据ID去请求详情页和价格接口最后把三层数据合并成一条完整记录。当时我用的核心代码长这样逻辑很直白import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.jd.com/ } def fetch_search_page(keyword, page): url fhttps://search.jd.com/Search?keyword{keyword}page{page} resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 return resp.text def parse_product_ids(html): soup BeautifulSoup(html, html.parser) items soup.select(li.gl-item) ids [] for item in items: pid item.get(data-sku) if pid: ids.append(pid) return ids列表页的每个li.gl-item自带>def fetch_detail(pid): detail_url fhttps://item.jd.com/{pid}.html resp requests.get(detail_url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) title soup.select_one(div.sku-name).text.strip() if soup.select_one(div.sku-name) else None comment_tag soup.select_one(#comment-count a) comments comment_tag.text.strip() if comment_tag else 0 return {id: pid, title: title, comments: comments} def fetch_price(pid): price_api fhttps://p.3.cn/prices/mgets?skuIdsJ_{pid} resp requests.get(price_api, headersheaders, timeout5) data resp.json() price data[0][p] if data else None return price这里有个很实际的坑详情页的评论数和好评率默认只展示一部分必须点“查看全部评价”才看得到全量。如果仅仅依赖详情页得到的好评率可能是“98%”这种四舍五入的展示值放到建模里会有一定误差。我当时处理的办法是直接采集商品标题里的品牌和型号把好评率当作展示级数据使用在模型里给它分配较低的权重避免单字段误差主导结论。2.3 采集链路不能忽略的合规与频率控制很多初学者写完爬虫就跑跑着跑着发现悲剧了IP被临时封禁页面返回滑块验证。这通常不是对方针对你而是你的请求频率明显超过了正常用户的浏览节奏。我实测下来的安全做法是import time import random def fetch_with_rl(url, headers, min_interval2.0, max_interval5.0): time.sleep(random.uniform(min_interval, max_interval)) resp requests.get(url, headersheaders, timeout10) return resp每次请求之间至少间隔 2 到 5 秒每个商品ID的详情和价格接口并行不超过 5 个线程。这样跑一个几千条数据的集合大约要两三小时但胜在稳定。真正生产化的系统可以再引入代理池但个人学习研究阶段没必要把复杂度拉这么高反而容易惹出合规问题。3. 数据落库与清洗没有干净的数据分析就是空中楼阁采集只是第一步真正决定分析质量的是落库和清洗。我在这个项目里用的是 MySQL pandas 的组合MySQL 存全量历史数据pandas 在分析时按需取出做清洗和计算。这套组合对中小规模数据集非常实用不需要上 Hadoop/Spark 这类重型武器。3.1 三张核心数据表的设计思路数据库设计决定了后续分析的灵活度。我建了商品表、评论表、价格历史表三张表各司其职CREATE TABLE products ( product_id VARCHAR(20) PRIMARY KEY, title VARCHAR(255), brand VARCHAR(50), model VARCHAR(50), shop_type VARCHAR(20), price DECIMAL(10,2), comments INT, good_rate DECIMAL(5,2), crawl_time DATETIME ); CREATE TABLE price_history ( id INT AUTO_INCREMENT PRIMARY KEY, product_id VARCHAR(20), price DECIMAL(10,2), crawl_time DATETIME ); CREATE TABLE reviews ( id INT AUTO_INCREMENT PRIMARY KEY, product_id VARCHAR(20), review_content TEXT, review_rating TINYINT, review_time DATETIME );商品表存当前快照价格历史表存同款商品不同时期的价格评论表存评论文本用于口碑分析。为什么价格要单独拆一张表因为如果你只在商品表里更新当前价格三个月后想算“这个型号平均降价了多少”就完全没法追溯了。价格历史表是时间序列分析的基础也是这个系统最有含金量的一部分。3.2 pandas 清洗的四个高频场景落库只是第一步抓下来的数据几乎不可能直接用于分析。我遇到最多的是这四个问题重复数据同一个商品在不同关键词的搜索结果里会出现多次比如搜“小米15”和搜“小米15 Pro”都可能带出同一款机型。处理方式是按商品ID去重df df.drop_duplicates(subsetproduct_id, keeplast)这里保留最后一次采集的快照因为keeplast意味着更新的价格和评论数会覆盖旧值。缺失字段第三方店铺的有些详情页不展示品牌字段价格接口偶尔返回空。这种场景不要直接删行先用众数或中位数填充。品牌字段可以考虑从标题里正则匹配“小米/华为/OPPO/vivo”等关键词拆出来。类型转换抓回来的评论数是字符串还带“万”字后缀必须统一转换def parse_comments(val): if isinstance(val, str) and 万 in val: return int(float(val.replace(万, )) * 10000) return int(val) df[comments] df[comments].apply(parse_comments)异常值过滤有些商品的价格会因为缺货、下架而变成 0 元或者异常低值比如 9.9 元的手机壳混进手机品类里直接用标准差法过滤掉偏离均值 3 倍以上的价格记录同时加上“价格大于 100 元”的业务硬阈值。3.3 增量更新的去重策略系统不能每次采集都全量重来商品表要支持增量更新。做法很简单先按商品ID去重再通过时间戳字段判断本次采集是否要覆盖旧记录。价格历史表则永远追加不覆盖。这样既保证当前快照的准确性也为后续做历史价格分析保留了完整的时间序列。4. 核心分析逻辑价格分布、品牌格局与性价比模型数据准备好之后真正的分析工作才开始。我当时给自己定的目标是这套系统要能回答三个问题——同价位段哪些手机值得买哪个品牌在当前的受欢迎程度最高某款手机现在的价格到底处于历史高位还是低位4.1 价格区间分布与品牌价格对比第一个问题用价格分布直方图就能直观回答。但要注意分箱宽度手机市场从 1000 元到 8000 元跨度太大用固定 1000 元一档会把入门机和旗舰机混在一起。我实测下来按 1500 元一档分箱比较合理能区分出“千元机”“中端机”“高端机”“旗舰机”四个层级。品牌价格对比更适合用箱线图看每个品牌的价格中位数和四分位距。中位数比均值更稳健因为旗舰机型的存在会把均值拉得很高中位数才能反映品牌的主力销售区间。import pandas as pd df_price pd.read_sql(SELECT brand, price FROM products, conn) brand_median df_price.groupby(brand)[price].median().sort_values()4.2 综合性价比评分模型的构建思路参考电商行业的常用做法我可以给出一个可解释的评分公式。它的设计原则是好评率衡量口碑评价数衡量热度价格衡量代价。口碑指数 好评率 × log(1 评价数) × 100 性价比指数 口碑指数 / 价格 × 1000为什么评价数要取对数因为评价数的分布是典型的长尾头部机型可能几十万条评价长尾机型几百条直接用线性值会把长尾机型全部压到接近零。取对数之后5000 条和 50000 条的差距被压缩到合理范围数量级差异不至于主导整个评分。代码实现很简单import numpy as np import pandas as pd df[log_comments] np.log1p(df[comments]) df[reputation_score] df[good_rate] * df[log_comments] * 100 df[value_score] df[reputation_score] / df[price] * 1000 df.sort_values(value_score, ascendingFalse, inplaceTrue)跑出来的结果挺有意思性价比榜第一梯队常常不是那些配置最高的旗舰机而是上一代降价后的次旗舰。这背后逻辑是次旗舰的性能和口碑都不弱但价格已经被新机型拉低导致性价比指数飙升。4.3 历史价格波动这个型号现在买亏不亏有价格历史表之后可以做一件很实用的事给每个型号计算“当前价格处于历史区间的位置”。price_range price_history.groupby(product_id)[price].agg([min, max, mean, std]) current df.set_index(product_id)[price] position (current - price_range[min]) / (price_range[max] - price_range[min])位置值 0 到 0.2 说明当前价格接近历史最低0.8 以上说明正处于高位。这个指标对于“要不要现在入手”的决策非常有参考价值。我把这个值起名叫“价格温度”一看到名字就知道数字代表什么。5. 可视化呈现让数据自己说话分析得再透彻如果图表没法让别人一眼看懂整个系统就缺了最后一公里。可视化这块我采用的是“本地脚本出静态图 Web 仪表盘出动态界面”的双轨方案兼顾快速查看和日常交互。5.1 四类核心图表的选择逻辑每个分析维度选图表都不是随便选个好看的模板而是要匹配数据类型分析目标图表类型为什么选它价格区间分布直方图1500元分箱直接展示各价位段的商品密度品牌价格对比箱线图同时展示中位数、四分位距和异常值好评率与评价数关系散点图观察两个连续变量之间的相关性品牌市场份额饼图/环形图直观展示各品牌的商品数量占比散点图是我最推荐大家多用的图表。我当时把好评率作为横轴、评价数作为纵轴对数刻度作图发现了一个非常有趣的现象绝大多数机型聚集在 95% 好评率以上但评价数差距巨大。这说明“好评率”本身的分辨率有限真正能把机型拉开差距的是评价数的量级——口碑好不等于卖得好这个洞察只有散点图能直观呈现。5.2 从 5000 条数据里提炼运营结论的实操案例当时我抓了大概 5000 条有效商品记录覆盖 12 个主流品牌。用上面的模型跑完全部数据之后几个结论非常清晰第一在 2000 元到 3500 元价位段竞争最激烈价格中位数密集单款评价数差异极大。这说明所谓的“中端机红海”不只是营销话术数据上就是如此。第二自营商品的评分均值显著高于第三方店铺商品但第三方店铺商品的价格整体低 8% 到 15%。这对于消费者来说是典型的“性价比 vs 信任感”权衡用在系统里可以直接生成决策建议。第三某些主打线下的品牌在京东平台的评价数中位数明显偏低但单款好评率并不差。这说明其线上存在感偏弱不是产品不行而是渠道重心不同。这些结论听起来像市场分析报告才会说的话但整套流程就是从数据获取、清洗、建模到可视化的标准动作里冒出来的。工具没有白交学费的意义能产出洞察的工具才是资产。5.3 用 Flask ECharts 搭建简易仪表盘脚本出静态图适合自己看但把系统交给不懂数据分析的人用还是需要一个交互界面。我搭了一个非常轻量的 Flask 应用前端用 ECharts后端只提供 JSON 数据接口。核心结构只有三个文件app.pyFlask 入口、queries.py数据库查询逻辑、templates/chart.htmlECharts 渲染页面。查询逻辑的关键是让数据库先在 SQL 层面完成聚合而不是把全量数据塞给 pandas 再算后者数据量一大就会导致页面卡成 PPTapp.route(/api/price_dist) def price_dist(): sql SELECT FLOOR(price/1500)*1500 AS bucket, COUNT(*) AS cnt FROM products GROUP BY bucket ORDER BY bucket data pd.read_sql(sql, conn) return data.to_json(orientrecords)6. 全流程踩坑记录那些文档里查不到的经验这个项目从立项到跑通中间踩的坑比想象中多。这节把几个典型问题完整记下来既是我自己的复盘也给后来人省点时间。6.1 四个高频问题及完整排查过程问题一搜索页返回数据突然变少某天跑完增量采集发现商品数量从 5000 掉到了 800。第一反应是页面结构变了但查看 HTML 后发现页面里确实有商品节点。后来对比请求头发现是登录 Cookie 过期导致京东把默认搜索切换成了“不显示无货商品”。排查过程先确认页面节点存在再怀疑筛选条件最后发现是 Cookie 被清。解决方案是重新登录京东网页端把 Cookie 写入配置。问题二多线程并发采集导致频繁弹出滑块验证我最初用 concurrent.futures 开了 10 个线程抓价格接口跑了一分钟就开始触发验证。排查过程先检查请求头没问题再查频率发现 10 个线程 × 每线程 1 次/秒整体 QPS 已经到 10远超正常人的浏览速度。解决方案是引入信号量把并发数限制到 3import threading semaphore threading.Semaphore(3) def fetch_with_limit(url, headers): with semaphore: time.sleep(random.uniform(1, 3)) return requests.get(url, headersheaders, timeout10).text问题三写入 MySQL 报中文乱码采集数据里有大量中文商品标题和品牌名写入时报Incorrect string value。排查确认是表字符集问题不是程序问题。建表时统一加上DEFAULT CHARSETutf8mb4连接字符串里也指定charsetutf8mb4即可解决。utf8 在 MySQL 里只能存三个字节而 emoji 和部分生僻汉字需要四个字节所以千万别偷懒。问题四matplotlib 画图中文显示为方块Windows 下 matplotlib 的默认字体不包含中文解决方案是强制指定中文字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False这个坑几乎是每个做中文可视化的 Python 学习者必踩的提前配置好能省不少无谓的折腾时间。6.2 性能优化全量重算改增量计算最初版本每次分析都会把全量数据重新读一遍跑一次要等十几秒。后来发现核心瓶颈在 read_sql 的数据传输量于是把所有聚合逻辑都下沉到 SQLPython 端只负责接收已经聚合好的小结果集。改造之后同样的分析跑完基本在一秒以内。如果数据量继续增长下一步可以考虑把价格历史表按月做分区或者定期把聚合结果物化到一张 summary 表让报表直接查 summary 而不是查明细。7. 这套系统目前的运行状态与后续扩展方向系统从最初 200 行的爬虫脚本到现在已经扩展成了一个包含采集、清洗、建模、可视化、Web 展示的完整项目。目前它每周日自动跑一次增量采集把新品和价格波动纳入数据库然后更新仪表盘的数据指标和图表。以我自己的实际使用体验来说这套系统最大的价值反而不是“帮我选手机”而是建立了一种用数据做消费决策的习惯。以前买手机会被各种宣传词带着跑现在我会先看这个型号的价格温度、性价比指数和品牌整体口碑十几秒就能判断值不值得入手。后续如果还有精力我想在两个方向继续扩展。一是接入评论正负面情感分析用简单的词典匹配或者调用现成的情感分析模型给评论数靠前的机型生成一个“用户痛点词云”看看被吐槽最多的是续航还是屏幕。二是接入更多品类把手机这套分析模板平移到笔记本、平板电脑、家电等标品门类形成一个通用的“3C 商品数据分析平台”。最后分享几个实操层面的个人建议。第一别追求把系统一次设计得很庞大先跑通一条最小链路再去补齐功能和数据维度。第二分析模型要留出可解释性你能说出每个数字是怎么算出来的别人才敢用你系统里的结论。第三爬虫采集务必控制频率和规模我见过太多人因为采样过度导致封 IP反而拖慢了整个项目进度。这套系统跑到现在每次打开仪表盘看到最新的价格分布和品牌格局我都还能发现一些新的小规律。工具本身并不神秘重要的是你对数据的好奇心和把它打通成系统的耐心。
返回列表