ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从评论爬虫到情感结论:一个Python情感分析工程全解析

从评论爬虫到情感结论:一个Python情感分析工程全解析

简介:面向希望快速入门自然语言处理与文本挖掘的 Python 学习者,这份电商平台商品评论情感分析项目包整合了可直接运行的完整实现代码与说明文档,适合用于毕业设计、课程设计、大作业或工程实训等场景。压缩包内共 4 个文件,整体约 97KB,包含爬虫脚本、CSV 格式的情感分析结果、README 说明文档以及一个系统辅助文件,文件数量虽少但主流程清晰,目录结构一目了然,便于快速通读、二次修改和迁移复用。目前已有 163 人学习下载。通过脚本可了解京东评论数据的爬取与整理方式,结合输出文件观察情感分析的结果格式,配合 README 能从头梳理整体技术路线;读者可据此替换评论数据源、调整分类规则或引入更复杂的模型,在轻量框架下完成从数据采集到情感判别的完整动手实践,是快速入门情感分析项目的实用参考。

1. 项目定位:一条能直接从商品评论爬到情感结论的 python 情感分析工程

以“python 情感分析”为关键词搜到的资料,多数只有模型脚本,数据集还得自己另找。这份资源不一样:自带爬虫脚本从电商平台详情页抓评论,output.csv 里已经存好带情感标签的评论结果,解压就能看到最终输出。也就是说,从“抓原始评论”到“拿到正向负向判断”,它不依赖外部数据集,整条链路在一个包里闭环。

它正好匹配三类人:拿来做毕设、课程设计、大作业的在校学生;刚接触爬虫和自然语言处理、想跑通完整流程的新手;想研究评论采集与情感分析怎么衔接的进阶开发者。工程基于 Python3,依赖少,没有分布式组件,代码量不大,单机单脚本就能跑明白。

跑通后你能得到三层东西:一份可复用的评论采集脚本、一套基于词典打分的情感分析实现、一份字段规范的 output.csv 结果文件。下面按“结构、爬虫、分析、避坑、改造”的顺序拆解,每一章都能对上压缩包里的实际文件。

2. 项目结构与数据管道:从压缩包到 output.csv 的一条业务主线

打开压缩包别急着运行,先花十分钟把目录结构和数据形态看清楚。后面每一步操作都会对应到具体文件,排查问题时心里才有底。

2.1 解压后的目录结构:先分清哪些是代码、哪些是产物

senti_analysis-main.zip 解压后的顶层结构大致是这样:

senti_analysis-main/ ├── README.md ├── output.csv ├── detail_JDcrawler.py ├── crawler/ │ ├── __init__.py │ ├── request.py │ └── parser.py └── .DS_Store

detail_JDcrawler.py 是评论采集的入口脚本,负责向商品评论接口发请求、接收响应、把 JSON 里的评论文本解析成结构化字段。crawler/ 是辅助模块目录,常见拆分方式是:request.py 管请求构造、参数拼接和翻页,parser.py 管响应解析,把评论内容、评分、发布时间从 JSON 里抠出来。入口脚本只做编排,不把逻辑堆成一坨,这是爬虫项目里比较常见的分层习惯。

output.csv 是情感分析模块跑完后的结果文件,每条评论一行,包含原文、评分、情感分数和情感类别。README.md 里写依赖安装和运行顺序,建议先读它,确认当前环境的 Python 版本是否匹配。.DS_Store 是 macOS 的 Finder 生成目录元数据文件,和项目逻辑没有任何关系,留着不碍事;如果项目进了 git,记得在 .gitignore 里把它拦住,不然每次提交都会带一个垃圾文件。

2.2 output.csv 的数据形态:情感结论是如何落到每一行的

这个项目最直观的成果就是 output.csv。跑通之后,每行对应一条评论,常见字段如下:

