资讯动态

Python爬虫实战:抓取豆瓣短评与影评的完整指南

发布时间:2026/10/16 23:06:19 来源:尧图企业网站定制
简介针对豆瓣影评与短评数据采集的Python爬虫源代码包面向正在学习网络爬虫、希望获取真实评论数据用于文本分析与舆情观察的开发者。压缩包内共2个文件均为.py脚本整体大小仅5KB结构精简、开箱即用。影评爬虫覆盖影评ID、用户、IP地址、发表时间、推荐度、星级、标题、影评正文、图片以及赞同、反对、收藏、转发、回应等完整字段短评爬虫则采集序号、用户、地点、推荐度、点赞数、时间、评论内容与用户主页信息。两份程序分工明确体现了常规的请求发送、页面解析与字段提取思路可帮助读者快速掌握requests、BeautifulSoup等库的典型用法也能在此基础上扩展保存格式、数据清洗或进一步可视化分析。目前已有116人学习下载适合作为爬虫入门后的实战参考。1. 为什么用 Python 爬豆瓣短评和影评最练手反爬密度高、数据形态双、落盘目标明确“24python爬虫豆瓣短评和影评源代码”这个标题背后是几乎所有 Python 爬虫学习者都绕不开的一道综合题同一部电影短评是一句话的碎片化文本影评是几百到几千字的长文两类数据形态不同、页面结构不同、分页逻辑不同却都处在这套以 UA、Cookie、频率限制为代表的反爬体系之下。把两类数据能抓到本地并落成 CSV等于把请求构造、HTML 解析、异常处理和存储清洗完整走了一遍。这个项目适合正在学 requests 和 BeautifulSoup 的初学者也适合需要短评做情感分析、用影评做长文本建模的数据从业者。整篇会从拆页面开始给出一套能直接跑通的代码并把高频翻车点集中到一章里讲透。2. 先拆页面再写代码短评的列表结构、影评的两级链路和反爬防线从哪里来2.1 短评页面一页 20 条start 是翻页钥匙评分藏在 CSS 类名里豆瓣电影短评页的标准形态是 https://movie.douban.com/subject/{电影ID}/comments。打开页面后每条短评是一个 .comment-item 块用户名、评分星星、短评正文、发布时间都散落在这个块的不同子节点里。这里有一个新手最容易卡住的点页面 URL 里看不到分页参数你需要自己构造 start 和 limit。start 是偏移量limit 是每页条数sort 控制排序方式new_score 按最新、hot 按热门。初次调试不带任何参数直接访问一次把返回 HTML 里 .comment-item 的个数打出来会看到一页正好返回 20 条这就是翻页的锚点。为什么豆瓣用 start 而不是页码这是老式列表接口的设计传统。页码依赖服务端维护“当前页”状态start 则把状态交给客户端。对爬虫来说start 意味着你可以跳到任意偏移位置但也意味着要自己保证每次偏移 20 后不会超出数据末尾。超出时页面返回空列表而不是报错所以代码里必须检查解析结果为空就停止翻页。这个停止条件在后面 3.3 的代码里有体现新手最容易在这里写成无限翻页白白浪费请求次数。评分星星是一个很隐蔽的解析点。它不是一个可见文本节点而是一个 CSS 类名比如 allstar50 表示 5 星、allstar40 表示 4 星。如果你取元素文本得到的是空字符串。我第一次写这个解析时就在这卡了一晚上后来打开浏览器开发者工具才发现豆瓣用 CSS 类表达语义。这个经验说明一件事先用浏览器 F12 看结构再写选择器顺序不能反。2.2 影评页面列表页只有摘要正文在独立详情页必须两级请求影评的数据链路和短评完全不同。影评列表页 https://movie.douban.com/subject/{电影ID}/reviews 只给你标题、作者和两行摘要完整正文在 https://movie.douban.com/review/{影评ID}/。抓影评必须分两步先请求列表页从每条影评的标题链接里提取影评 ID再拿着 ID 请求详情页从 .main-content 里提取正文。列表页的翻页参数是 start 和 countcount 固定为 20。如果不走详情页直接抓列表页拿到的只是摘要不是全文。这个两级链路的加入是影评爬虫和短评爬虫工作量差异的来源。短评把内容直接内联在列表页里一次请求就能拿到所有字段影评为了让列表页渲染更快把长正文拆到独立页面列表页只做索引。你不可能从列表页直接拼接出全文因为摘要本身就被截断在固定字数。这也是为什么抓影评时 ID 提取必须是第一优先级没有 review_id 就什么都没有。维度短评影评入口页面/comments/reviews 拿列表/review/{id}/ 拿正文正文位置列表页 HTML 内详情页 .main-content翻页参数start、limit列表用 start、count详情无翻页单条数据量一句到几句话几百到几千字登录依赖未登录可见部分未登录可看列表详情页可见性受限2.3 反爬第一道防线UA、Referer、Cookie 的手动获取与逐项验证豆瓣的反爬从请求头开始。没有 User-Agent 的 requests 请求会被直接拒绝Referer 缺失会让部分接口返回 403Cookie 缺失或过期则导致短评条目被截断。获取方式不复杂用 Chrome 登录豆瓣按 F12 打开开发者工具切到 Network 面板刷新任意一个短评或影评页面点开任意一条请求把 Request Headers 里的字段值复制出来。动手之前先在浏览器地址栏输入目标 URL 确认能正常打开这是很多人会跳过的一步浏览器都打不开的页面requests 大概率也拿不到数据。拿到 UA 后要注意复制的是完整字符串不要只截取中间一段也不要在里面加多余空格。Referer 先填豆瓣首页到具体请求评论页时再改成电影主页。Accept-Language 容易被漏但中文站点没带它偶尔会触发一次额外校验。Cookie 的管理建议单独放不要写死在代码里提示Cookie 会过期。最好把 Cookie 单独放到配置文件里读取。一旦发现 403 比例上升先检查 Cookie 是否失效再考虑调整频率。3. 短评爬虫最小跑通方案requests 会话、CSS 选择器解析、CSV 落盘一屏讲完短评爬虫的最小骨架就是三块会话构造、页面解析、CSV 落盘。先解决会话。3.1 建立会话与请求头把 requests 伪装成浏览器的第一步import requests import csv import time import random from bs4 import BeautifulSoup def get_session(): 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.0.0 Safari/537.36, Referer: https://movie.douban.com/, Cookie: 把浏览器里的 Cookie 粘到这里, Accept-Language: zh-CN,zh;q0.9, }) return session这里解释一下为什么用 Session 而不是裸的 requests.get。Session 对象会保存请求头和 Cookie 状态后续每个请求都自动携带不用每条请求单独传 headers。如果你用裸的 requests.get每次都要把三个头写一遍代码冗余不说还容易漏写某一个头踩 403。Session 还有连接池的作用复用底层 TCP 连接请求特征更接近真实浏览器的行为。UA 直接复制浏览器的完整值不要自己拼接短字符串。豆瓣对明显非浏览器的 UA 识别很快。Referer 先填豆瓣首页到具体请求时会改成对应电影页。Cookie 是登录凭证缺失时短评条数明显变少。Accept-Language 容易漏但建议带上它对中文站点有实际影响。3.2 解析短评条目一次取出评分、正文、时间和点赞数def parse_comments(html): soup BeautifulSoup(html, html.parser) results [] for item in soup.select(.comment-item): user_tag item.select_one(.comment-info a) content_tag item.select_one(.short) rating_tag item.select_one(.comment-info .rating) time_tag item.select_one(.comment-time) if user_tag is None or content_tag is None: continue rating_class rating_tag.get(class, [])[0] if rating_tag else results.append({ user: user_tag.text.strip(), rating: rating_class.replace(allstar, ), content: content_tag.text.strip(), time: time_tag.get(title, time_tag.text).strip() if time_tag else , vote: item.select_one(.vote-count).text if item.select_one(.vote-count) else 0, }) return results解析的核心是理解评分表达方式。rating_class 的取值是 allstar50、allstar40、allstar30 这种格式replace 掉 allstar 前缀后剩两位数第一位就是星数。所以 allstar50 变成 50取第一位得到 5。你也可以保留两位在统计时转成 1 到 10 分制。time 字段页面上显示的是“3 天前”这类相对时间但 title 属性里存着完整时间所以优先取 title代码里用 get(title, time_tag.text) 做兜底。vote 是这条短评的点赞数抓下来是为了给后续排序和去重留指标。如果解析结果全为空先别怀疑代码用浏览器开发者工具检查当前页面结构。豆瓣改版不频繁但评论区 class 结构确实调整过。把页面实际 HTML 和选择器对照一遍比反复改代码更高效。这个习惯适用于所有爬虫项目不只是豆瓣。3.3 翻页与限速start 偏移量加随机 sleep 的节奏def fetch_all_comments(session, movie_id, max_pages10): base_url fhttps://movie.douban.com/subject/{movie_id}/comments all_comments [] for page in range(max_pages): params { start: page * 20, limit: 20, sort: new_score, status: P, } resp session.get(base_url, paramsparams, timeout10) if resp.status_code ! 200: print(f第 {page 1} 页失败状态码 {resp.status_code}) break comments parse_comments(resp.text) if not comments: print(f第 {page 1} 页无数据停止翻页) break all_comments.extend(comments) time.sleep(random.uniform(2, 4)) return all_comments参数说明start 每页偏移 20limit 固定 20 是最自然的组合。sortnew_score 按最新排序sorthot 按热门排序影响的是数据口径要先想清楚需要哪种再选。statusP 代表只看看过该电影的用户的短评这是豆瓣评论区的过滤条件之一去掉它可能混入非观影用户的评论。max_pages 是保护上限脚本失控时最多请求这么多页先设 10 跑通再调大。timeout10 必须写否则请求挂起会让整个脚本停在原地。sleep 放在每页结束后随机 2 到 4 秒。固定间隔比随机间隔更容易被频率规则识别这一点有点玄学但实际测试下来固定 3 秒的脚本被限流的概率明显更高。如果你只需要最新 50 条短评max_pages 设 3 就够了没必要把所有页都爬完。每多一页就是多一次风险暴露够用就行。3.4 落盘 CSVutf-8-sig 编码与多字段去重def save_to_csv(comments, filenamedouban_comments.csv): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[user, rating, content, time, vote]) writer.writeheader() seen set() for row in comments: key (row[user], row[content], row[time]) if key in seen: continue seen.add(key) writer.writerow(row) print(f已保存 {len(seen)} 条短评到 {filename})encoding 用 utf-8-sig 而不是 utf-8这是血泪经验。utf-8-sig 会在文件头写入 BOMExcel 双击打开时中文才能正常显示直接用 utf-8 写出来的文件记事本和 Excel 打开都会乱码。fieldnames 的顺序决定了 CSV 的列顺序按 user、rating、content、time、vote 排后续用 pandas 读进来时列顺序稳定不用手动调整。去重的 key 用“用户 内容 时间”三元组。为什么不用内容单独做 key因为豆瓣短评里同一句话被多个用户复用的场景太常见了只看内容会误删大量有效数据。加上用户和时间重复的概率低很多。热门排序下翻页必然出现重复数据这一步不做统计结果会虚高。4. 影评爬虫完整链路列表页拿 ID、详情页取正文、超时重试与频率控制影评的复杂度比短评高一截核心原因是两级链路。你需要先有一份影评 ID 清单再去逐条抓正文。这一步不需要并发但要稳定。4.1 从列表页提取影评 ID解析标题链接并去掉尾斜杠影评列表页的每条影评都包在 .review-item 块中标题链接的 href 形如 /review/1234567/。提取逻辑如下def parse_review_list(html): soup BeautifulSoup(html, html.parser) items [] for item in soup.select(.review-item): a_tag item.select_one(a.title) if a_tag is None: continue href a_tag[href] review_id href.rstrip(/).split(/)[-1] items.append({ review_id: review_id, title: a_tag.text.strip(), author: item.select_one(.author a).text.strip() if item.select_one(.author a) else , summary: (item.select_one(.main-content .short-content).text .strip().replace((展开), ) if item.select_one(.main-content .short-content) else ), }) return itemshref 处理时的 rstrip(/) 是细节但很致命。如果不先去掉末尾斜杠/review/1234567/ 经过 split(/) 后取最后一个元素是空字符串。先 rstrip 再 split最后一个元素才是纯数字 ID。summary 字段提取后要 replace 掉“(展开)”字样这是列表页摘要常见的拼接内容。author 字段不一定在列表页里存在用了条件表达式兜底取不到就填空字符串不影响主流程。列表页解析失败时先确认页面里真的存在 .review-item。豆瓣偶尔会为某些影片展示不同的列表模板。我遇到过冷门电影的影评列表外层结构不同但内部 .review-item 依然存在所以解析器核心选择器不用变只需要确认容器存在。经验是不要把选择器写得太深依赖外层越多越容易在页面改版时挂掉。4.2 详情页正文提取多段落拼接与三次重试def parse_review_detail(session, review_id): url fhttps://movie.douban.com/review/{review_id}/ for attempt in range(3): try: resp session.get(url, timeout10) if resp.status_code 200: soup BeautifulSoup(resp.text, html.parser) article soup.select_one(.main-content) if article is None: print(f影评 {review_id} 正文为空可能已被删除) return None paragraphs article.select(p) content \n.join(p.text.strip() for p in paragraphs if p.text.strip()) return { review_id: review_id, title: soup.select_one(h1).text.strip() if soup.select_one(h1) else , content: content, } elif resp.status_code 403: print(f影评 {review_id} 返回 403等待 10 秒后重试) time.sleep(10) else: print(f影评 {review_id} 状态码 {resp.status_code}) return None except requests.Timeout: print(f影评 {review_id} 超时重试 {attempt 1}/3) time.sleep(5) return None三块要点。第一正文拼接时过滤掉空 p 标签避免段落之间出现大量空行这样落盘后的文本直接可以用 NLP 流程处理。第二403 和超时的重试策略不同403 是频率被标记的强烈信号等 10 秒再试超时等 5 秒。第三三次重试后仍失败就放弃这条返回 None由主循环决定跳过。有人会把重试次数调到 10 次甚至无限循环这在连续 403 场景下没有意义只会让请求堆积得更严重。如果重试期间一直 403说明整体频率太高正确的做法是把所有 sleep 拉长到 10 秒以上或者直接停止脚本过一段时间再跑。重试不是万能药它是兜底机制不是对抗反爬的工具。4.3 主循环列表页与详情页分层的限速节奏def crawl_reviews(session, movie_id, max_pages5): base_url fhttps://movie.douban.com/subject/{movie_id}/reviews all_reviews [] for page in range(max_pages): params {start: page * 20, count: 20} resp session.get(base_url, paramsparams, timeout10) if resp.status_code ! 200: break review_list parse_review_list(resp.text) if not review_list: break for review_item in review_list: detail parse_review_detail(session, review_item[review_id]) if detail: all_reviews.append(detail) time.sleep(random.uniform(4, 6)) # 详情页之间间隔更久 time.sleep(random.uniform(3, 5)) # 列表页之间间隔 return all_reviews列表页 count 固定 20start 每页加 20。详情页没有额外参数纯靠 URL 定位这是两级链路的主循环。详情页之间的 sleep 比列表页之间更长因为详情页请求更密集、更容易触发风控。max_pages 先设 5因为每页 20 条影评每条影评还要请求一次详情页5 页就是 100 次详情页请求这个量级足够测试稳定性了。如果某页列表解析为空直接 break不要继续空转。两种 sleep 写成两行而不是合成一行是为了方便分别调整粒度。实际跑的时候你很快会感受到一个矛盾sleep 太短会 403sleep 太长抓取 5 页影评可能要十几分钟。我的做法是先短间隔跑通流程再逐步拉长到稳定区间。不要一上来就追求最快速度爬虫项目的寿命取决于你如何管理请求节奏。5. 豆瓣反爬避坑清单403、验证码、乱码、重复数据的五个翻车现场5.1 高频 403Referer 缺失或 Cookie 过期现象脚本前几页正常持续翻页后开始连续 403即使把 sleep 拉到 5 秒也无效。 原因最常见的是 Referer 没带或 Cookie 过期其次是请求了不存在的电影 ID豆瓣对无效 ID 的反应就是 403。第三个容易被忽视的原因是 UA 字符串被截断或拼错导致请求头不符合浏览器特征。 解决把 Referer 改成对应电影主页地址确认 Cookie 仍有效。连续 403 时停 10 分钟再跑不要靠调高重试次数硬冲。判断 UA 是否有效的快速方法是在浏览器里复制那份 UA用命令行工具带它访问目标页面看返回码是否正常。5.2 短评数远少于页面显示登录态截断与折叠现象短评页面能打开解析结果只有几条跟页面标题上标的“共 1000 条”差距巨大。 原因未登录状态下豆瓣对短评做了截断展示只给热门几条。还有一部分短评被折叠在“展开”后面初始 HTML 里根本没有这些内容。 解决使用登录后的 Cookie 重新请求短评条数会显著提升。如果场景不允许使用登录态就要在数据说明里写清楚口径是“未登录可见短评”不要拿它当全量数据做模型训练。被折叠的短评可以尝试请求时带 statusP 参数但最终可见范围仍受登录态限制。5.3 中文乱码requests 编码推断有时会翻车现象短评正文在爬虫里显示乱码但浏览器里正常。 原因requests 依赖响应头里 charset 字段推断编码豆瓣部分页面没有明确的 charset 声明requests 会回退到 ISO-8859-1对中文来说必然乱码。 解决在 session.get 之后主动设置 resp.encoding utf-8放在解析之前。如果先把响应保存到文件再读取读取时也要用 encodingutf-8。乱码问题优先查编码不要怀疑选择器。把编码写死在代码里不是最优解但针对豆瓣这个站点是稳定且可靠的。5.4 翻页出现重复数据热门排序下 start 偏移不稳定现象抓 10 页短评去重后发现大量跨页重复有效数据只有预期的一半。 原因热门排序按点赞数动态变化点赞数一变排序就变start 偏移定位到的区间会与上一页重叠。最新排序也会因为新评论写入产生轻微偏移。 解决按“用户 内容 时间”三元组去重不要只按内容去重。同一句话可能被不同用户重复写只看内容会把有效数据误删。去重逻辑要放在写入前见 3.4 的代码。如果对时效性要求高切到最新排序后定期抓取并用时间戳做二次判断。5.5 影评正文被截断列表页摘要和详情页全文混在一起看现象影评“正文”只有两行字怀疑解析漏了段落。 原因列表页的摘要本来就是截断的完整正文在详情页。如果详情页正文也很短可能是该条影评本身就是“短书评”形态字数就少。 解决确认请求地址是 /review/{id}/ 详情页而不是列表页。在详情页里检查 .main-content 下是否有多段 p 标签如果只有一段文本且长度很短说明这条影评本身是短评形态数据没问题。这个场景下要判断的是自己是否需要长文本而不是代码有没有写对。6. 断点续爬与增量抓取把脚本从“能跑一次”变成“能跑一年”到这里短评和影评两条链路都能跑了但这类项目最容易翻车的其实是中断后的处理。我自己的经历是脚本跑到第 3 页被验证码打断数据只存了一半重新跑又抓了一堆重复。后来我给自己定下两条规矩。第一条是把“已抓 ID”落成文件。在写入正文之前先把 review_id 或短评三元组写进 done.txt下次启动时读一遍这个文件跳过已存在的记录。这样断点续爬只需要把 max_pages 调大不需要改任何逻辑。第二条是抓下来的数据要做二次校验。rating 字段应该能转成整数时间字段不应该早于对应电影的上映日期影评 content 长度至少大于 50 字。校验不通过的数据单独写进 error.log不直接丢弃方便回溯到底是编码问题还是页面结构变了。验证阶段给一个可操作的小习惯抓完 20 条短评和 5 条完整影评后用 pandas 读一次 CSV打印每一列的 dtype 和空值计数。然后做两个粗糙统计短评评分分布、影评平均字数。如果评分分布集中在两三个分值上大概率是 rating 解析错位如果平均字数异常低可能把摘要当正文用了。这两个指标能快速暴露解析层的问题比逐条对页面省时间得多。我保留到现在的一个习惯是每次启动爬虫前先去浏览器手动刷新一次目标页面确认自己能正常打开。浏览器都进不去脚本一定抓不到数据。频率和耐心比代码技巧更能决定一个爬虫项目能跑多久。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