☰
Python爬虫实战:从豆瓣影评采集到情感分析系统设计
2026/10/12 4:28:38 网站建设 项目流程

简介:一份面向本科毕业设计及课程报告的爬虫项目论文,以豆瓣影评分析系统为具体案例,围绕网络爬虫基本原理、网页请求与解析、公开接口调用、数据库存储、文本清洗、情感分析和可视化展示等完整技术链路展开,同时说明反爬限制应对策略,适合计算机科学与技术、数据分析等专业的本科生参考。整个资源仅含一个docx文档,大小约32KB,内容为论文全文,按章编排,包含绪论、Python爬虫技术基础、豆瓣影评数据获取、数据分析与可视化、系统设计与实现、总结与展望六个部分,目录清晰,便于查阅。已有1246人浏览学习,说明该选题具有一定的参考热度。读者可从中获得一份完整的毕业论文文稿,既能借鉴绪论中研究背景、目的意义与国内外现状的撰写方式,也能参考爬虫框架选型、数据抓取与清洗流程、系统需求分析、架构设计及测试等核心章节的内容组织,可帮助快速理清毕业设计写作脉络。

1. 基于 Python 爬虫的豆瓣影评分析系统:一份值得照着复现的毕业设计

做爬虫的人心里都清楚,豆瓣是国内反爬强度最有代表性的网站之一:请求头校验严格、IP 封禁频繁、登录态有效期短,很多人第一次爬豆瓣影评都是被 418、403 劝退的。这份《基于 Python 爬虫对豆瓣影评分析系统的设计与实现》本科毕业论文,价值不在于它有多前沿,而在于它把「爬虫获取 → 数据清洗 → 情感分析 → 可视化展示」这一整套流程完整地走通了一遍,从绪论到系统测试共六章,技术栈全用 Python 生态:requests 抓页面、BeautifulSoup 解析、pandas 清洗、nltk/SnowNLP 做情感判断、matplotlib 出图,最后落在 MySQL 存储和结果展示上。如果你是正在做爬虫类课程设计或毕设的本科生,这份资源能帮你省掉大量查资料和搭框架的时间;如果你是刚入门的爬虫开发者,照着它的模块拆分方式做一遍,也能把「爬虫到底怎么落地成一个系统」这件事看清楚。它不是能直接跑的成品代码,而是一份可以边读边复现的设计蓝图,坑在哪儿、参数怎么调,下面我从头拆给你看。

2. 爬虫原理与库选型:先想清楚用 requests 还是 Scrapy

2.1 爬虫的基本流程与请求头伪装

爬虫的技术本质是模拟浏览器行为:程序代替人去访问 URL、接收响应、解析 HTML,最后把页面里的结构化数据提取出来。整个流程在论文第二章写得很清楚,五个阶段——目标确定、发送请求、解析页面、数据处理、存储数据——这也是大多数爬虫脚本的共同骨架。

import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://movie.douban.com/", "Accept-Language": "zh-CN,zh;q=0.9", } url = "https://movie.douban.com/subject/1292052/comments" resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") comments = soup.select("span.short") for c in comments: print(c.get_text().strip())

这段代码是爬虫主流程的最小可用版本。headers里的三个字段是关键:User-Agent必须用真实浏览器的 UA,豆瓣会检查这个字段来判断访问是否来自合法客户端;Referer也不能省略,豆瓣对直接访问评论页的请求会倾向于拒绝;Accept-Language用中文值是为了避免服务器返回繁体或英文默认页。BeautifulSoup的select方法支持 CSS 选择器,span.short是豆瓣短评列表常见的评论容器标签,实际使用时需要打开浏览器开发者工具确认当前页面结构。

2.2 为什么这份论文选 requests + BeautifulSoup 而不是 Scrapy

论文第二章提到了 Scrapy,但实际的数据获取模块实现用的是 requests 库。这个选择很聪明,原因有三点:第一,课程的周期限制了框架学习成本,Scrapy 的 Spiders、Middlewares、Pipelines 概念体系需要额外两周左右的时间消化,而 requests 加 BeautifulSoup 半天就能写出能跑的脚本;第二,豆瓣影评列表页面的结构相对规整,不同电影的热门短评都在同一个div容器下,用 CSS 选择器就能定位,不需要 Scrapy 那种面对复杂站点设计的分布式调度能力;第三,requests 的Session对象可以同时管理 Cookie 和请求头,对于需要模拟登录或者维持访问会话的场景,代码结构更直观。

