
简介一套基于Python的豆瓣网站爬虫与数据可视化完整源码主要面向需要完成Python课程期末大作业、毕业设计或课程设计的在校学生无论是答辩展示还是二次开发参考都有较高价值。项目实现了豆瓣数据的自动抓取、清洗、存储与可视化展示代码注释细致新手也能看懂系统功能完善、界面美观、操作简单经过严格调试确保可运行部署后即可直接使用很适合作为高分课程作业或毕设项目。压缩包共110个文件大小6.28MB核心为5个Python脚本同时包含HTML、CSS、JavaScript等前端资源以及db数据库、字体图标、配置文档等辅助文件完整覆盖数据采集、后端处理和前端展示链路前端基于Bootstrap等样式构建界面交互友好展示效果直观。目录结构清晰模块间耦合度低便于二次开发。已有140人学习下载适合希望快速搭建高质量项目或系统学习爬虫可视化开发的读者。1. 把豆瓣短评爬下来做可视化这份“高分大作业”到底在考察什么每年毕业季和期末python豆瓣网站爬虫和可视化源码都是搜索榜上的常客。这个标题看起来是把“请求网页、解析数据、画几张图”串在一起但真正拿过高分的作业考察的远不止requests加BeautifulSoup。豆瓣的反爬策略、短评接口的分页机制、多线程与并发控制、数据清洗的边界以及可视化图表能否回答一个具体问题才是拉开分差的地方。这篇博文不会假装见过某个现成的压缩包而是按一线工程师做这类项目时最常见、最可靠的路径把从接口分析到可视化落地的完整闭环拆开讲清楚。目标读者是能跑通pip install、但还没系统处理过反爬和并发问题的开发者如果你正卡在“单页能抓、全量抓不全”或者“图表画出来不知道说什么”这篇应该能给你一个可复现的方案。2. 爬虫选型与合规边界为什么先分析接口再写代码2.1 Robots 协议与请求头不是所有页面都该爬写任何爬虫之前第一件事不是打开编辑器而是看一眼目标网站的访问规则。豆瓣的robots.txt对部分路径有明确限制短评页面/subject/id/comments/通常在允许范围内但用户主页、私信这类路径属于明确禁止抓取的区域。高分作业和低分作业的第一个分水岭就在这里是否在代码里做了合法路径校验。常见做法是在爬虫入口维护一个允许访问的前缀列表比如只允许/movie/,/subject/,/j/下的公开接口遇到其他路径直接跳过并记录日志。这样既符合公序良俗也能在答辩时清晰说明“我做了约束”。请求头方面User-Agent必须伪装成真实浏览器但仅仅设置 UA 远远不够。豆瓣会校验Accept-Language、Referer和Connection的完整性缺少任何一个都可能触发418或403。下面这个请求头模板是经过实际检验的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/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://movie.douban.com/, Connection: keep-alive }这段代码的逻辑在于Referer告诉服务器请求是从哪个页面跳转过来的豆瓣对直接访问短评页的请求会做校验Accept-Language影响返回内容的编码和排序方式缺失时偶尔会出现乱码或跳转到hk版。参数q0.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/下面需要携带Referer为https://m.douban.com/movie/才能返回JSON数据。这个接口每页返回 20 到 25 条包含author、rating、content、time等结构化字段解析成本极低是短评爬虫的首选。第三种是内部 JSON 端点在短评列表页 HTML 源码中可以看到window.__DATA__变量它包含了第一页的完整数据。高分开源项目通常会同时实现 HTML 解析和 JSON 解析两种模式因为两者互为后备。下面以移动端 API 为例展示一个字节级别的请求构造def fetch_mobile_comments(movie_id, page_limit20): session requests.Session() session.headers.update(HEADERS) url ( fhttps://m.douban.com/rexxar/api/v2/movie/{movie_id} f?ckrefererhttps%3A%2F%2Fm.douban.com%2Fmovie%2F ) params { start: 0, count: page_limit, status: P, sort: new_score, } resp session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json() # 逻辑说明 # 1. 用 Session 复用 TCP 连接避免每次请求都走三次握手 # 2. params 中 sortnew_score 表示按热度排序hot 则按时间 # 3. statusP 表示只看短评不包含长评和影评 # 4. 如果返回的 json 里没有 comments 字段说明请求被风控或参数失效这里有一个容易踩的坑start参数并不是简单的(页码 - 1) * count。豆瓣短评 API 的start是累计偏移量最大只允许偏移总条数 - count且单次请求的count上限不是25实测传100也能返回 100 条但超过500会被直接拒绝。因此全量抓取应该用while True循环每次按返回的total判断是否终止而不是硬编码页数。2.3 反爬风控的四个阶段从限速到验证码豆瓣的风控是渐进式的摸清阶段特征可以避免无意义的重试浪费。第一阶段是单 IP 限速短时间请求超过 40 次/分钟返回418状态码第二阶段是参数校验此时请求头缺少Referer或cookie无效会返回403第三阶段是验证码触发条件通常是高频访问某个热门影片的短评页响应体里会出现img idcaptcha_image第四阶段是封禁 IP所有请求直接超时。标题里的“高分大作业”通常会在requests之外封装一层retry逻辑核心代码如下from tenacity import retry, stop_after_attempt, wait_exponential import logging retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def safe_get_comments(movie_id, page_limit20): data fetch_mobile_comments(movie_id, page_limit) if data.get(error): raise ValueError(fAPI error: {data[error]}) return datatenacity库的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设置在4到8之间并且配合一个全局节流器import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed class RateLimiter: 令牌桶思路的简易限速器确保每秒请求数不超过给定阈值 def __init__(self, max_qps5): 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_qps5) def fetch_with_limit(movie_id): limiter.wait() return fetch_mobile_comments(movie_id) def run_parallel(movie_ids): results {} with ThreadPoolExecutor(max_workers4) 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_qps5和max_workers4配合使用目的是让线程数大于 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 f2025-{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_pathdouban_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 评分分布与短评情感倾向的叠加单一评分分布条形图过于基础高分作业应该叠加“情感倾向预测”结果没有机器学习模型的场景下可以用SnowNLP或BosonNLP做预训练情感打分。SnowNLP的默认模型是电商评论迁移到电影短评会有偏差但作为大作业的参考维度完全够用。from snownlp import SnowNLP import pandas as pd df pd.read_sql_query(SELECT rating, content FROM comments WHERE movie_id1292052, 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(indexrating, columnssentiment_group, valuescontent, aggfunccount, fill_value0) pivot pivot.reindex(rating_order)这段代码的要点pd.cut的边界值 0.4 和 0.6 是根据影评数据分布调的电商评论可以用 0.3/0.7但电影短评两极分化明显0.4/0.6 能更好区分中立倾向。4.2 Top250 电影评分的散点矩阵年份、评分、评价人数一张散点图同时映射三个变量是数据可视化课的经典考题。横轴为上映年份纵轴为评分点的大小为评价人数颜色为类型。用matplotlib手动实现时注意scatter的s参数需要归一化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], ssizes, cdf[rating], cmapviridis, alpha0.7, edgecolorswhite) ax.set_xlabel(上映年份) ax.set_ylabel(豆瓣评分) ax.set_title(豆瓣 Top250 电影散点矩阵) plt.colorbar(sc, label评分) plt.show()sizes的映射逻辑是将评价人数线性变换到 20 到 820 的圆圈面积避免数量级差异过大导致小点不可见。c与cmap组合实现颜色映射这里不能同时c为离散字符串否则会报错。4.3 短评关键词词云过滤停用词的顺序决定效果词云的坑几乎全在预处理阶段。如果直接把短评切词然后丢给WordCloud得到的一定是“电影”、“这部”、“真的”这类高频无意义词。正确顺序是先做jieba切词再用自定义停用词表过滤最后统计词频。import jieba from collections import Counter from wordcloud import WordCloud stopwords set() with open(stopwords_cn.txt, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) def make_wordcloud(comments_text, output_pathwordcloud.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_pathmsyh.ttc, # Windows 下为 simhei.ttf width1200, height800, background_colorwhite, max_words200) 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 rating5星 THEN 5 WHEN rating4星 THEN 4 WHEN rating3星 THEN 3 WHEN rating2星 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(orientrecords)) if __name__ __main__: app.run(debugFalse, port5000)接口设计说明HAVING cnt 50过滤掉样本过少的电影避免平均分失真avg_rating的CASE WHEN把星级映射为数值这是 SQL 层面的数据清洗比在 Python 里逐行apply更快。前端页面核心是fetch(/api/movies)拿到数据后用echarts.init生成一个可筛选的下拉框和对应图表。下拉框联动用ECharts的setOption替换数据序列即可。5.2 大屏适配与性能别再为每次滑动发请求“可视化大屏”是热搜词但大作业里的看板不需要花哨的动态粒子背景。真正影响体验的是前端是否做了数据缓存。常见方案是Flask-Caching给接口加 5 分钟过期时间from flask_caching import Cache cache Cache(app, config{CACHE_TYPE: SimpleCache}) app.route(/api/movies) cache.cached(timeout300) def list_movies_cached(): # 原函数逻辑不变本文还有配套的精品资源点击获取