☰
Python爬虫京东评论分析:从JSON采集到情感可视化
2026/9/27 23:59:30 网站建设 项目流程

简介:这是一套基于Python爬虫的京东商品评论采集与分析系统,覆盖文本情感分析和可视化展示,适合计算机、人工智能、电商数据挖掘等方向的学生用于毕业设计或课程实践,也可作为初学者进阶NLP项目的参考。压缩包共113个文件,大小约55.81MB,以15个Python脚本为核心,搭配37个CSV格式评论数据集、30张JPG和8张PNG可视化结果图、LSTM模型文件及说明文档等,完整涵盖数据采集、清洗、训练与结果展示流程。目前已有81人学习浏览。内含完整源代码与配套设计文档,并附多组真实京东评论样本及情感分类模型,可直接运行演示;有基础的用户可在此基础上二次开发,拓展更多功能模块。针对部署和运行中遇到的问题,可与作者交流并获得远程协助或技术指导。

1. Python爬虫京东商品评论分析:先把链路想清楚再拆包

看到「Python爬虫京东商品评论分析系统:文本情感分析+可视化.zip」这个标题,很多人的第一反应是找源码、跑命令。这类免费Python源码包在网上并不少见,但能一次跑通的不多。真正值得拆解的不是文件列表,而是这套流程:京东评论不是静态HTML而是JSON接口,文本情感分析在中文场景下不能直接套英文模型,可视化要把数据库里的数字变成能汇报的大屏图表。

三件事串起来,就是一个从采集、清洗、建模到展示的完整闭环。这个方向适合正在学Python爬虫和数据分析的入门者,也适合需要快速了解某款商品口碑的运营人员。我的建议是别急着点开zip,先把链路想清楚再动手。

2. 从商品ID到评论文本:京东评论采集链路的三层拆解

先说环境:Python 3.9以上加MySQL 5.7以上是最省心的组合。如果你还没装Python,去官网下载安装包时记得勾选Add to PATH,后面所有命令才能直接用python而不是python3。MySQL装完要确认服务能启动,连接串里的账号密码要和本地一致。

2.1 先探JSON接口:京东评论的懒加载数据藏在哪

打开任意一个京东商品页,按F12进入Network面板,刷新页面后把评论区往下拉,能看到一个名为productPageComments.action的请求。这个请求就是评论数据的入口,返回的是标准JSON而不是HTML。评论是懒加载的,页面初始HTML里只有商品信息,评论文本全部靠这个接口异步拉取。

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", "Referer": "https://item.jd.com/100012043978.html", } def fetch_comments(product_id, page=0, page_size=20): url = "https://club.jd.com/comment/productPageComments.action" params = { "productId": product_id, "score": 0, "sortType": 5, "page": page, "pageSize": page_size, "isShadowSku": 0, "fold": 1, } # 评论接口返回JSON,拿不到comments说明被风控了 resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() return data.get("comments", []) if __name__ == "__main__": comments = fetch_comments("100012043978", page=0, page_size=10) for c in comments: print(c["content"][:30], c["creationTime"], c["score"])

这段代码有三个参数值得解释。score=0表示取全部评论,抓差评时改成score=1,抓中评改成score=2;sortType=5是默认排序,sortType=6是按时间排序,采集最新评论时用6更合理;page从0开始,偏移量是page乘pageSize。

Referer字段必须带,它是京东服务端校验来源的第一步,缺失时大概率被拒。跑通接口后你会看到每个comment节点都有content、creationTime、score、nickName字段。creationTime是毫秒时间戳,不要直接入库,第5章会专门说这个坑。

2.2 触发反爬时补上Selenium:Cookie复用与滑块兜底

requests接口跑得很顺,但连续翻几十页后返回的不再是JSON,而是一段JS校验代码。这是京东的频控策略,本质是对请求频率、来源、Cookie的综合判断。最稳的做法是降频、带Cookie、随机延时,如果还是被拦,就上Selenium模拟真实浏览器滚动评论区,让服务端认为你是正常用户。

from selenium import webdriver import time options = webdriver.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options) driver.get("https://item.jd.com/100012043978.html") time.sleep(3) # 滚动到页面底部,触发评论懒加载请求 driver.execute_script("window.scrollTo(0, document.body.scrollHeight)") time.sleep(2) # 导出Cookie给requests复用 cookies = driver.get_cookies() cookie_str = "; ".join(f"{c['name']}={c['value']}" for c in cookies) print(cookie_str)

