news 2026/9/15 4:33:53

豆瓣短评爬虫与可视化:从反爬应对到交互看板的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
豆瓣短评爬虫与可视化:从反爬应对到交互看板的完整实践

简介:一套基于Python的豆瓣网站爬虫与数据可视化完整源码,主要面向需要完成Python课程期末大作业、毕业设计或课程设计的在校学生,无论是答辩展示还是二次开发参考,都有较高价值。项目实现了豆瓣数据的自动抓取、清洗、存储与可视化展示,代码注释细致,新手也能看懂;系统功能完善、界面美观、操作简单,经过严格调试确保可运行,部署后即可直接使用,很适合作为高分课程作业或毕设项目。压缩包共110个文件,大小6.28MB,核心为5个Python脚本,同时包含HTML、CSS、JavaScript等前端资源,以及db数据库、字体图标、配置文档等辅助文件,完整覆盖数据采集、后端处理和前端展示链路;前端基于Bootstrap等样式构建,界面交互友好,展示效果直观。目录结构清晰,模块间耦合度低,便于二次开发。已有140人学习下载,适合希望快速搭建高质量项目或系统学习爬虫可视化开发的读者。

1. 把豆瓣短评爬下来做可视化,这份“高分大作业”到底在考察什么

每年毕业季和期末,python豆瓣网站爬虫和可视化源码都是搜索榜上的常客。这个标题看起来是把“请求网页、解析数据、画几张图”串在一起,但真正拿过高分的作业,考察的远不止requestsBeautifulSoup。豆瓣的反爬策略、短评接口的分页机制、多线程与并发控制、数据清洗的边界,以及可视化图表能否回答一个具体问题,才是拉开分差的地方。这篇博文不会假装见过某个现成的压缩包,而是按一线工程师做这类项目时最常见、最可靠的路径,把从接口分析到可视化落地的完整闭环拆开讲清楚。目标读者是能跑通pip install、但还没系统处理过反爬和并发问题的开发者;如果你正卡在“单页能抓、全量抓不全”或者“图表画出来不知道说什么”,这篇应该能给你一个可复现的方案。

2. 爬虫选型与合规边界:为什么先分析接口再写代码

2.1 Robots 协议与请求头:不是所有页面都该爬

写任何爬虫之前,第一件事不是打开编辑器,而是看一眼目标网站的访问规则。豆瓣的robots.txt对部分路径有明确限制,短评页面(/subject/<id>/comments/)通常在允许范围内,但用户主页私信这类路径属于明确禁止抓取的区域。高分作业和低分作业的第一个分水岭就在这里:是否在代码里做了合法路径校验

常见做法是在爬虫入口维护一个允许访问的前缀列表,比如只允许/movie/,/subject/,/j/下的公开接口,遇到其他路径直接跳过并记录日志。这样既符合公序良俗,也能在答辩时清晰说明“我做了约束”。

请求头方面,User-Agent必须伪装成真实浏览器,但仅仅设置 UA 远远不够。豆瓣会校验Accept-LanguageRefererConnection的完整性,缺少任何一个都可能触发418403。下面这个请求头模板是经过实际检验的:

import requests 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://movie.douban.com/", "Connection": "keep-alive" }

这段代码的逻辑在于:Referer告诉服务器请求是从哪个页面跳转过来的,豆瓣对直接访问短评页的请求会做校验;Accept-Language影响返回内容的编码和排序方式,缺失时偶尔会出现乱码或跳转到hk版。参数q=0.8是权重值,表示优先级,不是随便写的。

2.2 短评接口的三种形态:PC 端 HTML、移动端 API、内部 JSON

豆瓣短评的获取方式至少有三条路,很多教程只讲了第一条,导致一遇到改版就抓瞎。

第一种是PC 端 HTML解析,地址形如https://movie.douban.com/subject/1292052/comments/。这种方式最直观,但 HTML 结构里短评内容被包裹在div.comment-item中,需要处理杂乱的标签嵌套,且某些字段(比如投票数)被隐藏。适合小规模抓取,500 条以内不需要上 API。

第二种是移动端 API,端点固定在https://m.douban.com/rexxar/api/v2/movie/1292052/下面,需要携带Refererhttps://m.douban.com/movie/才能返回JSON数据。这个接口每页返回 20 到 25 条,包含authorratingcontenttime等结构化字段,解析成本极低,是短评爬虫的首选。

第三种是内部 JSON 端点,在短评列表页 HTML 源码中可以看到window.__DATA__变量,它包含了第一页的完整数据。高分开源项目通常会同时实现 HTML 解析和 JSON 解析两种模式,因为两者互为后备。