字段示例说明
product_id1000123商品 SKU 编号
review_content性价比很高,物流也快评论文本原文,已去掉首尾换行
rating5用户打分,1 到 5 分
sentiment_score0.87情感分数,归一化到 -1 到 1
sentiment_label正向由分数和阈值映射出的三分类
review_time2025-03-12 10:22评论发布时间

从字段能看出两点设计意图。第一,sentiment_score 和 sentiment_label 同时保留:前者做定量分析,比如按分数排序抽极端评论;后者做定性统计,比如算正向占比。第二,原文和结论放在同一行,后续做筛选不用再去 join 别的表,pandas 读进来直接能算。

这里有个新手常踩的坑:用 Excel 直接打开 output.csv,中文全是乱码。这不是文件坏了,而是脚本用 UTF-8 写文件、Excel 用 GBK 解码,两边对不上。常见做法是写文件时用 utf-8-sig 编码,也就是带 BOM 的 UTF-8;已经乱码的文件,用pd.read_csv("output.csv", encoding="utf-8")重新读一遍再另存即可。

2.3 数据流向:原始评论、中间 CSV 与情感分析模块如何衔接

整套工程的数据流是一条从采集到标注的单链:入口脚本请求评论接口,拿到 JSON 后解析出文本、评分、时间,写入一个原始评论 CSV;情感分析模块读取这个 CSV,逐行做分词、情感打分、阈值映射,最后把 sentiment_score 和 sentiment_label 追加进去,生成 output.csv。中间 CSV 是原始数据快照,即使不交付也建议留着,日后换情感分析算法就不用重新爬一遍。

这种分层的设计有两个实际好处。其一是可独立验证:爬虫部分跑完,直接用 Excel 打开中间 CSV 就能判断评论抓得全不全;情感分析部分只依赖 CSV 文件,模型写坏了不会反向影响采集数据。其二是可替换:情感分析模块本质是一个“读 CSV、逐行打分、写回 CSV”的转换器,换成 SnowNLP、深度模型或者大模型接口,只要保持输入输出字段不变,内部实现随便动。

运行顺序也很简单:先把 detail_JDcrawler.py 跑起来拿到原始评论 CSV,再运行情感分析脚本生成 output.csv。我一般会在项目根目录建一个虚拟环境,先装依赖,避免和系统 Python 环境的其他包起冲突。

3. 核心模块拆解:评论爬虫的请求构造与解析逻辑

这一章深入 detail_JDcrawler.py。抓电商评论本质上是“接口逆向”:先定位评论接口,构造参数,发请求,再解析返回的 JSON。把这几环看明白,整段爬虫代码就通了。

3.1 请求与翻页:detail_JDcrawler.py 的入口逻辑

评论接口基本都是分页返回,入口脚本的请求构造通常是这样:

import time import requests HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", "Referer": "https://item.example.com/1000123.html", "Accept": "application/json", } def fetch_reviews(sku_id, max_page=5, page_size=20): rows = [] for page in range(1, max_page + 1): params = { "skuId": sku_id, "page": page, "pageSize": page_size, } resp = requests.get( "https://club.example.com/comment/review/list", params=params, headers=HEADERS, timeout=10, ) data = resp.json() comments = data.get("comments", []) if not comments: break rows.extend(parse_comments(comments, sku_id)) time.sleep(0.5) return rows

代码里有两个细节值得注意。params 中的 skuId 是商品唯一标识,page 从 1 开始递增,pageSize 是每页条数;每次翻页后,判断当前页评论是否为空,为空就 break。这是最简单也最实用的翻页终止条件,比硬编码“抓满 100 页”靠谱得多。

headers 里必须带 Referer,很多评论接口会校验这个字段,不带直接返回 403。time.sleep(0.5) 是主动限速,抓取频率太高容易被平台风控,0.3 到 1 秒随机间隔是常见做法;如果你要连抓几十个商品,可以把 sleep 换成random.uniform(1, 3),把请求间隔拉开。