Selenium在这里不是直接拿JSON,而是拿登录态和Cookie。把cookie_str拼到requests的headers里,请求被拦的概率会显著降低。注意Chrome 111以后的版本会通过Selenium Manager自动匹配driver,但如果你用的是公司内网或离线环境,还是要手动下载对应版本的chromedriver,这个环节踩坑的人非常多。

滚动加载有一个细节:京东评论区不是一次全量渲染,滚到页尾后要等1到2秒让XHR返回。sleep时间太短,页面还没发起请求,导出的Cookie可能是残缺的。建议滚动两次,第一次滚到底,第二次滚回评论区位置,确保评论列表真实渲染过。

2.3 SQLAlchemy建三张表:商品、评论、情感结果怎么落库

采集到的数据和后续情感分析结果要落到数据库。我一般用MySQL加SQLAlchemy的ORM,因为后期做情感标注和聚合查询时,ORM的对象操作比裸写pymysql游标舒服得多。

from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Float from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class Product(Base): __tablename__ = "jd_product" id = Column(Integer, primary_key=True, autoincrement=True) product_id = Column(String(32), unique=True, index=True) title = Column(String(255)) shop = Column(String(255)) class Comment(Base): __tablename__ = "jd_comment" id = Column(Integer, primary_key=True, autoincrement=True) product_id = Column(String(32), index=True) content = Column(Text) creation_time = Column(DateTime) score = Column(Integer) nickname = Column(String(128)) sentiment_score = Column(Float, default=0.5) sentiment_label = Column(String(8), default="neutral") engine = create_engine( "mysql+pymysql://root:yourpassword@localhost:3306/jd_review?charset=utf8mb4" ) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)

建表时三个参数别写错。charset=utf8mb4是中文不乱码的关键,少这个后缀中文会变问号;product_id加索引,因为查询评论几乎都按这个字段过滤;sentiment_score和sentiment_label先给默认值,情感分析跑完后原地更新,不用重建表。

我把情感结果直接放在jd_comment表里而不是单独建表,原因是评论和情感结果是一对一的,拆开只会增加多余的联表查询。如果以后想做多模型对比,再建一张sentiment_result表也不迟。SQLAlchemy储存爬虫数据这个组合在个人项目里足够,不用一开始就上重型框架。

3. 文本情感分析:SnowNLP打分与京东评论的预处理细节

3.1 SnowNLP跑通最小样例:阈值怎么定更合理

京东评论的情感分析,我推荐先用SnowNLP。它是纯Python的中文情感分析库,模型基于电商购物评论训练,对"便宜、好用、物流快"这类口语表达判断得比较准,没有复杂依赖。对比之下,BERT系列效果更强但部署环境重,练手项目没必要一上来就上。

from snownlp import SnowNLP text1 = "物流很快,包装完好,手机用起来很流畅,性价比很高" text2 = "用了一周就卡死,客服态度差,退货流程超繁琐" print(SnowNLP(text1).sentiments) # 约0.93 print(SnowNLP(text2).sentiments) # 约0.08

SnowNLP输出0到1的浮点数,越接近1越正向。阈值怎么定有很多说法,我跑过几百条京东评论后的经验是:大于等于0.6归正向,小于等于0.4归负向,中间为中性。只用0.5当分界线会把大量无倾向的评论错分到正向,而0.6和0.4这个区间给模糊短评留了缓冲。

注意:0.6和0.4是基于电商评论语料的经验值,换商品品类后建议重新做一批人工标注验证,不要照搬。

要清楚SnowNLP的底层是朴素贝叶斯分类器,对你来说是个黑匣子:它只给出概率,不解释原因。遇到明显误判,不要硬改模型,而是用规则或自定义词典兜底,这在后面会展开。

3.2 京东评论的三个预处理步骤:表情、追评、短评

京东评论的原始文本比想象中脏,最典型的三类是表情占位符、未填写内容占位、极短评语。表情以[高兴]、[流泪]这类方括号字符串存在;部分用户没有填写内容,系统自动填了"此用户未填写评价内容";还有不少评论只有"好评"两个字,SnowNLP基本无法判断。