下面以移动端 API 为例,展示一个字节级别的请求构造:

def fetch_mobile_comments(movie_id, page_limit=20): session = requests.Session() session.headers.update(HEADERS) url = ( f"https://m.douban.com/rexxar/api/v2/movie/{movie_id}" f"?ck=&referer=https%3A%2F%2Fm.douban.com%2Fmovie%2F" ) params = { "start": 0, "count": page_limit, "status": "P", "sort": "new_score", } resp = session.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() # 逻辑说明: # 1. 用 Session 复用 TCP 连接,避免每次请求都走三次握手 # 2. params 中 sort=new_score 表示按热度排序,hot 则按时间 # 3. status=P 表示只看短评,不包含长评和影评 # 4. 如果返回的 json 里没有 comments 字段,说明请求被风控或参数失效

这里有一个容易踩的坑:start参数并不是简单的(页码 - 1) * count。豆瓣短评 API 的start是累计偏移量,最大只允许偏移总条数 - count,且单次请求的count上限不是25,实测传100也能返回 100 条,但超过500会被直接拒绝。因此全量抓取应该用while True循环,每次按返回的total判断是否终止,而不是硬编码页数。

2.3 反爬风控的四个阶段:从限速到验证码

豆瓣的风控是渐进式的,摸清阶段特征可以避免无意义的重试浪费。第一阶段是单 IP 限速,短时间请求超过 40 次/分钟,返回418状态码;第二阶段是参数校验,此时请求头缺少Referercookie无效会返回403;第三阶段是验证码,触发条件通常是高频访问某个热门影片的短评页,响应体里会出现<img id="captcha_image">;第四阶段是封禁 IP,所有请求直接超时。

标题里的“高分大作业”通常会在requests之外封装一层retry逻辑,核心代码如下:

from tenacity import retry, stop_after_attempt, wait_exponential import logging @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def safe_get_comments(movie_id, page_limit=20): data = fetch_mobile_comments(movie_id, page_limit) if data.get("error"): raise ValueError(f"API error: {data['error']}") return data

tenacity库的wait_exponential会用 2 秒、4 秒、8 秒的指数退避重试,避免在风控触发后仍以固定频率打请求。注意stop_after_attempt(3)表示最多试三次,超过后抛出异常,由上层记录失败任务,而不是无限循环。

3. 单页爬通之后:并发设计、会话保持与数据存储的坑

3.1 从 requests.Session 到并发:线程池的正确打开方式

单页爬通只是第一步。豆瓣 Top250 每部电影有上千条短评,如果一部部串行去抓,250 部电影平均每部 3 秒,仅请求时间就要 12 分钟以上,再加上解析和落盘,总计轻松超过 20 分钟。这在演示时很容易翻车,因为评委不会等你跑完。

常见的优化手段是concurrent.futures.ThreadPoolExecutor,但线程池不是无脑开。豆瓣对单 IP 的限速决定了总 QPS 上限,建议max_workers设置在48之间,并且配合一个全局节流器:

import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed class RateLimiter: """令牌桶思路的简易限速器:确保每秒请求数不超过给定阈值""" def __init__(self, max_qps=5): self.max_qps = max_qps self.lock = threading.Lock() self.last_second = time.time() self.count = 0 def wait(self): with self.lock: now = time.time() if now - self.last_second >= 1: self.last_second = now self.count = 0 self.count += 1 if self.count > self.max_qps: sleep_time = 1 - (now - self.last_second) + 0.01 time.sleep(max(sleep_time, 0)) limiter = RateLimiter(max_qps=5) def fetch_with_limit(movie_id): limiter.wait() return fetch_mobile_comments(movie_id) def run_parallel(movie_ids): results = {} with ThreadPoolExecutor(max_workers=4) as executor: futures = {executor.submit(fetch_with_limit, mid): mid for mid in movie_ids} for future in as_completed(futures): mid = futures[future] try: results[mid] = future.result() except Exception as exc: logging.error(f"电影 {mid} 抓取失败: {exc}") return results

逻辑说明:RateLimiter的核心是“每秒计数”,超过max_qps就阻塞。这里把max_qps=5max_workers=4配合使用,目的是让线程数大于 QPS 上限,这样总有线程在等待,不会在轮询时产生空窗。不要试图把max_qps调到 20 以上,豆瓣对同一 IP 的短评接口风控阈值实测在 15 QPS 左右。

3.2 短评数据清洗:标签、空格、重复和日期规范化