下面是这段代码里涉及的主要参数说明:

参数例子说明
skuId1000123商品 SKU,在商品详情页 URL 里能看到
page1页码,从 1 开始
pageSize20每页评论条数,接口有上限,超过会报错
max_page5脚本允许翻的最大页数,防止死循环

3.2 评论解析:从 JSON 里提取评论文本、评分与时间

评论接口返回的 JSON 通常是{"comments": [{"content": "...", "score": 5, "creationTime": "..."}]}这种结构,解析逻辑长这样:

def parse_comments(comments, sku_id): parsed = [] for item in comments: parsed.append({ "sku_id": sku_id, "review_content": item.get("content", "").strip(), "rating": item.get("score", ""), "review_time": item.get("creationTime", ""), }) return parsed

这里用item.get("content", "")而不是item["content"],是有讲究的:如果某条评论缺失字段,中括号取法会直接抛 KeyError,整个脚本停掉;get 加默认值,最多丢掉那一条的字段,不影响整体抓取。.strip()去掉评论文本首尾的换行和空格,这是写 CSV 前的第一层清洗。

如果你实际运行后发现评论全为空,先别急着改代码,用浏览器开发者工具打开商品详情页,切到 Network 面板,刷新页面筛选 XHR 请求,找到真正返回评论的接口,对比字段名。接口字段变了是爬虫项目里最常见的事故,脚本本身可能没问题,只是平台改了返回结构。

3.3 落盘:为什么把原始评论写进 CSV 而不是直接入库

抓到的评论列表,脚本通常会直接写进 CSV:

import csv def save_to_csv(rows, filename="raw_reviews.csv"): with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter( f, fieldnames=["sku_id", "review_content", "rating", "review_time"], ) writer.writeheader() writer.writerows(rows)

用 utf-8-sig 而不是 utf-8 写文件,就是为了解决第 2 章说的乱码问题:带 BOM 后 Excel 双击直接打开不乱码。newline=""是 csv 模块的惯用写法,避免 Windows 下每两行之间多出一个空行。

为什么不直接写数据库?对一个课程设计或毕设项目来说,CSV 足够。它是开放格式,能用 pandas、Excel、文本编辑器三种方式验证数据;中间产物可读,哪里不对一眼就能看出来。如果后续数据量到几十万条、还要增量更新,再换 SQLite 也不迟,这就是最后一章要聊的二次开发方向。

4. 核心模块拆解:情感分析是怎么把评论文本变成极性分数的

爬虫拿到原始评论后,重头戏才刚开始:把“性价比很高,物流也快”这种自然语言,变成 0.87 这样的数值,再映射成“正向”。这章把分词、打分、落盘三个环节完整过一遍。

4.1 先清数据:停用词过滤与分词准备

评论文本和新闻稿不一样,短、口语化、充满“哈哈哈”“物流快”这种碎片表达。直接对整句打分误差很大,常见做法是先分词,再滤掉没有情感倾向的虚词。

import jieba STOPWORDS = {"的", "了", "是", "在", "和", "也", "就", "都", "很", "有"} def clean_text(text): words = jieba.cut(text) return [w.strip() for w in words if w.strip() and w not in STOPWORDS]

jieba.cut 会把“性价比很高”切成“性价比 / 很 / 高”,过滤掉“很”之后留下“性价比”和“高”。注意,“很”在这里被当作停用词处理了,实际上它是程度副词,严谨的情感分析会把它单独保留,用来放大后接情感词的分数。基本版先过滤,逻辑简单,也好解释。

4.2 情感打分策略:基于词典的正负词累加与归一化

最朴素也最容易答辩讲清楚的算法,是准备两个词表:正向词表、负向词表。逐词判断,正向词加一分,负向词减一分,最后除以命中词数,得到一个 -1 到 1 的归一化分数。