import re def clean_comment(text): if not text or "此用户未填写评价内容" in text: return "" text = re.sub(r"\[.*?\]", "", text) text = re.sub(r"<.*?>", "", text) text = re.sub(r"\s+", "", text) return text.strip() def analyze(text): cleaned = clean_comment(text) if len(cleaned) < 3: return 0.5, "neutral" score = SnowNLP(cleaned).sentiments if score >= 0.6: label = "positive" elif score <= 0.4: label = "negative" else: label = "neutral" return score, label

预处理函数有三个细节。正则\[.*?\]匹配JD表情占位符,用非贪婪匹配避免一次吞掉多个表情;长度小于3的文本直接标中性,因为"好评""一般"这种词交给SnowNLP,概率会随机漂移,标中性比强行判断更诚实;追评内容里同时包含原始评价和追加评价时不需要刻意拆分,合并清洗后分析可以反映用户的最终态度。

如果想更稳,可以把标点和数字也去掉,但要注意去掉数字会影响"第3天就坏了"这类时间状语的情感权重,我选择保留。

3.3 情感结果落库与统计口径

清洗和分析后的结果写回数据库。常见做法是批量读取未处理的评论,逐条更新sentiment_score和sentiment_label。

from sqlalchemy.orm import sessionmaker Session = sessionmaker(bind=engine) session = Session() for comment in session.query(Comment).filter(Comment.sentiment_label == "neutral").all(): score, label = analyze(comment.content) comment.sentiment_score = score comment.sentiment_label = label session.commit()

每次循环单独commit会慢,可以改成每50条commit一次,减少事务开销。但好处是崩溃后已处理的不丢,可以断点续跑。

落库后统计口碑时不要只看情感平均分。更合理的口径是分别看正向、负向、中性的占比,同时把用户打星(score字段1到5分)和情感分析结果分开统计。有人给3星但文字写"还可以吧",SnowNLP可能判成正向,这时以文字为准还是星级为准取决于分析目的。做商品缺陷挖掘时优先看负向评论里的高频词,做总体口碑评级时用星级和情感标签交叉验证更可靠。

4. 可视化大屏:Flask + ECharts把评论情绪变成能汇报的图

4.1 聚合查询:从MySQL掏出情感分布和时间趋势

可视化不是把全量评论丢给前端,而是先在SQL层聚合。常见做法是写两个接口,一个返回情感占比,一个返回时间趋势。如果你的目标是可视化大屏,这两个接口就够撑起主面板了。

from flask import Flask, jsonify, request from sqlalchemy import create_engine app = Flask(__name__) engine = create_engine("mysql+pymysql://root:yourpassword@localhost:3306/jd_review?charset=utf8mb4") @app.route("/api/sentiment_dist") def sentiment_dist(): product_id = request.args.get("pid", "100012043978") with engine.connect() as conn: rows = conn.execute( "SELECT sentiment_label, COUNT(*) FROM jd_comment " "WHERE product_id = :pid GROUP BY sentiment_label", {"pid": product_id} ).all() return jsonify([{"name": row[0], "value": row[1]} for row in rows]) @app.route("/api/trend") def trend(): product_id = request.args.get("pid", "100012043978") with engine.connect() as conn: rows = conn.execute( "SELECT DATE(creation_time) AS d, AVG(sentiment_score) AS avg_s, COUNT(*) AS cnt " "FROM jd_comment WHERE product_id = :pid " "GROUP BY DATE(creation_time) ORDER BY d", {"pid": product_id} ).all() return jsonify([ {"day": str(row[0]), "avg_score": round(float(row[1]), 3), "cnt": row[2]} for row in rows ]) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)

两个接口都走SQL聚合而非Python内存计算。原因很直接:评论量到几万条后,把全量数据塞进内存再统计是个糟糕方案。DATE(creation_time)做日维度聚合,AVG算每日情感均值,前端折线图直接消费即可。

这里有个小坑:要记得import request。另外engine.connect()返回的是Connection对象,用with语句确保连接自动释放。Flask默认的debug模式不要开,它会暴露调试器和Python栈信息,部署时是安全隐患。

4.2 ECharts折线图、饼图与词云的中文字体参数

前端用ECharts做展示。情感分布用环形饼图,每日情感均值用折线图,热门词用词云。三类图对应三组配置参数。

