
简介这是一份基于Python爬虫的豆瓣影评分析系统本科毕业论文设计文档面向计算机科学与技术、大数据相关专业学生及爬虫与文本分析初学者。资源以豆瓣电影影评为切入点从爬虫基本原理入手介绍requests、BeautifulSoup、Scrapy等常用爬虫库的选型与使用并讲解API调用、模拟登录、动态IP等反爬应对方法在数据处理环节涵盖数据清洗、MySQL/MongoDB存储、情感分析与关键词统计结合Matplotlib、Seaborn实现可视化最后给出系统需求分析、前后端架构设计与测试流程。包体为1个docx文件压缩包约32KB已有1246人学习。文档含六章完整目录从绪论、技术基础到系统实现与总结结构清晰并带有西南财经大学学士学位论文格式可作为毕业设计排版与写作参照。通过学习可直接复用爬虫框架选型、反爬应对思路、影评情感分析及可视化方案节省从零调研的时间尤其适合作为本科项目或论文初稿的蓝本。1. 基于python爬虫的豆瓣影评分析系统到底要解决什么不止是抓评论而是把脏数据变成可用结论豆瓣影评分析系统是Python爬虫选题里被提到最频繁的方向之一但真正跑通的人不多。多数项目在第二周就翻车爬虫能抓到页面清洗后发现评论里塞满“有用”“来自豆瓣”这类残留字段情感分析的结果又看不出和评分有什么相关性。这个系统真正难的不是“爬”而是把采集、存储、清洗、分析、展示串成一条稳定链路并让最终结论能解释“这部电影的口碑到底如何”。这套思路适合课程设计、个人作品集也适合想系统走一遍完整数据流程的从业者。本文按一条不绕弯的路径展开先定数据模型再做爬虫再做清洗与情感分析最后可视化并验证结果。2. 系统设计与数据模型先定边界再写第一行爬虫代码做这个方向最常见的坑是拿到选题就写爬虫脚本一撸到底最后发现数据字段对不上分析需求返工成本极高。我一般会先用半小时把系统切成四个模块明确每个模块的输入输出再动手写代码。2.1 模块边界怎么切爬虫、存储、清洗、分析、展示整个系统按数据流向分成四层每层只依赖前一层的结果不跨层操作。爬虫模块负责把网页上的短评原文、评分、“有用”数、发布时间抓下来输出统一格式的字典列表存储模块负责去重并持久化到数据库清洗与分析模块从库里读数据做分词和情感打分展示模块只读分析结果不关心数据怎么来的。这样切的好处是出问题时能单独替换某层比如换存储引擎、换情感分析算法不用动爬虫代码。模块边界一旦定了爬虫只需要回答一个问题我负责产出哪些字段。接下来的分析模块需要三个维度的数据文本内容、数值评分、时间信息。文本是最核心的情绪来源评分数值用来和情感打分做对照验证时间字段则用来观察口碑随上映周期的变化。抓取时多存一个“有用”数也不亏它是短评权重的天然指标后面排序和过滤低质量评论都会用到。用 SQLite 还是 CSV 是这个项目里被反复纠结的问题。如果数据量在几千条、单机演示级别SQLite 比 CSV 更适合。CSV 的优点是打开方便但多字段更新、按条件筛选时非常笨拙而且 Python 进程崩溃时 CSV 容易写出半行脏数据。SQLite 是单文件数据库启动无服务、查询走 SQL也可以直接把分析结果写回表里便于后一步可视化直接聚合。2.2 影评数据模型与SQLite建表方案建表是整套系统的地基。我常用的模型是两张表movie 存电影维度的信息review 存评论维度的内容。字段数量宁少勿多够分析用即可避免爬虫维护成本失控。movie 表保留电影ID、标题、评分和年代review 表保留评论正文、短评星级、点赞数、发布时间和情感分析结果。CREATE TABLE IF NOT EXISTS movie ( movie_id INTEGER PRIMARY KEY, title TEXT NOT NULL, rating REAL, year INTEGER, crawl_time DATETIME ); CREATE TABLE IF NOT EXISTS review ( review_id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER NOT NULL, content TEXT NOT NULL, rating REAL, vote_count INTEGER DEFAULT 0, comment_time DATETIME, sentiment TEXT, FOREIGN KEY (movie_id) REFERENCES movie(movie_id) ); CREATE INDEX idx_review_movie ON review(movie_id);建表语句里值得专门说明的是索引。review 表按 movie_id 做外键关联后续分析几乎全都是“查某部电影的全部评论”不建索引的话数据量到两三千条时查询就开始变慢建了索引后仍然是毫秒级。comment_time 和 vote_count 这两个字段来自爬虫页面前者用于按时间聚合观察口碑走势后者用于加权分析如果字段类型写错后面统计会有隐性问题比如点赞数存成 TEXT 导致 SUM 直接报错。字段设计有个常见误区把情感分析结果直接存成 1 和 -1。我一般建议存“正向/负向/中性”的文本值而不是纯数字。原因有两层一是展示阶段直接映射中文标签更省事二是后续换打分算法时数字阈值可能变化但文本标签相对稳定重新分析也不难覆盖。存储层落地后爬虫模块的产出就有了明确目标。下一步写爬虫时只需要按表结构逐条组装数据即可不用边爬边想字段放哪里。这种“先建表再爬取”的顺序看起来多花几分钟实际能避免绝大多数返工。3. 爬虫落地用requests先跑通再谈对抗策略豆瓣的短评页面结构比较稳定入门阶段没必要直接上 Selenium 或复杂框架。requests 加 BeautifulSoup 就能完成冒烟测试先把“能抓到列表页”这件事跑通再做频率控制和异常处理。一上来就堆 Scrapy 不一定是错但对这个体量的项目来说投入产出比不划算。3.1 最小可运行抓取脚本先搞定单页再写循环我先写一个只抓单页短评的最小脚本验证请求能不能成功、选择器能不能命中再做翻页和去重逻辑。很多人一上来就写全量循环结果请求头有问题时循环里跑几百次全是错误页日志都看不出问题在哪。import time import requests from bs4 import BeautifulSoup session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }) def fetch_reviews(movie_url, start0): params {start: start, limit: 20} resp session.get(movie_url, paramsparams, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) rows [] for item in soup.select(.comment-item): content_tag item.select_one(.short) if not content_tag: continue rows.append({ content: content_tag.get_text(stripTrue), vote: item.select_one(.votes).get_text(stripTrue), rating: parse_rating(item), # 函数见后文说明 }) return rows, bool(soup.select_one(.next)) # 冒烟测试只请求第一页 rows, has_next fetch_reviews(https://movie.douban.com/subject/xxxx/, 0) print(len(rows), has_next)这段脚本的核心参数有两个timeout 和 start 分页参数。timeout 设 10 秒是为了在网络抖动时不至于让程序悬挂半小时start 每页递进 20 是对应页面默认的分页大小。选择器 .comment-item 精确到单个评论容器比直接 select 整个列表稳得多。验证请求是否成功用 raise_for_status 而不是手动判断状态码遇到 403、418 时直接抛出异常比拿到一个错误页面解析很久才发现没有评论要省时间。3.2 请求头、限速策略与验证码边界冒烟测试通过后进入循环抓取阶段此时最重要的参数不是代码逻辑而是请求间隔。豆瓣对高频请求有比较明显的识别连续快速翻页很容易触发验证码页或 403。我常用的策略是固定间隔加随机抖动基础间隔 2 秒再叠加 0.5 到 1 秒的随机数。这个频率下每分钟约 25 到 30 个请求持续跑半小时很少触发异常如果把间隔压到 0.5 秒通常翻不到十页就会被弹验证码。def crawl_reviews(movie_url, max_pages10): all_rows [] for page in range(max_pages): rows, has_next fetch_reviews(movie_url, startpage * 20) all_rows.extend(rows) if not has_next: break time.sleep(2 0.5 * (page % 3)) # 随机化间隔避免稳定节奏被识别 return all_rows限速参数 2 秒不是玄学而是大量失败案例倒推出来的经验值。另一个关键点是保持同一会话的 Cookie。requests.Session 会自动保留服务端下发的 Cookie这比每次新建连接更像真实浏览器的行为。如果项目里用了多线程并发抓取我建议先砍掉并发顺序爬足够满足几千条数据的需求也更容易定位限流触发点。爬虫里只需要“识别验证码出现”的逻辑不需要也不应该做任何破解尝试。识别方式很简单请求返回的页面里出现“检测到有异常请求”或验证码图片节点时程序自动暂停 30 分钟再继续。这个策略虽然笨但完全够用而且不会触碰平台的安全边界。真正要关注的是数据质量如果页面里大量评论是“该条短评已被删除”说明电影本身评论量稀疏这时候应该换个目标而不是调整爬虫参数。4. 数据清洗与情感分析让分析结果不是随机情绪映射爬虫把原文存进数据库只是第一步难的是从原文里提取可用特征。短评里充斥大量与观点无关的残留文本和网络用语直接把原始文本送进情感模型结果基本不可信。4.1 清洗管线与 jieba 分词参数评论原文的脏数据有几个固定来源爬虫选择器误抓的标签文本、短评里“有用”“来自豆瓣”等 UI 残留、HTML 实体编码和全角空格。清洗管线按顺序处理这些项先去掉标签再压缩空白最后过滤无意义字符。import re import jieba STOPWORDS set() with open(stopwords.txt, encodingutf-8) as fp: for line in fp: STOPWORDS.add(line.strip()) def clean_text(text: str) - str: text re.sub(r[^], , text) text re.sub(r\s, , text) text re.sub(r【.*?】, , text) text text.replace(有用, ).replace(来自, ) return text.strip() def tokenize(text: str) - list: words jieba.lcut(clean_text(text)) # 过滤停用词和单字单字在短评里几乎不携带情感权重 return [w for w in words if w not in STOPWORDS and len(w) 1]清洗函数的参数值得抠一抠。替换“有用”和“来自”看起来像硬编码实际是豆瓣短评页面里最常见的残留短语数据量大时这两个词会以极高频率进入词云干扰真实主题。停用词表用 100 到 200 词的自建集合就够了覆盖“你、我、他、的、了、就、都、是”这类无信息量词长度过滤 len(w) 1 是为了去掉单字短评里的单字几乎不能独立表达情感。jieba 分词参数里还有一个常用开关jieba.load_userdict 可以加载自定义词典。如果目标电影是特定题材比如科幻片里的“硬核”、剧情片里的“催泪”可以手工补充这些词避免分词器把它们切开。注意分词的一致性问题同一句话在两次运行之间分词结果必须一致否则情感打分会出现随机波动。这一点在 jieba 默认配置下没问题但不要开启 HMM 的随机性选项去调参。4.2 基于情感词典的打分规则与阈值设置情感分析在这个项目里不建议直接上大模型接口。网络调用会引入延迟和费用而且课程设计文档里“离线词典打分”本身就是很完整的分析方案。词典法的思路是维护正向词表和负向词表对分词结果逐个匹配并用否定词和程度副词修正分数。POS_WORDS set([好, 喜欢, 经典, 感动, 值得, 精彩, 优秀]) NEG_WORDS set([烂, 差, 难看, 失望, 浪费, 敷衍, 糟糕]) NEGATES set([不, 没, 别, 未曾, 无]) INTENSIFIERS {很: 1.5, 太: 2.0, 非常: 1.8, 极其: 2.5, 有点: 0.7} def sentiment_score(tokens): score 0.0 magnify 1.0 negate 1 for w in tokens: if w in INTENSIFIERS: magnify INTENSIFIERS[w] elif w in NEGATES: negate -1 elif w in POS_WORDS: score 1.0 * magnify * negate magnify, negate 1.0, 1 elif w in NEG_WORDS: score - 1.0 * magnify * negate magnify, negate 1.0, 1 return score def label_sentiment(score): if score 0.5: return 正向 if score -0.5: return 负向 return 中性打分函数里 negate 和 magnify 是两个流转状态遇到否定词把权重反向遇到程度副词放大或缩小权重处理完一个情绪词后要复位。这个设计能解决“不好看”这类组合词如果不处理否定词“好”会贡献正向分数整句被误判为正向。阈值取 0.5 而不是 0是因为短评里常有程度较弱的情绪词比如“还可以”“一般”这类评论算中性更合理也拉高结果的稳定性。这里还有一个必须提醒的坑正负词典比例不均衡时结果会整体偏向词表更大的一方。我见过一个项目正词表 80 个词、负词表 20 个词最后所有评论的负向比例只有个位数看起来“口碑极好”实际是词表失衡。落地时先从每一类里随机抽 100 条短评人工标出高频情绪词补进词典后再跑全量数据。这套校验流程比调阈值更能提升准确率。清洗和打分的代码跑完后要把结果写回 review 表的 sentiment 字段。这一步为可视化准备数据也让后面章节里的校验有依据。5. 避坑从采集到分析会遇到的5个典型问题这个方向的血泪经验集中在五个问题上每一个都在真实项目里反复出现。按“现象、原因、解决”拆开写排查时可以直接对号入座。5.1 爬虫频繁返回验证码页进度中断现象程序跑了五六页后返回的页面里出现“检测到有异常请求”提示或者 HTTP 状态码变成 403/418。原因请求间隔太短或者 User-Agent 过于老旧。豆瓣对短周期内的高频请求有频率限制固定 0.5 秒间隔基本扛不住十页以上。解决把请求间隔提高到 2 秒以上并加入随机抖动。Session 保持 Cookie 不中断。程序里检测到验证码特征时不再继续请求直接 sleep 30 分钟。不要尝试绕过验证码暂停重试是最稳妥且合规的做法。5.2 抓到的评论全是“有用”和“短评内容”之类残留词现象清洗前的原文里频繁出现与评论无关的固定短语词云也被这些词占据。原因选择器精度不够选中了评论框外围的 UI 元素或者有些评论本身为空被页面渲染成占位文案。解决先用浏览器开发者工具检查每个评论的容器节点确保选择器落在 .comment-item 内部的 .short 节点上。对“该条短评已被删除”“此评论已被折叠”这类文案在清洗函数里直接过滤不送进分词器。注意折叠评论的判断逻辑不是所有折叠评论都无效但作为演示数据默认过滤掉比保留更合理。5.3 存 CSV 时中文乱码或写入半截文件现象用 csv 模块写入评论后Excel 打开全是乱码或者程序中断后 CSV 文件最后一行是半条记录。原因CSV 文件没有写入 UTF-8 BOMExcel 默认用 GBK 解码导致乱码写入过程中程序异常退出缓冲区里的半行数据被落盘。解决写 CSV 时使用 open(path, w, encodingutf-8-sig)这样 Excel 能正确识别编码。更推荐的做法是存 SQLite每抓一页就 commit 一次中断时只丢最近未提交的少量数据且不会出现半行脏记录。5.4 情感分析结果严重偏向正向且与评分分布对不上现象某部电影评分只有 5 分但情感分析结果里正向评论占了 70%明显不符合直觉。原因正负词表大小失衡负向词覆盖不足另外“不好看”“不难看”这类带否定结构的短句没被正确处理。解决先做分层采样从正向和负向标签中各抽 50 条人工复核把出现频率高但词表里没有的情绪词补进去。校验 rule 是同一批短评的人工标注和程序标注重合率应高于 70%低于这个值优先补词典而不是调阈值。5.5 数据量到几千条时程序内存暴涨现象爬虫和分析过程都正常但数据量接近 3000 条时内存占用超过 1GB操作越来越卡。原因代码里把全量评论一次性读进内存比如 data list(db.cursor().fetchall())。情感分析和词云都想直接拿全量数据导致内存叠加。解决清洗和分析改为分页读库每批处理 200 条结果写回数据库后释放这批数据。词云统计只需要计数结果先 SQL 聚合再生成词频字典不需要把几千条原文全部载入内存。这五类问题的共同点是看起来是“换个库、加行代码”就能解决实际都和数据设计有关。每条改动在项目早期做成本不超过一个下午等项目快收尾再回头补返工量会成倍放大。6. 把结果沉淀为报告分布图、词云与情感召回校验最后一公里的价值在于让分析结果能被别人看懂。只给一张“正向 45%、负向 30%”的饼图太单薄完整的可视化至少包含评论量分布、关键词词云和情感标签占比三块。这里有两个必调的参数中文字体路径和词云最大词数。import matplotlib.pyplot as plt from wordcloud import WordCloud # 中文字体必须显式指定否则图表全是豆腐块 FONT_PATH /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc def draw_rating_distribution(reviews): ratings [r[rating] for r in reviews if r.get(rating)] plt.hist(ratings, bins10, edgecolorwhite) plt.xlabel(rating) plt.ylabel(review count) plt.title(fShort comment rating distribution, avg{sum(ratings)/len(ratings):.2f}) plt.savefig(rating_dist.png, dpi120, bbox_inchestight) def draw_wordcloud(word_freq): wc WordCloud( font_pathFONT_PATH, max_words120, background_colorwhite, stopwordsSTOPWORDS, ).generate_from_frequencies(word_freq) wc.to_file(wordcloud.png)词云参数 max_words 设为 120 而不取默认 200是为了避免太稀频词占据版面stopwords 参数不建议在这里再做一次过滤因为分词阶段已处理过重复设置反而影响速度。font_path 是整个可视化模块最容易被忽略的坑matplotlib 和 wordcloud 默认字体都不支持中文不指定路径时输出的图片里中文全是方框。可视化只是让结论可见真正让结论可信的是校验。手动抽 20 条评论先不看程序结果人工标注正/负/中再和 sentiment 字段比对算出重合率。词典法在这个量级下达到 70% 以上就算合格不用追求更高因为短评里大量反讽和玩梗是词典法天然覆盖不了的。如果重合率低于 70%优先补词表而非调阈值。这个习惯是我做过多个类似方向后最深的教训先人工看一批数据再画图顺序反了会浪费大量时间在美化不可信的结果。希望帮到你。本文还有配套的精品资源点击获取