def load_lexicon(pos_path, neg_path): with open(pos_path, encoding="utf-8") as f: pos_words = set(line.strip() for line in f) with open(neg_path, encoding="utf-8") as f: neg_words = set(line.strip() for line in f) return pos_words, neg_words def sentiment_score(text, pos_words, neg_words): words = clean_text(text) score = 0 hit = 0 for w in words: if w in pos_words: score += 1 hit += 1 elif w in neg_words: score -= 1 hit += 1 if hit == 0: return 0.0 return score / hit

pos_words 和 neg_words 用 set 存储,成员判断是 O(1),评论短、词量小,跑起来很快。分母用 hit 而不是整句词数,是为了避免“好好好好好”这种泛泛文本因为总词数多把分数稀释得太低。返回值落在 -1 到 1 区间,正数偏正向,负数偏负向,0 是中性和未命中情感词。

这种方案其实不玄学,所有判分逻辑都能拿到桌面上讲:哪句话、哪个词命中了哪个词表,一查便知。缺点是词表不全时漏判明显,需要根据评论语料持续补充。实际项目里,可以下载通用的中文情感词典,也可以自己整理电商场景的高频情感词,比如“物流、售后、回购、踩雷”这类的。

补充一点对比:如果想省掉词表维护,常见做法是直接用现成库做情感判断,比如 SnowNLP 的 sentiments 属性,输出 0 到 1 的概率,大于 0.5 偏向正向。它的优点是开箱即用,缺点和基于词典的思路分数区间不一致,融合时得先归一化。我一般建议做课程设计时选词典方案,答辩好讲;做进阶论文再叠加一个预测模型做对比实验。

4.3 分数到标签:阈值映射与结果写回 output.csv

拿到分值后,需要把它映射成“正向 / 中性 / 负向”三分类:

def to_label(score, pos_threshold=0.1, neg_threshold=-0.1): if score > pos_threshold: return "正向" if score < neg_threshold: return "负向" return "中性"

两个阈值取正负 0.1,含义是情绪不够强烈就不表态。阈值可以调:调大,中性比例变高,结论保守;调小,正负极更激进。通常我会先跑一版,抽样看 20 条人工核对一遍,再根据偏差去调整阈值或扩充词表。

写回 output.csv 有两种方式:一种是把情感列直接 append 到原 CSV 的每一行,另一种是读中间 CSV、计算、再写出新文件。后者的代码更清晰:

import pandas as pd df = pd.read_csv("raw_reviews.csv", encoding="utf-8") df["sentiment_score"] = df["review_content"].apply( lambda x: sentiment_score(x, pos_words, neg_words) ) df["sentiment_label"] = df["sentiment_score"].apply(to_label) df.to_csv("output.csv", index=False, encoding="utf-8-sig")

pandas 的 apply 对每一行评论调用打分函数,逐行算完再映射标签,最终 DataFrame 保留原始字段,只多了 sentiment_score 和 sentiment_label。生成的 output.csv 与第 2 章的字段设计完全对上,到这一步整个情感分析链路就通了。

5. 常见问题与避坑:跑这套工程最常踩的 5 个坑

这一章算是血泪经验,提前知道这些坑,能省下好几个晚上的调试时间。

5.1 乱码:Excel 打开 output.csv 全是“锟斤拷”

现象:脚本跑完,Excel 直接打开 agg 是集体乱码,记事本打开却正常。
原因:文件用 UTF-8 写入,Excel 在 Windows 默认按 GBK 解码,两边编码对不上。
解决:把写文件的编码从 utf-8 换成 utf-8-sig;已经产生的乱码文件,用pd.read_csv("output.csv", encoding="utf-8")读进来重新保存一份。今后凡是写 CSV 给业务方看,我一律用 utf-8-sig。

5.2 空列表:爬虫跑完,评论列表一条都没有