用 requests 的代价是并发效率低。处理这个问题,论文的做法是用多线程调度爬取任务,而不是引入 Scrapy 的异步机制。常见的写法是concurrent.futures.ThreadPoolExecutor,控制线程数在 4 到 8 个之间,既能提高抓取速度,又不会因为请求频率过高触发封禁。线程数再往上加就属于高风险操作了,后面避坑章节会细说。

2.3 数据清洗:正则去噪声与文本预处理

抓下来的影评数据不能直接用。豆瓣页面的评论内容里有大量换行符、HTML 实体、特殊符号,还有用户回复时嵌套的引用文本,这些都会干扰后续的情感分析。论文在数据预处理环节定义了三个清洗步骤:去除噪声字符、过滤非中文内容、统一评分字段格式。

import re def clean_comment(text): # 去掉 HTML 标签和实体 text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"&nbsp;|&amp;|&lt;|&gt;", "", text) # 只保留中文字符、英文字母和常用标点 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()]", "", text) # 合并多个连续空格和换行 text = re.sub(r"\s+", " ", text).strip() return text raw = "这部电影太棒了!<br>强烈推荐&nbsp;&nbsp;" print(clean_comment(raw)) # 输出: 这部电影太棒了!强烈推荐

这个clean_comment函数看起来简单,但正则的写法有讲究:第一行re.sub(r"<[^>]+>", "", text)用来去 HTML 标签,注意这里用[^>]+而不是.*,是因为.*会贪婪匹配到不该删的尖括号内容;第二行处理常见 HTML 实体,&nbsp;和&amp;是豆瓣页面上最常出现的两种;第三行的字符范围是从 Unicode 中过滤掉表情符号、特殊符号和乱码字节,只保留中文、英文、数字和中文标点。注意在 Python 正则里,\u4e00-\u9fa5表示中文字符区间,这是文本清洗约定俗成的写法。

3. 数据获取与存储:从豆瓣页面解析到 MySQL 入库

3.1 页面解析策略:直接抓 HTML 而不是依赖 API

论文的第三章标题叫「豆瓣影评 API 调用」,但实际实现里,豆瓣官方开放 API 早已不对个人开发者提供匿名访问,需要申请 API key 且审核严格。大多数爬虫项目采用的做法是直接解析 HTML 页面,论文最终落地的也是这条路线。爬虫代码需要处理的页面类型主要有三种:电影详情页获取评分和基本信息,短评列表页获取用户评论和评分星级,长评页获取完整影评内容。

def parse_comment_page(html): soup = BeautifulSoup(html, "html.parser") data = [] items = soup.select("div.comment-item") for item in items: user = item.select_one("a") vote = item.select_one("span.votes") rating = item.select_one("span.rating") comment = item.select_one("span.short") data.append({ "user": user.get_text().strip() if user else "", "votes": int(vote.get_text().strip()) if vote else 0, "rating": rating.get("class", [""])[0] if rating else "", "content": comment.get_text().strip() if comment else "", }) return data

这段解析逻辑依赖豆瓣评论列表页的 DOM 结构,div.comment-item是每条短评的容器,用户信息、有用数、评分、评论内容分别对应不同的子节点。其中评分字段比较特殊,豆瓣的星级不是文本值而是span标签上的 class 属性,比如allstar50、allstar40分别代表五星和四星,所以提取时要拿class而不是get_text()。这个细节容易踩坑,按文本提取会得到空字符串。

3.2 分页与请求频率控制

豆瓣短评列表页每页显示 20 条,通过 URL 里的start参数翻页,第一页是 0,第二页是 20,以此类推。论文中采用循环请求的方式翻页,同时在两次请求之间强制休眠:

import time base_url = "https://movie.douban.com/subject/{}/comments?start={}&limit=20" movie_id = 1292052 all_comments = [] for offset in range(0, 200, 20): url = base_url.format(movie_id, offset) resp = requests.get(url, headers=headers, timeout=10) if resp.status_code != 200: print(f"请求失败: {resp.status_code},offset={offset}") break page_data = parse_comment_page(resp.text) if not page_data: print(f"offset={offset} 无数据,可能页面结构变化或已被封禁") break all_comments.extend(page_data) time.sleep(3)