从接口拿到了JSON不代表数据能直接用于可视化。短评内容大量包含HTML 实体换行符微博式话题标签,还有用户的可视化中常见的“此条为短评”占位符。清洗逻辑一般分四步:

import re import html from datetime import datetime def clean_comment(raw_comment: str) -> str: # 1. 反转义 &amp; 这类 HTML 实体 text = html.unescape(raw_comment) # 2. 去掉所有尖括号标签 text = re.sub(r'<[^>]+>', '', text) # 3. 将多个空白字符压缩为单个空格 text = re.sub(r'\s+', ' ', text).strip() # 4. 去掉纯表情符号行(比如[moc]开头的) text = re.sub(r'^\[[a-z]+\]', '', text) return text def normalize_time(raw_time: str) -> str: # 输入形如 "2025-01-12 23:45:12" 或 "01-12 23:45" if len(raw_time) > 10: return raw_time[:10] return f"2025-{raw_time[:5]}"

参数说明:html.unescape必须放在正则去标签之前,否则&lt;会先被正则误伤;日期归一化成YYYY-MM-DD格式是为了后续按时间序列聚合,如果保留原始格式会导致pandas排序错乱。

3.3 存储层选择:CSV、SQLite 还是 MySQL

大作业的存储方案决定答辩时的可扩展性。只存自己爬的 250 部电影短评,SQLite是性价比最高的选择,单文件、无需服务、支持 SQL 查询。CSV适合交付,但并发写入会互相覆盖,不是好的运行时存储。

推荐表结构如下:

CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER NOT NULL, movie_title TEXT, author TEXT, rating TEXT, -- 豆瓣评分如 "5星" useful_vote INTEGER DEFAULT 0, content TEXT, comment_time TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_movie_time ON comments(movie_id, comment_time);

写入逻辑用executemany批量插入,避免逐条execute带来的事务开销:

import sqlite3 def save_to_sqlite(records, db_path="douban_comments.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.executemany( """INSERT OR IGNORE INTO comments (movie_id, movie_title, author, rating, useful_vote, content, comment_time) VALUES (?, ?, ?, ?, ?, ?, ?)""", records ) conn.commit() conn.close()

这里INSERT OR IGNORE依赖id或唯一索引去重,如果你没建唯一索引,这个语句不会生效。建议在movie_id + author + comment_time上建唯一索引,因为同一用户在同一时间对同一部电影只有一条短评。

4. 从数据到图表:豆瓣短评可视化的四个必做图

4.1 评分分布与短评情感倾向的叠加

单一评分分布条形图过于基础,高分作业应该叠加“情感倾向预测”结果,没有机器学习模型的场景下,可以用SnowNLPBosonNLP做预训练情感打分。SnowNLP的默认模型是电商评论,迁移到电影短评会有偏差,但作为大作业的参考维度完全够用。

from snownlp import SnowNLP import pandas as pd df = pd.read_sql_query("SELECT rating, content FROM comments WHERE movie_id=1292052", conn) df["sentiment"] = df["content"].apply(lambda x: SnowNLP(x).sentiments) df["sentiment_group"] = pd.cut(df["sentiment"], bins=[0, 0.4, 0.6, 1.0], labels=["负面", "中性", "正面"]) rating_order = ["1星", "2星", "3星", "4星", "5星"] pivot = df.pivot_table(index="rating", columns="sentiment_group", values="content", aggfunc="count", fill_value=0) pivot = pivot.reindex(rating_order)

这段代码的要点:pd.cut的边界值 0.4 和 0.6 是根据影评数据分布调的,电商评论可以用 0.3/0.7,但电影短评两极分化明显,0.4/0.6 能更好区分中立倾向。

4.2 Top250 电影评分的散点矩阵:年份、评分、评价人数

一张散点图同时映射三个变量是数据可视化课的经典考题。横轴为上映年份,纵轴为评分,点的大小为评价人数,颜色为类型。用matplotlib手动实现时注意scatters参数需要归一化:

import matplotlib.pyplot as plt import numpy as np def plot_movie_scatter(df): fig, ax = plt.subplots(figsize=(14, 8)) sizes = (df["rating_count"] - df["rating_count"].min()) / \ (df["rating_count"].max() - df["rating_count"].min()) * 800 + 20 sc = ax.scatter(df["year"], df["rating"], s=sizes, c=df["rating"], cmap="viridis", alpha=0.7, edgecolors="white") ax.set_xlabel("上映年份") ax.set_ylabel("豆瓣评分") ax.set_title("豆瓣 Top250 电影散点矩阵") plt.colorbar(sc, label="评分") plt.show()

sizes的映射逻辑是:将评价人数线性变换到 20 到 820 的圆圈面积,避免数量级差异过大导致小点不可见。ccmap组合实现颜色映射,这里不能同时c为离散字符串,否则会报错。

4.3 短评关键词词云:过滤停用词的顺序决定效果

词云的坑几乎全在预处理阶段。如果直接把短评切词然后丢给WordCloud,得到的一定是“电影”、“这部”、“真的”这类高频无意义词。正确顺序是先做jieba切词,再用自定义停用词表过滤,最后统计词频。

import jieba from collections import Counter from wordcloud import WordCloud stopwords = set() with open("stopwords_cn.txt", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def make_wordcloud(comments_text, output_path="wordcloud.png"): words = [] for text in comments_text: seg_list = jieba.lcut(text) for w in seg_list: w = w.strip() if len(w) > 1 and w not in stopwords and not w.isdigit(): words.append(w) counter = Counter(words) wc = WordCloud(font_path="msyh.ttc", # Windows 下为 simhei.ttf width=1200, height=800, background_color="white", max_words=200) wc.generate_from_frequencies(dict(counter)) wc.to_file(output_path)

注意font_path必填,不指定的话词云里的中文全是方框。Linux 服务器上常见字体位置在/usr/share/fonts/truetype/wqy/wqy-zenhei.ttcmatplotlib同样依赖这个字体配置才能正常显示中文坐标轴。

5. 把可视化做成交互:用 Flask 搭一个可筛选的短评看板

5.1 Flask + ECharts:前后端分离的最小实现

静态图片适合写报告,但真正能拿高分的作品应该提供一个可交互的页面。技术栈上用Flask提供 JSON 接口,前端用ECharts渲染图表,两者都不需要复杂配置,适合大作业周期。

后端路由示例如下:

from flask import Flask, jsonify, request import sqlite3 import pandas as pd app = Flask(__name__) DB_PATH = "douban_comments.db" @app.route("/api/movies") def list_movies(): conn = sqlite3.connect(DB_PATH) df = pd.read_sql_query(""" SELECT movie_id, movie_title, COUNT(*) as cnt, AVG(CASE WHEN rating='5星' THEN 5 WHEN rating='4星' THEN 4 WHEN rating='3星' THEN 3 WHEN rating='2星' THEN 2 ELSE 1 END) as avg_rating FROM comments GROUP BY movie_id, movie_title HAVING cnt > 50 ORDER BY avg_rating DESC """, conn) conn.close() return jsonify(df.to_dict(orient="records")) if __name__ == "__main__": app.run(debug=False, port=5000)

接口设计说明:HAVING cnt > 50过滤掉样本过少的电影,避免平均分失真;avg_ratingCASE WHEN把星级映射为数值,这是 SQL 层面的数据清洗,比在 Python 里逐行apply更快。

前端页面核心是fetch("/api/movies")拿到数据后,用echarts.init生成一个可筛选的下拉框和对应图表。下拉框联动用EChartssetOption替换数据序列即可。

5.2 大屏适配与性能:别再为每次滑动发请求

“可视化大屏”是热搜词,但大作业里的看板不需要花哨的动态粒子背景。真正影响体验的是前端是否做了数据缓存。常见方案是Flask-Caching给接口加 5 分钟过期时间:

from flask_caching import Cache cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"}) @app.route("/api/movies") @cache.cached(timeout=300) def list_movies_cached(): # 原函数逻辑不变

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 4:31:57

Spring Boot植物健康管理系统实战:从需求到部署全流程复盘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 4:31:30

AI烧token真相与降本实战:从流量降价到JWT续签避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 4:30:29

国产修图软件悟空图像PhotoSir实测:能否替代Photoshop?

1. 为什么我停了Photoshop&#xff0c;开始认真用悟空图像PhotoSir先交代一下背景。我平时的工作流里&#xff0c;修图这东西绕不开。公众号头图、活动海报、产品详情页、偶尔还要给客户出一版快手方案&#xff0c;几乎天天都在跟图层、蒙版、钢笔路径打交道。以前电脑上常年挂…

作者头像 李华
网站建设 2026/9/15 4:29:44

多摄像头融合的上帝视角监控系统:从单应性矩阵到实时目标追踪

1. 项目概述1.1 我为什么想做一个“上帝视角”系统先说一个很现实的场景&#xff1a;我手头管理着园区里三个分散的监控区域&#xff0c;加起来二十多路摄像头。传统监控画面是一块块小格子&#xff0c;保安盯得眼睛都快瞎了&#xff0c;还是容易出现“人在画面A消失、在画面B没…

作者头像 李华