fetch("/api/sentiment_dist?pid=100012043978") .then(res => res.json()) .then(data => { echarts.init(document.getElementById("pie")).setOption({ tooltip: { trigger: "item" }, legend: { bottom: 0 }, series: [{ type: "pie", radius: ["40%", "70%"], data: data, label: { formatter: "{b}: {d}%" } }] }); }); fetch("/api/trend?pid=100012043978") .then(res => res.json()) .then(data => { echarts.init(document.getElementById("line")).setOption({ tooltip: { trigger: "axis" }, xAxis: { type: "category", data: data.map(d => d.day) }, yAxis: { type: "value", min: 0, max: 1 }, series: [{ type: "line", data: data.map(d => d.avg_score), smooth: true, areaStyle: {} }] }); });

环形饼图的关键是radius: ["40%", "70%"],代表内圆半径40%、外圆70%,label的formatter里{d}显示占比,适合汇报。折线图的smooth: true让曲线平滑,areaStyle: {}填充面积,视觉上更接近趋势感。yAxis的min和max固定为0和1,因为情感分数就是这个区间,不固定的话图表会自动留白,让波动显得比实际大,这在汇报时容易被质疑。

词云我习惯用Python的wordcloud生成图片而不是前端组件,少引依赖。中文字体是重灾区:

from wordcloud import WordCloud import matplotlib.pyplot as plt wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", width=800, height=400, background_color="white", max_words=100 ).generate(" ".join(top_words)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig("wordcloud.png", dpi=150)

font_path必须指向一个支持中文的字体文件。Windows下通常是C:/Windows/Fonts/simhei.ttf,Linux下要改成系统里Noto Sans CJK的实际路径。不指定字体,生成的图片里中文全是方块。如果想在Flask里展示,把图片存到static目录,前端img标签直接引用。

在ECharts页面上,如果还想加实时刷新,在setOption外面套一层setInterval每隔5分钟重新fetch。这类轻量轮询在个人可视化项目中已经够用,比WebSocket简单得多,并且不容易踩连接泄漏的坑。

5. 京东评论爬虫的避坑指南:反爬、编码与时间戳翻车现场

5.1 抓回的不是JSON而是JS代码

现象:requests连续请求几十页后,resp.json()抛JSONDecodeError。打印返回内容发现不是JSON,而是一段带script标签的JS校验代码,甚至页面上会出现滑块组件。

原因:京东对评论接口有频控,判断维度包括单IP单位时间请求次数、请求头完整度、Cookie有效性。短时间高频请求会直接触发风控。

解决:采用三重降级。第一,循环里用随机延时,不要固定sleep(2),而是random.uniform(1.5, 3.5);第二,把Selenium导出的Cookie拼进requests头,绕过匿名风控;第三,写一个带重试的请求函数,捕获JSONDecodeError之后换UA等待重试。注意重试次数不要超过3次,无限重试会把IP送进黑名单。

import random, time def fetch_with_retry(url, params, headers, retries=3): for attempt in range(retries): try: resp = requests.get(url, params=params, headers=headers, timeout=10) return resp.json() except requests.exceptions.JSONDecodeError: wait = random.uniform(2, 4) * (attempt + 1) time.sleep(wait) headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0 Safari/537.36" return None

这个函数里wait随着重试次数递增,避免在一瞬间多次撞墙。requests默认不会在响应后主动检查是不是JSON,所以这类错误全靠自己捕获。

5.2 中文入库全变问号

现象:评论抓回来了,Python打印正常,但MySQL表里全是??。

原因:字符集没对齐。可能有三个位置出错:数据库本身是latin1、表是latin1、SQLAlchemy连接串没带charset=utf8mb4。任何一个都会导致中文被转成问号。

解决:先排查再修复。执行两条SQL确认现场,然后把库和表的字符集都改过来。已经存在的乱码数据没有后悔药,只能删掉重抓,所以建表前先确认连接串。

SHOW CREATE TABLE jd_comment; ALTER DATABASE jd_review CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE jd_comment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

注意ALTER TABLE CONVERT和MODIFY是有区别的。CONVERT会改变整张表的默认字符集并转换现有数据,MODIFY只改列定义。这里应该用CONVERT。

5.3 "还行""一般"被识别成正向

现象:"一般般吧""还行吧""凑合"这些词,SnowNLP给出的sentiments往往在0.7以上,被错误划为正向。

原因:SnowNLP是朴素贝叶斯模型,训练语料里这类词通常伴随中性或偏正向的上下文,模型对单个词的推断存在惯性偏差。