这里的time.sleep(3)是反爬策略的最小成本手段。豆瓣对访问频率的容忍阈值大约在每秒 1 到 2 个请求之间,超过这个频率就会触发封禁或验证码。3 秒的间隔意味着每分钟 20 条评论的获取速度,看似慢,但可以稳定地跑完大部分电影的热门短评。如果需要加速,可以用随机间隔替代固定间隔,比如time.sleep(random.uniform(2, 5)),这样请求节奏更接近人工浏览行为。

3.3 MySQL 建表与数据入库

论文选择 MySQL 存储影评数据,数据库表的设计考虑到了后续分析的查询需求,核心表结构可以归纳为:影评内容、用户评分、评论时间、有用数、电影 ID。建表语句和插入逻辑如下:

CREATE TABLE douban_review ( id INT AUTO_INCREMENT PRIMARY KEY, movie_id INT NOT NULL, user_name VARCHAR(64), rating VARCHAR(16), comment TEXT, votes INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_movie (user_name, movie_id, comment(100)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
import pymysql conn = pymysql.connect( host="localhost", user="root", password="123456", database="douban", charset="utf8mb4" ) def insert_review(movie_id, item): with conn.cursor() as cursor: sql = """ INSERT INTO douban_review (movie_id, user_name, rating, comment, votes) VALUES (%s, %s, %s, %s, %s) """ cursor.execute(sql, (movie_id, item["user"], item["rating"], item["content"], item["votes"])) conn.commit()

建表时用UNIQUE KEY unique_user_movie (user_name, movie_id, comment(100))做去重约束很关键。爬虫脚本重复运行时,或者翻页过程中遇到页面数据重叠,会带来大量重复记录,依赖应用层去重既慢又容易漏。数据库层的唯一索引直接把重复数据挡在外面,代价是插入时偶尔会抛DuplicateEntry错误,捕获异常并跳过即可。comment(100)表示对评论内容的前 100 个字符建立索引,这是 MySQL 对 TEXT 字段的唯一索引限制要求,也是保证查重的必要妥协。

4. 数据处理与情感分析:从分词到情感极性判断

4.1 中文分词与停用词过滤

影评数据是中文短文本,与英文文本处理最本质的差别在于分词。英文单词天然以空格分隔,中文必须先通过分词工具把句子切分成词序列。论文的处理流程里,分词和停用词过滤是连贯的两个步骤——分词把句子拆成词语,停用词过滤把「的、了、是、在」这类高频但无实际意义的词去掉。

import jieba stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def tokenize(text): words = jieba.lcut(text) filtered = [w for w in words if w not in stopwords and len(w.strip()) > 1] return filtered sample = "这部电影的剧情很紧凑,演员演技在线,就是结局有点仓促。" print(tokenize(sample)) # 输出: ['电影', '剧情', '紧凑', '演员', '演技', '在线', '结局', '仓促']

jieba.lcut返回的是列表类型,比cut更适合下游处理。过滤条件len(w.strip()) > 1把单个字组成的词排除了,这类单字词在中文本里大多是助词或语气词,对情感判断贡献极小,而且容易引入噪声。停用词表可以从 GitHub 上找现成的中文停用词库,也可以自己根据语料统计高频词后临时补充,论文的做法是两者结合。

4.2 基于 SnowNLP 的情感得分计算

在情感分析模块,论文选用的是 Python 自然语言处理库 SnowNLP。相比 NLTK 和 TextBlob,SnowNLP 对中文文本的训练结果更贴合国内用户的表达习惯,它内置的情感模型基于电商评论语料训练,虽然不完全匹配影评领域,但胜在部署简单、调用方便,一句s.sentiments就能得到情感得分。

from snownlp import SnowNLP def sentiment_score(text): s = SnowNLP(text) return s.sentiments # 0 到 1 之间的值,越接近 1 越正面 reviews = ["这部电影非常震撼,值得二刷", "剧情拖沓,睡着了", "中规中矩吧,没有惊喜"] for r in reviews: score = sentiment_score(r) label = "正面" if score > 0.6 else ("负面" if score < 0.4 else "中性") print(f"{score:.3f} -> {label} <- {r}")

SnowNLP 的sentiments属性返回一个 0 到 1 之间的概率值:越接近 1 判定为正面,越接近 0 判定为负面。0.4 到 0.6 的区间当作中性处理,这个阈值不是论文里写的固定标准,而是我在实际跑数据时根据结果调的——豆瓣短评里大量用户给三星,常规的正负二分法会把它们硬算成负面,加一个缓冲区间能明显减少误判。影评数据的情感分布通常是偏态的,高分电影正面评论占比极高,这是领域特性不是模型偏差。

4.3 评分归一化与特征统计

除了评论文本,用户评分也是情感分析的重要辅助特征。豆瓣页面上短评的星级以 class 形式出现,转换为数值后可以做归一化处理,让评分字段与情感得分一起构成综合分析的特征向量:

def rating_to_score(rating_class): """将豆瓣星级 class 转为 1-5 分""" mapping = { "allstar50": 5, "allstar45": 4.5, "allstar40": 4, "allstar35": 3.5, "allstar30": 3, "allstar25": 2.5, "allstar20": 2, "allstar15": 1.5, "allstar10": 1, } return mapping.get(rating_class, 0) def normalize_score(score): return (score - 1) / (5 - 1) raw_rating = "allstar40" print(normalize_score(rating_to_score(raw_rating))) # 0.75

归一化后的评分和情感得分可以组合成一个简单的「观众满意度指标」,比如0.6 * sentiment_score + 0.4 * normalized_rating。这个加权公式在论文里没有细讲,但实际分析时这样做的效果不错——因为同一部电影里,用户打分和文字评论之间偶尔会出现不一致,比如有人给五星但评论内容偏吐槽,融合两个维度比只看一个维度的结论更稳定。

5. 常见问题与避坑指南:豆瓣反爬的血泪经验

5.1 请求频率过高导致 IP 被临时封禁

现象:爬虫脚本运行一段时间后,突然连续出现 403 响应,且页面上返回的内容不是正常评论列表,而是一个包含「有异常请求」提示的验证码页面,脚本随后进入无限 403 循环。

原因:豆瓣的反爬机制是单 IP 维度限流,单位时间内的请求次数超过阈值就直接拒绝后续连接。即使设置了time.sleep(3),如果线程数开得过多,比如同时跑 8 个线程,每个线程都在独立请求,聚合请求频率还是会超出限制。

解决:把并发线程数降到 4 或更低,同时把休眠时间改成随机范围,用random.uniform(3, 6)替代固定数值。如果 IP 已经被封,最快的解决方法是重启路由器换 IP,或者用代理池轮换出口 IP。论文里没有提到一点值得注意:豆瓣封禁通常只封接口路径而非全站,短时间 403 后可以先访问电影详情页确认 IP 是否真正被封。

5.2 解析规则失效导致数据大量缺失

现象:爬虫运行正常、状态码返回 200,但解析出的评论列表突然变成空数组,日志显示本页成功 0 条数据。

原因:豆瓣在前端改版时会调整页面 DOM 结构,曾经用div.comment-item或span.short作为选择器的写法会一次性失效。页面真实内容可能改用新的 class 名或换成>DELETE t1 FROM douban_review t1 INNER JOIN douban_review t2 WHERE t1.id > t2.id AND t1.user_name = t2.user_name AND t1.movie_id = t2.movie_id AND t1.comment = t2.comment;

5.4 Cookie 登录态过期导致只能爬取公开数据

现象:爬取部分电影的详细短评时,请求返回 200 但解析出来的是「登录后查看」的提示文本,而不是评论内容;或者翻到第 6、7 页时突然所有字段都为空。

原因:豆瓣限制未登录用户只能浏览部分短评列表的后续分页,超过一定页数就强制要求登录。只带 User-Agent 的匿名请求无法满足这个条件,需要在请求头里附上登录后的 Cookie。

解决:在代码里维护一个 Cookie 管理函数,登录一次后把 Cookie 字符串持久化到本地文件,过期后重新登录刷新。用requests.Session()维护会话状态,把session.cookies传给每个请求,避免每次请求都重新构造 Cookie。这里的常见做法是先手动在浏览器里登录豆瓣,把 Cookie 复制进脚本,适合课程设计这种不需要长期运行的批量任务。

6. 可视化与系统展示:让分析结果能直接看到

6.1 基于 matplotlib 和 Flask 的结果呈现

数据分析和模型计算都完成了,还有一个环节决定论文的系统完整性——结果可视化。论文选择用 matplotlib 和 seaborn 绘制图表,配合 Flask 搭建一个轻量级的 Web 查询页面。这个组合很适合本科毕设的定位:matplotlib 负责把情感分析结果渲染成图表,Flask 不需要额外引入大型前端框架,服务端模板加简单 HTML 就能跑起来。

from flask import Flask, render_template, request import pymysql import matplotlib.pyplot as plt import io import base64 app = Flask(__name__) def get_reviews_from_db(movie_id): conn = pymysql.connect(host="localhost", user="root", password="123456", database="douban", charset="utf8mb4") with conn.cursor() as cursor: sql = "SELECT rating, comment FROM douban_review WHERE movie_id = %s" cursor.execute(sql, (movie_id,)) rows = cursor.fetchall() conn.close() return rows @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": movie_id = request.form.get("movie_id") reviews = get_reviews_from_db(movie_id) scores = [sentiment_score(r[1]) for r in reviews] # 生成情感分布直方图 plt.figure(figsize=(8, 5)) plt.hist(scores, bins=20, color="#2E86AB", alpha=0.8) plt.title(f"Movie {movie_id} Sentiment Distribution") plt.xlabel("Sentiment Score") plt.ylabel("Count") buf = io.BytesIO() plt.savefig(buf, format="png") buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode("utf-8") plt.close() return render_template("result.html", chart=img_base64, count=len(reviews)) return render_template("index.html") if __name__ == "__main__": app.run(debug=True, port=5000)

这段代码演示的是最简化的 Flask 集成方式:前端提交电影 ID → 后端查数据库 → 逐条算情感得分 → matplotlib 绘制分布图 → base64 编码传给前端模板显示。有个性能细节值得注意,sentiment_score函数每次调用都要对文本重新做一整轮 SnowNLP 分析,影评多的时候页面加载会明显变慢。论文的系统测试里没体现这一点,但实际使用时我一般会加一层结果缓存,把已经算过情感得分的评论 ID 存进 Redis 或直接存数据库字段,下次直接读缓存结果。

6.2 情感分布图与评论趋势的读法

情感得分的分布直方图能直观反映一部电影的「口碑形状」。高分电影通常是左偏分布,峰值集中在 0.8 以上;争议电影的分布会出现明显的双峰,一堆人打高分、一堆人打低分;平庸电影则是中间隆起的钟形,两头矮。这个图形分析维度在论文 4.2 节有过文字描述,但代码层面建议加上一句对分布偏度的自动判断:

from scipy import stats def distribution_shape(scores): skew = stats.skew(scores) if skew < -0.5: return "正面主导型口碑" elif skew > 0.5: return "负面主导型口碑" else: return "争议均衡型口碑"

scipy.stats.skew返回的是偏度系数,负值表示左偏,即高分评论数量占绝对优势;正值表示右偏,低分评论更多。这个自动分类对批量对比多部电影的口碑很有用,不用一张张看图就能快速提取结论。

6.3 文本主题聚类:用 LDA 找出观众讨论的热点

论文第 1.4 节研究方法中提到了主题建模,但系统实现部分没有展开。实际上用 LDA 快速提取评论关键词,能让分析系统的内容维度丰富不少。我一般会在数据清洗后,把全部评论按分词结果转换成语料库,然后调用gensim的 LDA 模型:

from gensim import corpora, models def topic_analysis(tokenized_docs, num_topics=5): dictionary = corpora.Dictionary(tokenized_docs) corpus = [dictionary.doc2bow(doc) for doc in tokenized_docs] lda_model = models.LdaModel(corpus, num_topics=num_topics, id2word=dictionary, passes=15) topics = lda_model.print_topics(num_words=8) return topics tokenized_docs = [tokenize(r[1]) for r in reviews] for topic in topic_analysis(tokenized_docs): print(topic) # 输出示例: (0, '0.038*"剧情" + 0.026*"演员" + 0.022*"导演" + ...)

LDA 模型的输出是「主题编号 + 单词权重」的形式,每个主题对应一组相关词汇。对这些主题做简单归纳,比如「演员」「演技」相关词聚成一个主题,「剧情」「节奏」聚成另一个主题,就能看出观众对这部电影的讨论焦点分布。需要注意passes=15这个参数,它表示模型训练时遍历语料库的轮数,设置越高模型收敛越好但时间更长,15 轮对本科毕设的语料规模足够用。

从那以后我做任何爬虫类项目,都会把清洗、入库、分析、可视化这四个阶段拆成独立模块,每个模块单独调试通过后再串联——论文里的一句话「模块化设计提高了系统的可维护性」,在实际写代码时是能救命的原则。这份论文资源虽然文字部分有些地方讲得不够细,但结构和技术选型都很规整,按它的目录一步步复现,你能得到一个功能完整、可以写进简历的影评分析系统。希望帮到你。

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

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

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

立即咨询