简介这是一套基于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, page0, page_size20): 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, paramsparams, headersheaders, timeout10) data resp.json() return data.get(comments, []) if __name__ __main__: comments fetch_comments(100012043978, page0, page_size10) for c in comments: print(c[content][:30], c[creationTime], c[score])这段代码有三个参数值得解释。score0表示取全部评论抓差评时改成score1抓中评改成score2sortType5是默认排序sortType6是按时间排序采集最新评论时用6更合理page从0开始偏移量是page乘pageSize。Referer字段必须带它是京东服务端校验来源的第一步缺失时大概率被拒。跑通接口后你会看到每个comment节点都有content、creationTime、score、nickName字段。creationTime是毫秒时间戳不要直接入库第5章会专门说这个坑。2.2 触发反爬时补上SeleniumCookie复用与滑块兜底requests接口跑得很顺但连续翻几十页后返回的不再是JSON而是一段JS校验代码。这是京东的频控策略本质是对请求频率、来源、Cookie的综合判断。最稳的做法是降频、带Cookie、随机延时如果还是被拦就上Selenium模拟真实浏览器滚动评论区让服务端认为你是正常用户。from selenium import webdriver import time options webdriver.ChromeOptions() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions) 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_keyTrue, autoincrementTrue) product_id Column(String(32), uniqueTrue, indexTrue) title Column(String(255)) shop Column(String(255)) class Comment(Base): __tablename__ jd_comment id Column(Integer, primary_keyTrue, autoincrementTrue) product_id Column(String(32), indexTrue) content Column(Text) creation_time Column(DateTime) score Column(Integer) nickname Column(String(128)) sentiment_score Column(Float, default0.5) sentiment_label Column(String(8), defaultneutral) engine create_engine( mysqlpymysql://root:yourpasswordlocalhost:3306/jd_review?charsetutf8mb4 ) Base.metadata.create_all(engine) Session sessionmaker(bindengine)建表时三个参数别写错。charsetutf8mb4是中文不乱码的关键少这个后缀中文会变问号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.08SnowNLP输出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(bindengine) 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(mysqlpymysql://root:yourpasswordlocalhost:3306/jd_review?charsetutf8mb4) 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(host0.0.0.0, port5000, debugFalse)两个接口都走SQL聚合而非Python内存计算。原因很直接评论量到几万条后把全量数据塞进内存再统计是个糟糕方案。DATE(creation_time)做日维度聚合AVG算每日情感均值前端折线图直接消费即可。这里有个小坑要记得import request。另外engine.connect()返回的是Connection对象用with语句确保连接自动释放。Flask默认的debug模式不要开它会暴露调试器和Python栈信息部署时是安全隐患。4.2 ECharts折线图、饼图与词云的中文字体参数前端用ECharts做展示。情感分布用环形饼图每日情感均值用折线图热门词用词云。三类图对应三组配置参数。fetch(/api/sentiment_dist?pid100012043978) .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?pid100012043978) .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_pathC:/Windows/Fonts/simhei.ttf, width800, height400, background_colorwhite, max_words100 ).generate( .join(top_words)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(wordcloud.png, dpi150)font_path必须指向一个支持中文的字体文件。Windows下通常是C:/Windows/Fonts/simhei.ttfLinux下要改成系统里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, retries3): for attempt in range(retries): try: resp requests.get(url, paramsparams, headersheaders, timeout10) 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连接串没带charsetutf8mb4。任何一个都会导致中文被转成问号。解决先排查再修复。执行两条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, tztimezone.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, tztimezone.utc)不要依赖MySQL的FROM_UNIXTIME它默认按秒处理毫秒值进去会得到诡异日期。5.5 词云图全是方块现象词云图生成了但中文全是小方块英文正常。原因wordcloud默认字体是DroidSansMono不包含中文字形。解决显式指定font_path。Windows用C:/Windows/Fonts/simhei.ttfmacOS用/System/Library/Fonts/PingFang.ttcLinux要先用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], digits3)) 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在这个项目里只做简单队列不需要上哨兵或集群单机默认配置足够。这个阶段要评估收益。对个人分析项目单机加延时基本够用真正要上分布式时你会先遇到反爬限制而不是性能瓶颈所以优先优化的是采集频率和去重策略而不是机器数量。做这个项目一路踩下来我最大的教训是先拿一两件商品把采集、情感、可视化整条链路跑通再谈扩量。很多人一上来就全量爬结果反爬被限制、情感误判一片、图表失真项目直接烂尾。先小规模验证阈值再放开量这条路会顺得多。希望帮到你。本文还有配套的精品资源点击获取