现象:脚本不报错,但原始评论 CSV 只有表头,没有数据。
原因:平台的评论接口地址或返回字段变了,data.get("comments")取不到内容,返回空列表直接被翻页循环 break 终止。
解决:打开浏览器开发者工具的 Network 面板,找到真实的评论请求,比对 URL 结构和 JSON 字段,更新脚本里的解析逻辑。评论接口和商品页接口通常不在同一个域名下,得仔细翻 XHR 请求列表。

5.3 全是中性:情感分析结果区分度太低

现象:output.csv 里情感标签集中在“中性”,正向负向没几条。
原因:有三种可能——情感词表太小;jieba 分词把“性价比”这类词切碎导致情感词没命中;阈值设置太高。
解决:先调阈值,把正负阈值从 0.1 降到 0.05,看中性比例是否下降;再用jieba.lcut()打印几条评分的分词结果,人工检查切词是否正确;最后扩充词表,把评论语料里的高频褒贬词加进去,这一步对效果提升最明显。

5.4 被限流:抓了几十页后返回 403

现象:前面几页正常,翻到后面突然 403,或者频繁跳验证码。
原因:请求频率超过了平台单 IP 的访问限制,风控开始拦截。
解决:把 sleep 间隔调大并加入随机性,time.sleep(random.uniform(1, 3));每次请求轮换 User-Agent;如果任务量大,再考虑代理 IP 池,但对课程设计来说把间隔拉大就够了。

5.5 .DS_Store 干扰:解压后多了一个不明文件

现象:解压目录里有个 .DS_Store 文件,用 git 提交代码时也要把它提交上去,代码仓库看起来很不干净。
原因:.DS_Store 是 macOS Finder 浏览目录时自动生成的元数据文件,跟项目没有任何关系。
解决:直接删除;如果项目在 git 里,在 .gitignore 加一行.DS_Store,以后就不再误提交。类似的还有 Windows 下的 Thumbs.db,处理方式相同。

6. 二次开发:把一次性分析脚本改成周期运行的评论监控工具

跑通原项目只算拿到一个合格基线。真正能放进简历或作为毕设主体的,通常是把它改造成有扩展性的小工具。这里给三个改造方向,任选其一都能让项目脱胎换骨。

第一个方向:把单商品分析改成多商品批量分析。在入口脚本外面包一层循环,sku 列表从文件或数据库读取,每个商品抓完存一份独立 CSV,最后汇总一个跨商品的情感对比表。代码量增加很小,但项目能回答的问题从“这个商品口碑怎么样”变成“哪些商品差评最多、为什么”。

第二个方向:把纯词典打分升级成“词典 + 现成模型”融合。用 SnowNLP 这类库跑一遍,得到 0 到 1 的情感概率,再和词典分数加权平均,比如final = 0.6 * dict_score + 0.4 * (snownlp_score * 2 - 1)。代码改动不超过十行,准确率通常会有可感知的提升,还能在报告里写一组算法对比实验。

第三个方向:给差评提取高频问题词。从 output.csv 里筛出情感标签为“负向”的评论,用 jieba 分词后做词频统计,去掉停用词,取 Top 20。输出结果往往就是“配送慢、客服差、货不对板”这类关键词,商品运营拿到就能直接改进,也是答辩时最容易讲出价值的点。

验证方法很简单:随机抽 50 条评论,人工标注成正向、中性、负向,用改造后的代码重跑一遍,算出情感分类准确率,把这个数字写进 README,作为项目效果的量化结论。我一般会让两个版本跑同一份 raw_reviews.csv,对比新旧 output.csv,而不是改了代码就直接上全量数据。

改造时有个习惯想分享:每次调整词表、模型或阈值,都要用同一份原始数据重跑,再做输出对比。这样新引入的误判会明明白白显示出来。从那以后,凡遇到情感分析项目,我都会强制走一遍“原始数据固定、只改模型、对比输出”的流程,这算是一路踩坑攒下来的后悔药。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进