做竞品分析最烦的就是数据。去年一个做电商运营的朋友找我,说想调研某品类在京东上的竞争格局,人工去翻页面、记价格、数评价数,光几十个SKU就得折腾一两个星期,等统计完市场又变了。我当时直接用Python爬虫把京东公开的商品标题、价格、评价数和评论文本批量抓了下来,再配合pandas做清洗、可视化出报告,整个过程大半天搞定。这篇文章就把这套完整方案写出来,从列表页采集到详情页补全,再到评论区的数据挖掘,全部是可复现的实操流程。适合有Python基础、想用爬虫做真实业务分析的同学参考,也适合想系统了解京东页面采集思路的入门者。
1. 开搞之前:想清楚京东数据能做什么分析,也把合规边界划明白
1.1 京东商品信息的分析价值拆解
先说为什么要盯京东。京东平台的商品数据结构化程度很高,商品标题、价格、店铺名称、评价数、好评率这些字段基本都直接渲染在页面上,非常便于采集和对比。和淘宝那种千人千面的个性化推荐页面相比,京东同一关键词下的搜索结果排序相对稳定,干扰因素少,做横向竞品分析时数据可比性更强。
具体到一个品类下,能做的分析维度主要有这几块:
- 价格分布:把竞品价格全部拉下来,看价格带集中在哪个区间,找出被市场接受的定价锚点。
- 评价数量:评价数直接反映销量热度,虽然不等于销量,但作为横向对比指标足够说明问题。
- 好评率:京东各商品页会显示好评度百分比,这是用户口碑的直接量化体现。
- 评论内容:这是最有价值的部分。用户会在评论里提到"续航不行""做工好""物流快"等细节,这些是天然的产品功能反馈池。
- 店铺归属:看竞品是自营还是第三方店铺,不同店铺类型背后对应的货源和售后策略不同。
把这些字段整合起来,基本就能回答"这个品类里谁在领跑、谁在掉队、用户到底在意什么"这三个竞品分析中的核心问题。
1.2 采集边界与合规原则:公开数据不等于可以乱采
动工之前必须先把规矩说清楚。京东页面上的商品信息属于公开数据,个人基于学习研究目的做适量采集,这类操作本身是行业里比较常见的做法。但这里有几个边界必须遵守:
- 只采集商品详情、价格、评论等公开信息,不碰用户的个人隐私数据,比如订单信息、收货地址、账号信息。
- 不绕过登录验证机制,不使用账号Cookie去获取非公开数据。
- 必须控制请求频率,不能对目标服务器造成压力。业内比较稳妥的做法是单次请求间隔2到5秒,单账号单IP的采集量控制在合理范围内。
- 遵守robots.txt协议精神,京东明确禁止的路径不去踩。
这篇文章里所有代码,都基于"模拟普通用户浏览行为"这个前提来写。我不会展示任何破解验证码、绕过风控或伪造身份的内容。爬虫只是数据获取工具,真正的功夫在于数据清洗和业务分析。
1.3 开发环境与依赖安装
我用的是Python 3.9版本,推荐3.8以上。核心依赖如下:
| 库名 | 用途 | 安装命令 |
|---|---|---|
| requests | 发起HTTP请求 | pip install requests |
| beautifulsoup4 | 解析HTML页面 | pip install beautifulsoup4 |
| lxml | 解析器,速度比html.parser快 | pip install lxml |
| pandas | 数据清洗与分析 | pip install pandas |
| matplotlib | 图表绘制 | pip install matplotlib |
| jieba | 中文分词,用于评论分析 | pip install jieba |
| wordcloud | 生成词云图 | pip install wordcloud |
如果你打算把采集结果存到数据库,建议装一个SQLite,Python标准库自带,不用额外安装。后续所有代码我都会给出可以直接复制的片段,但请根据自己的需求改字段和参数,不要原封不动跑完就完事。
提示:写爬虫之前,先想清楚数据分析要什么字段,这会直接影响解析代码怎么写。很多人是爬到一半发现字段不够,又回头改,白白浪费时间。
2. 商品列表页采集:从关键词搜索到结构化商品字段
2.1 先看一次搜索请求,摸清页面返回规律
我拿"蓝牙耳机"这个关键词举例。浏览器打开京东搜索页,地址栏里的URL长这样:
https://search.jd.com/Search?keyword=蓝牙耳机&enc=utf-8&wq=蓝牙耳机这里有个关键点:京东搜索列表页的商品数据有两种来源,一部分直接渲染在HTML里,另一部分是通过异步接口加载的。用requests直接请求这个URL拿回来的HTML里,已经包含了前几页的商品标题、店铺和评价数。价格字段比较特殊,后面单独讲。
实操时我习惯先把首页HTML保存到本地,再用文本编辑器打开,看数据在哪个标签结构里。这一步很重要,因为京东页面改版频率不低,我之前写的选择器隔了三个月就失效了,重新分析一次页面结构是常态。
2.2 构造请求头与搜索参数,避开第一波拦截
直接裸请求基本都会触发风控,第一个要处理的就是请求头。一个最基础但有效的请求头配置:
import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", "Referer": "https://www.jd.com/" } def get_page(keyword, page): url = "https://search.jd.com/Search" params = { "keyword": keyword, "enc": "utf-8", "wq": keyword, "page": page } resp = requests.get(url, params=params, headers=headers, timeout=10) resp.encoding = "utf-8" return resp.text几个容易被忽略的细节:
- User-Agent必须模拟真实浏览器的完整UA字符串,不要用默认的python-requests。
- Referer填京东首页,这符合正常浏览路径。
- timeout一定要设置,避免某个请求卡死导致整个采集流程挂掉。
- 编码统一用utf-8,页面返回的中文才不会乱码。
注意:京东搜索页的翻页参数page有个特点,第1页对应page=1,第2页对应page=3,页码按奇数递增。这个规律是京东页面的老传统,但改版后不一定完全一样,建议先手动访问第2页看URL参数再确认。
2.3 解析列表页的商品字段
拿到HTML之后,用BeautifulSoup提取商品信息。我的解析思路是先定位每个商品的外层容器,再从容器里逐字段抽取:
from bs4 import BeautifulSoup def parse_list_html(html): soup = BeautifulSoup(html, "lxml") items = [] # 每个商品的容器是 li.gl-item for li in soup.select("li.gl-item"): try: sku_id = li.get("data-sku") # 标题 title_tag = li.select_one(".p-name em") title = title_tag.get_text(strip=True) if title_tag else "" # 店铺 shop_tag = li.select_one(".p-shop a") shop_name = shop_tag.get_text(strip=True) if shop_tag else "" # 评价数 comment_tag = li.select_one(".p-commit a") comment_count = comment_tag.get_text(strip=True) if comment_tag else "0" # 详情页链接 link_tag = li.select_one(".p-name a") link = "https:" + link_tag.get("href") if link_tag else "" items.append({ "sku_id": sku_id, "title": title, "shop_name": shop_name, "comment_count": comment_count, "link": link }) except Exception as e: # 单个商品解析失败不影响整体 continue return items这里我要重点说下选择器的稳定性问题。.p-name em这类class选择器依赖京东前端的CSS类名,页面改版后类名很容易变。我在实际项目中总结出一套更抗改版的策略:
- 优先用
>import time import random def crawl_list(keyword, max_page=10): all_items = [] for page in range(1, max_page * 2, 2): html = get_page(keyword, page) items = parse_list_html(html) if not items: # 连续两页无数据就停止 break all_items.extend(items) time.sleep(random.uniform(2, 5)) return all_items第三,异常请求要记录。我习惯把每个keyword、page、请求状态码、异常信息写进日志文件,方便跑完复盘。这一步看简单,排查问题时能省好几小时。
3. 商品详情与价格数据:处理动态加载的实战经验
3.1 详情页URL的拼接规则
列表页解析出来的link字段通常是
//item.jd.com/100012345.html这种格式,补上https:前缀就能访问。详情页URL的规律非常固定:只要拿到sku_id,就能直接拼出详情页地址:def build_detail_url(sku_id): return f"https://item.jd.com/{sku_id}.html"这意味着列表页解析如果拿不到完整链接,用sku_id补全路径也一样。实际项目里我基本是两条腿走路:列表页先拿sku_id和基础字段,详情页再根据sku_id补全价格和好评率。
3.2 价格数据的动态加载与JSON接口
京东详情页的价格并不直接渲染在HTML里,而是通过异步接口加载的。这也是很多初学者卡壳的地方:用requests拿到详情页HTML,搜索"价格"两个字,发现页面里只有占位符,没有实际价格数字。
京东价格接口是独立的JSON接口,有比较固定的请求格式:
def get_price(sku_ids): """ sku_ids: 逗号分隔的多个sku_id,例如 "100012345,100067890" """ url = "https://p.3.cn/prices/mgets" params = {"skuIds": f"J_{sku_ids}"} headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://item.jd.com/" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() prices = {} for item in data: sku = item.get("id", "").replace("J_", "") prices[sku] = item.get("p") return prices这个接口返回的JSON结构里,关键字段是
p,表示售价,还有m表示原价,以及op表示历史最低参考价等。实测下来这个接口在低频请求下比较稳定。提示:价格接口虽然好用,但不要在短时间内高频调用。我测试的时候试过一秒请求一次,连续几十次后就会出现返回空数据的情况。加延时、控制调用量,是保证采集稳定的基础。
3.3 好评率与店铺信息的补充采集
好评率数据在详情页HTML里可以直接提取。打开详情页源码,搜索"好评率"附近的内容,通常能看到类似
<span class="percent-con">95%</span>的结构。用BeautifulSoup解析:def parse_detail_html(html): soup = BeautifulSoup(html, "lxml") result = {} # 好评率 percent_tag = soup.select_one(".percent-con") if percent_tag: result["good_rate"] = percent_tag.get_text(strip=True) # 店铺名称,商品页内的店铺信息模块 shop_tag = soup.select_one(".shop-name a") if shop_tag: result["shop_name"] = shop_tag.get_text(strip=True) return result详情页里还有商品规格参数、品牌、分类等更细的字段。如果你的竞品分析需要这些维度,可以在同一个详情页请求中一并解析,减少额外的请求量。
3.4 数据清洗与存储设计
采集到的数据必须规整后才能用于分析。我通常的处理流程是:
- 价格字段转数值:接口返回的价格可能是字符串,需要转成float类型。
- 评价数清理:京东评价数经常是"10万+"这种格式,要统一转成数字。
- 去重:同一个sku可能在多页列表里重复出现,采集结束后按sku_id去重。
- 缺失字段处理:部分商品可能缺店铺名或好评率,先保留空值,分析时再决定怎么处理。
存储方面,我用SQLite做中间存储:
import sqlite3 conn = sqlite3.connect("jd_data.db") conn.execute(""" CREATE TABLE IF NOT EXISTS products ( sku_id TEXT PRIMARY KEY, title TEXT, shop_name TEXT, price REAL, good_rate TEXT, comment_count TEXT, link TEXT, keyword TEXT, crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """)用SQLite的好处是采集过程中断了数据也不会丢,而且后续用pandas读取非常方便:
import pandas as pd df = pd.read_sql_query("SELECT * FROM products", conn)这一步做完,商品维度的分析数据集就齐了。
4. 评论数据采集:从JSON接口中挖出竞品口碑
4.1 定位评论数据接口
商品评论和价格一样,也是异步加载的。有个京东评论区通用的JSON接口,格式如下:
https://club.jd.com/comment/productPageComments.action?productId={sku_id}&score=0&sortType=5&page=0&pageSize=10&isShadowSku=0&fold=1参数含义:
productId:商品sku_idscore:评分筛选,0表示全部,1表示差评,2表示中评,3表示好评,5表示推荐sortType:排序方式,5表示按时间排序page:页码,从0开始pageSize:每页数量,京东限制最大10
用requests请求这个接口,返回的是JSON,里面包含三个关键部分:评论列表、商品评分摘要、评论标签聚合。
4.2 直接解析评论JSON
这个接口返回的JSON结构比较清晰,可以直接解析:
def fetch_comments(sku_id, page=0): url = "https://club.jd.com/comment/productPageComments.action" params = { "productId": sku_id, "score": 0, "sortType": 5, "page": page, "pageSize": 10, "isShadowSku": 0, "fold": 1 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": f"https://item.jd.com/{sku_id}.html" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() comments = data.get("comments", []) result = [] for c in comments: result.append({ "sku_id": sku_id, "nickname": c.get("nickname", ""), "content": c.get("content", ""), "score": c.get("score"), "product_color": c.get("productColor", ""), "product_size": c.get("productSize", ""), "comment_time": c.get("creationTime", ""), "reply_count": c.get("replyCount", 0), "useful_count": c.get("usefulCount", 0) }) return result这里的
productColor和productSize字段很有意思,可以分析用户购买的商品颜色和规格偏好,在SKU级别的竞品分析里很有用。4.3 评论翻页与去重策略
评论接口的翻页从
page=0开始,每页最多10条。理论上要抓全一个爆款商品的几千条评论,需要请求几百次,这个量级在个人学习中不太推荐。我更常用的策略是:- 每个商品采集最近50到100条评论,足以捕捉主要口碑趋势。
- 按时间排序,优先采集最新评论,反映最近的产品质量和用户反馈。
- 采集时把
comment_id字段存下来做去重标识。
写循环的时候要加两个控制逻辑:
def crawl_comments(sku_id, max_pages=10): all_comments = [] seen_ids = set() for page in range(max_pages): comments = fetch_comments(sku_id, page) if not comments: break for c in comments: c_id = c.get("id") if c_id and c_id not in seen_ids: seen_ids.add(c_id) all_comments.append(c) time.sleep(random.uniform(2, 4)) return all_comments注意:评论接口对请求频率更敏感,我实测过连续快速请求十几页就会触发限制。每个商品间隔拉长到3秒以上,整体稳定性会好很多。
4.4 基于简单规则的情感倾向初判
抓到的评论文本是纯文本,如果没做过自然语言处理,直接看几百条文本也能判断大致口碑,但不够量化。这里介绍一个不用训练模型就能用的方法:情感词词典打分法。
先准备一个简单的情感词表,正向词比如"好用、舒服、满意、值得、速度快、清晰、漂亮",负向词比如"差劲、退货、失望、卡顿、漏发、异味、差评"。然后对每条评论做分词,统计正向词和负向词出现次数,简单打分:
import jieba positive_words = set(["好用", "舒服", "满意", "值得", "速度快", "清晰", "漂亮", "稳定", "喜欢", "推荐"]) negative_words = set(["差劲", "退货", "失望", "卡顿", "漏发", "异味", "差评", "坏了", "后悔", "不好"]) def sentiment_score(text): words = jieba.lcut(text) pos = sum(1 for w in words if w in positive_words) neg = sum(1 for w in words if w in negative_words) return pos - neg这个方法精度有限,但胜在可解释性强、运行快。在竞品分析场景里,它已经足够帮你找出"口碑明显偏负面的竞品"和"被用户持续好评的竞品"。
如果你有足够的评论量和时间,可以考虑用更成熟的预训练模型做情感分类,精度会更高。但作为日常竞品分析,词典法加人工抽验已经能覆盖90%的场景。
4.5 把评论变成竞品功能口碑对比
评论数据最有价值的用法是把评论文本拆成用户关注点。我常用的方法是先对评论文本做词频统计,再用词云可视化,快速看出用户关注哪些功能点。
from wordcloud import WordCloud import matplotlib.pyplot as plt def generate_wordcloud(comments_text, output_path): wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", # 中文字体路径,按自己系统调整 width=800, height=600, background_color="white", max_words=100 ) wc.generate(" ".join(jieba.lcut(comments_text))) wc.to_file(output_path)把A、B、C三个竞品的评论词云放在一起对比,差异非常直观。比如A商品词云里"续航""轻便"出现频率高,B商品词云里"信号""售后"出现频率高,一次对比就能看出两个产品在用户心智中的差异点。
我拿这个思路做过一次真实分析,发现某个小品牌虽然没有大牌知名度,但评论里"做工好""客服耐心"出现频率极高,这就是差异化定位的机会点。
5. 竞品分析的落地输出:从数据库到可视化报告
5.1 商品价格带分布与核心SKU对比
数据采集结束后,先用pandas做基础统计。价格带分布用直方图最直观:
import pandas as pd import matplotlib.pyplot as plt # 读取数据 df = pd.read_sql_query("SELECT * FROM products", conn) # 清洗价格字段 df["price_num"] = pd.to_numeric(df["price"], errors="coerce") # 按价格排序 df_sorted = df.dropna(subset=["price_num"]).sort_values("price_num") plt.figure(figsize=(10, 6)) plt.hist(df_sorted["price_num"], bins=20, edgecolor="black") plt.xlabel("价格区间(元)") plt.ylabel("商品数量") plt.title("竞品价格带分布") plt.savefig("价格带分布.png", dpi=150)这张图能直接回答"这个品类主战场在哪个价位段"。如果是百元以内的快消品,价格带集中在低价区,说明用户对价格敏感;如果价格带分布分散,说明市场还没有出现统治级的价格锚点。
核心SKU对比表我通常做成这样:
商品标题 店铺 价格(元) 评价数 好评率 评论情感得分 商品A XX旗舰店 299 10万+ 97% +25 商品B XX专卖店 159 2.3万 91% -3 商品C XX自营 499 5.8万 95% +11 这张表是竞品分析报告的核心,所有结论都围绕它展开。
5.2 价格-评价数散点图:寻找性价比区间
价格和评价数的组合分析特别有意思。我常用散点图把竞品画在二维坐标里,横轴是价格,纵轴是评价数。散点图右上角代表"卖得贵但热度高"的商品,左下角是"既便宜又冷门"的商品。
真正值得关注的是中间区域:评价数高但价格低于同类均值的商品,这往往是性价比之王,也是潜在威胁最大的竞品。
plt.figure(figsize=(10, 6)) plt.scatter(df_sorted["price_num"], df_sorted["comment_count_num"], alpha=0.6) plt.xlabel("价格(元)") plt.ylabel("评价数") plt.title("价格与评价数分布") plt.savefig("价格评价散点图.png", dpi=150)做这步分析的时候,评价数的单位要统一。京东显示"10万+"这种格式需要先转成100000,不然画图时会因为字符串类型报错。
5.3 定时采集与增量更新
竞品分析不是一次性工作,市场变化很快,需要定期更新数据。我把上面的采集逻辑封装成脚本后,用系统的定时任务来驱动:Windows下用任务计划程序,Linux下用crontab。
设计上要注意增量和全量的选择。我的习惯是:
- 商品基础信息(标题、店铺)全量更新,因为SKU变化不大。
- 价格和评价数每次重新抓取,这两个字段变化快。
- 评论数据只增量抓取新评论,判断依据是评论时间是否已存在于数据库。
增量更新的核心代码如下:
# 获取数据库中已有的评论时间 existing_times = set(pd.read_sql_query("SELECT comment_time FROM comments", conn)["comment_time"]) new_comments = [c for c in crawled_comments if c["comment_time"] not in existing_times]加上定时后,这套系统就跑成了一个小型数据监测工具,每周自动更新一次竞品数据,报告也随之刷新。
5.4 采集过程中的常见问题与我的应对方式
跑了几个月,几个高频问题值得列出来:
问题一:列表页商品数量变少。大概率是触发了频率限制。我的处理方式是立即停止采集,休息5到10分钟再继续,同时降低单次任务的采集页数上限。
问题二:评论接口返回空数据。可能原因有两个:一是请求频率过高,二是该商品确实没有评论。判断方法是手动在浏览器里访问评论接口URL,看返回内容。浏览器能打开、脚本请求为空,就是频率问题。
问题三:价格接口返回的JSON字段缺失。上游更新导致字段名变化,需要在解析代码里加个容错,用get()方法配合默认值,而不是直接下标取值。
问题四:中文字体乱码。绘制图表和词云时,如果系统没装中文字体,保存图片会出现方框。解决方案是下载一个中文字体文件(如simhei.ttf),在代码里指定字体路径。
这些坑我全踩过,现在新写的采集脚本都会把这些容错逻辑默认带上。写爬虫的人得有一个意识:页面结构和接口永远在变,代码必须写得足够健壮,异常要能自动跳过,日志要能定位问题,数据要能恢复重跑。
6. 从这套方案延伸出去的几个想法
单纯把数据抓下来只是第一步,真正考验功力的是怎么把数据变成业务建议。
我举个例子。之前分析某个品牌的竞品时,发现某款竞品的好评率只有89%,明显低于同品类平均水平。我去翻它的评论,高频词汇集中在"发热""掉电快""售后慢"。这几个关键词直接指出了竞品的软肋。后来这个结论被我朋友用在了产品宣传策略里,主打"低温运行"和"续航持久"这两个差异点,效果相当明显。
类似的分析思路还可以继续延伸:
- 评论的时间序列分析:按月统计竞品评论量的变化,可以推测竞品的推广节奏。
- 价格波动跟踪:记录竞品价格的历史变化,判断竞品的促销周期和调价策略。
- 评论标签聚合:京东每条评论都附带用户选择的标签(如"性价比高""功能强大""外观漂亮"),直接统计标签占比,比纯文本分析更快出结论。
如果你打算把这套方案做得更工程化,可以引入Scrapy框架管理请求调度,或者用Playwright做浏览器渲染来应对复杂页面。但个人做竞品分析的话,requests加BeautifulSoup这套组合轻量、可控、容易排查问题,已经能覆盖绝大部分需求,没必要一开始就上重型框架。
最后再分享一个实用技巧:采集完成后,把所有数据导出成CSV或Excel文件,用Excel的数据透视表也能做一部分分析,不一定每个问题都要写代码。把技术工具和业务分析结合好,才是这套方案真正的价值所在。