解决:维护一个模糊语义规则表,命中短语后用规则覆盖模型输出。规则表不是一次建好的,要在实际样本里不断积累,这个项目的真正价值在规则表的迭代上。

FUZZY_RULES = { "一般般": "neutral", "还行": "neutral", "凑合": "neutral", "没有想象中好": "negative", "一分钱一分货": "neutral", } def analyze_with_rules(text): for phrase, label in FUZZY_RULES.items(): if phrase in text: return 0.5, label return analyze(text)

注意"一分钱一分货"标中性其实有争议,在某些场景里它可能是正向。规则表要根据具体商品调整,建议把规则表独立成一个py文件,方便不同项目复用。

5.4 评论时间显示成1970年

现象:评论采集正常,但图表里的时间轴出现1970-01-01,或者时间比实际早8个小时。

原因:京东评论接口返回的creationTime是毫秒级时间戳,你把它当成秒级处理了。时区偏差则是MySQL会话时区设置为系统时区而Python用了UTC。

解决:在Python侧统一转换。毫秒时间戳通常大于10的12次方,可以用这个特征判断再除以1000。时区上建议用datetime.fromtimestamp(ts, tz=timezone.utc)存UTC,展示时再转本地,避免服务器时区不一致导致图表错乱。

from datetime import datetime, timezone def convert_ts(ts_millis): if ts_millis > 10**12: ts_millis = ts_millis / 1000 return datetime.fromtimestamp(ts_millis, tz=timezone.utc)

不要依赖MySQL的FROM_UNIXTIME,它默认按秒处理,毫秒值进去会得到诡异日期。

5.5 词云图全是方块

现象:词云图生成了,但中文全是小方块,英文正常。

原因:wordcloud默认字体是DroidSansMono,不包含中文字形。

解决:显式指定font_path。Windows用C:/Windows/Fonts/simhei.ttf,macOS用/System/Library/Fonts/PingFang.ttc,Linux要先用fc-list查一下系统有哪些中文字体。我一般在代码里先检查字体文件是否存在,不存在就抛一个能看懂的错误,而不是让matplotlib画一堆方块。

import os FONT_PATHS = [ "C:/Windows/Fonts/simhei.ttf", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", ] font_path = next((p for p in FONT_PATHS if os.path.exists(p)), None) if not font_path: raise FileNotFoundError("未找到中文字体,请通过 fc-list 确认系统字体")

这个思路同样适用于matplotlib的其他中文显示问题,比如图表标题里的中文。

6. 验证与进阶:情感准确率怎么评估,数据量大了往哪走

6.1 人工标注100条输出准确率与混淆矩阵

情感分析不能只看样例输出,最稳的评估方式是人工标注测试集。挑100条评论,自己标好positive、neutral、negative,再拿analyze_with_rules的预测结果对比,输出分类报告和混淆矩阵。

from sklearn.metrics import classification_report, confusion_matrix labels = [...] # 人工标注 preds = [...] # analyze_with_rules 的结果 print(classification_report(labels, preds, target_names=["negative", "neutral", "positive"], digits=3)) cm = confusion_matrix(labels, preds) print(cm)

如果negative被大量误判成neutral,说明0.4的阈值对这个商品不够灵,可以把负向阈值上调到0.45并重新评估。调SnowNLP的阈值在某种程度上像玄学,但混淆矩阵能告诉你往哪个方向调。人工标注100条大概花30分钟,这点时间省不得。

6.2 从单机到队列:Redis缓存与分布式采集的取舍

当需要跟踪多个商品的长周期口碑时,单机顺序采集会越来越吃力。常见做法是引入Redis做两件事:用SETNX对评论ID去重,防止重复入库;用列表存放待采集的商品ID,起多个进程消费。Redis的可视化管理可以装一个客户端工具看队列积压情况,比命令行直观得多。

注意:Redis在这个项目里只做简单队列,不需要上哨兵或集群,单机默认配置足够。

这个阶段要评估收益。对个人分析项目,单机加延时基本够用;真正要上分布式时,你会先遇到反爬限制而不是性能瓶颈,所以优先优化的是采集频率和去重策略,而不是机器数量。

做这个项目一路踩下来,我最大的教训是:先拿一两件商品把采集、情感、可视化整条链路跑通,再谈扩量。很多人一上来就全量爬,结果反爬被限制、情感误判一片、图表失真,项目直接烂尾。先小规模验证阈值,再放开量,这条路会顺得多。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询