资讯动态

Python爬虫实战:豆瓣短评稳定抓取与反爬避坑指南

发布时间:2026/10/13 16:03:50 来源:尧图企业网站定制
简介一套完整的Python爬虫实战项目资料面向希望掌握网络数据采集、清洗与可视化全流程的初学者及进阶者。项目聚焦豆瓣平台病毒与疾病题材电影的短评数据从零演示如何使用Requests库发送HTTP请求、借助BeautifulSoup解析网页结构并提取电影ID、短评内容和评分字段同时针对常见反爬机制给出了设置User-Agent、处理Cookie、使用代理IP等实用规避技巧。数据阶段引入Pandas进行去重、缺失值处理和文本清洗再通过jieba分词与Counter计数完成高频词统计可视化部分则利用Matplotlib、Seaborn和WordCloud生成词云及统计图表。压缩包为RAR格式约7.84MB涵盖项目源代码、爬取到的原始数据文件以及生成的可视化结果结构清晰便于边读边练。已有1205人浏览学习适合作为课程设计参考或爬虫技能从入门到实践的完整样例。1. 抓豆瓣短评前先想清楚这四件事决定你能不能跑通Python爬取豆瓣短评看起来是“拿requests请求一个页面再解析HTML”的入门操作但实际做起来你会发现同样的代码白天能跑晚上就403换台电脑可能直接重定向到登录页。这篇笔记不是教你把豆瓣当成免费语料库无节制地抓而是把“如何稳定地抓取一个电影的短评列表”讲清楚请求头里少了什么、分页参数是什么语义、限速该怎么设计、字段解析怎么不漏。适合刚学Python不久、想用真实网页练手的学习者也适合正在积攒影评文本做分析的人。读完你能跑通“单页短评抓取→多页翻页→CSV落盘”的完整链路遇到反爬也能定位问题。2. 短评页面结构与请求分析为什么直接requests能看到数据2.1 短评页URL和HTML结构豆瓣电影短评页的URL遵循固定模式。例如一部电影的subject_id豆瓣内部编号是1292052那么短评页就是https://movie.douban.com/subject/1292052/comments页面通过URL参数控制翻页和排序常见参数包括start从第几条短评开始默认0limit每页返回多少条常见20status短评类型P表示看过F表示在看等sort排序方式new_score是按推荐/热度time是按时间打开短评页后“查看网页源代码”能看到每条短评被包裹在div classcomment-item节点内。里面至少有五个有用字段评论者昵称、评分通常用span的class表示几星、评论发表时间、有用数“有用XX”以及短评正文一般放在span classshort里。有的老版本页面还会出现“展开”按钮说明正文超过几十字被截断这些细节我们后面解析要注意。2.2 不带头请求头会发生什么直接用requests.get(https://movie.douban.com/subject/1292052/comments)访问大概率返回200但里面没有短评而是一个“安全检测”提示页甚至重定向到登录页。这是因为豆瓣的访问策略会先检查请求头中的User-Agent和Referer如果看起来不像浏览器就返回一个简化页面或直接302。解决的办法是模拟浏览器的请求头。最关键的是User-Agent比如Chrome浏览器的UA字符串其次是Referer它应当指向这部电影的主页https://movie.douban.com/subject/1292052/再一个就是Cookie。豆瓣的爬虫识别在近两年变得更严格即使有UA和Referer也不一定每次都能过所以Cookie通常要从你自己的浏览器里复制一份带着它请求的成功率会高很多。注意这里有个边界Cookie相当于是你的登录凭证自己学习使用没问题不要打包在公开代码里更不要用别人的账号信息。我们后面所有代码里的Cookie都应该是你自己的而且用完记得在分享时抹掉。2.3 用开发者工具确认关键请求参数在动手写代码前我习惯先把浏览器开发者工具打开F12切到Network面板手动刷新一次短评页然后找到名字是comments的XHR或Doc请求。不同类型页面加载出的文件名不一样老版短评是普通HTML文档新版可能是接口JSON。如果看到的是/j/review/...之类的接口说明你访问到了新版页面那么解析方式就得换成直接消费JSON字段比解析HTML更省事。但大多数电影短评页仍然返回HTML文档所以这篇文章以HTML解析为主后面会提一句JSON接口的适配。这一步的真正目的是确认三件事请求URL带了哪些query参数、请求头里实际发送的Cookie是什么、响应体中出现短评内容的节点。把这些确认清楚下面写代码就不会靠猜。这里也可以提一句robots豆瓣的robots协议并不允许爬虫肆意抓取所有页面所以我们只做低频率、少量页面的学习爬取永远不要把短评页的limit参数改成100。很多“被封”案例就是单次请求太贪心。3. 写第一个可用的爬虫脚本requests BeautifulSoup 拉取短评3.1 环境准备与最小依赖在开始之前确保Python环境为3.8以上。需要安装requests和beautifulsoup4以及后面保存数据用的pandas非必须但处理起来方便。安装命令pip install requests beautifulsoup4 pandas说明pandas用来把结构化数据写成CSV或Excel如果你不想装也可以在代码末尾用csv.writer自行写文件。这里用pandas的主要理由是它对中文编码的处理更顺手也方便做后续的情感分析。3.2 解析单页短评的代码我们先把单页短评解析做成一个函数输入是HTML文本输出是这个页面的短评列表。以“评论项、评论者、评分、时间、有用数、内容”为字段。参考代码from bs4 import BeautifulSoup def parse_comment_page(html): soup BeautifulSoup(html, html.parser) items soup.select(div.comment-item) records [] for item in items: # 评论者昵称通常在 comment-info 里的第一个 a 标签 author item.select_one(div.comment-item .comment-info a) nickname author.text.strip() if author else # 评分rating 元素 class 形如 allstar40表示4星 rating item.select_one(span.allstar40, span.allstar50, span.allstar30, span.allstar20) rating_text if rating: class_name rating.get(class, []) if class_name: star_num class_name[0].replace(allstar, ) rating_text f{int(star_num) / 10:.1f} 星 # 时间comment-time 里的 title 或者文本 time_node item.select_one(span.comment-time) comment_time time_node.get(title, ) if time_node else # 有用数vote-count 节点 vote item.select_one(span.vote-count) vote_count vote.text.strip() if vote else 0 # 短评正文span.short content_node item.select_one(span.short) content content_node.text.strip() if content_node else records.append({ author: nickname, rating: rating_text, time: comment_time, votes: vote_count, content: content, }) return records代码解析选择器div.comment-item定位每条评论的容器昵称、评分、时间、有用数、正文都在固定子节点中。这里用select_one所以某个字段缺失不会让整个脚本崩溃。参数说明select_one(span.allstar40, span.allstar50, ...)是一个并集选择器实际豆瓣评分节点class就是allstar40这样的格式数字为星数乘以10。如果你只想要特定星级的评论也可以自行过滤。解析时间时推荐读取title属性因为文本里的时间格式可能带“刚刚”等相对时间而title里是标准ISO时间更方便排序。3.3 请求模块带上请求头与分页控制解析函数准备好后需要写请求函数。注意我们把Cookie、User-Agent、Referer放在一个字典里并且用一个Session对象来保持连接。这样做的原因是豆瓣会短暂记住你访问的主页和评论页之间的关系如果用独立的requests.get每次连接都是独立的更容易被判定为脚本。import requests import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://movie.douban.com/subject/, Accept-Language: zh-CN,zh;q0.9, } def fetch_short_comments(session, subject_id, start0, limit20): url fhttps://movie.douban.com/subject/{subject_id}/comments params { start: start, limit: limit, status: P, sort: new_score, } # 注意这里可以用 session.get因为 session 里保存了 cookie resp session.get(url, paramsparams, headersHEADERS, timeout15) if resp.status_code 403: raise RuntimeError(f访问被拒绝start{start}请检查Cookie或UA) resp.raise_for_status() return resp.text代码里的params会由requests自动拼接成查询字符串。statusP表示只看“看过”的用户短评这类评论通常更正式如果你需要“在看/想看”的评论可以改F或W。每次请求之间必须间隔否则会很快触发反爬。建议间隔时间设置在2到5秒之间随机不要固定一个值。3.4 多页爬取与CSV保存单页搞定后多页只是循环start偏移量。豆瓣普通短评列表最多能翻到500条左右再往后需要登录或者进入“全部短评”的聚合页。我们以一个电影ID为例抓取前10页每页20条共200条。代码def crawl(subject_id, max_pages10): session requests.Session() session.headers.update(HEADERS) # 把浏览器里的cookie放进session # 注意填Cookie时不要带 Cookie 前缀只填 value 字符串 session.cookies.update({your_cookie_key: your_cookie_value}) all_records [] for page in range(max_pages): start page * 20 try: html fetch_short_comments(session, subject_id, startstart) except RuntimeError as e: print(e) break records parse_comment_page(html) all_records.extend(records) print(f第{page1}页抓到{len(records)}条) # 随机间隔 2~5 秒让请求看起来更接近人工浏览 time.sleep(random.uniform(2, 5)) return all_records if __name__ __main__: subject_id 1292052 # 改成你要抓的电影ID data crawl(subject_id, max_pages3) print(f总共抓取 {len(data)} 条短评)补充说明CSV保存import pandas as pd def save_to_csv(records, filenamedouban_comments.csv): df pd.DataFrame(records) # utf-8-sig 编码能避免 Excel 打开中文乱码 df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f已保存到 {filename}共{len(df)}条)参数说明subject_id是豆瓣电影在URL中的编号可以通过搜索电影后从地址栏复制max_pages要谨慎建议从3页开始跑通确认字段无误后再增加。如果打印出来的记录数和页面实际数量对不上多半是解析结构变了回到2.3用开发者工具重新看节点。你可能会问为什么这个版本没有处理登录过期如果你的Cookie失效通常第二次请求就返回403。我们在第4章讲重试和限速时会补上。4. 反爬识别与请求频率控制状态码、间隔和重试4.1 豆瓣的反爬状态码403与302分别说明什么豆瓣的反爬机制并不神秘但你需要学会看状态码。简单来说如果请求返回403 Forbidden说明请求被服务器判定为不被允许的客户端常见原因是缺Cookie、IP频繁、UA异常。如果返回302并跳到passport.douban.com说明服务器要求你登录或者验证常见于未带Cookie访问“全部短评”接口或者当前IP被要求重新验证。在实际运行中我的习惯是写一个简单的响应检查函数def check_response(resp): if resp.status_code 403: print(403IP或请求头被拒尝试等待并更换UA) return False if resp.status_code 302: print(302被要求登录/安全验证检查Cookie并稍后重试) return False if 验证码 in resp.text or 安全验证 in resp.text: print(遇到验证码页面停止抓取并延长间隔) return False return True这个方法的价值不只是打印状态而是帮你快速定位原因。例如出现验证码页面时状态码可能是200如果只看状态码会误以为成功所以一定要检查响应文本关键字。4.2 随机间隔与限速设计请求频率是反爬的第一道门槛。很多翻车案例是循环里没有sleep几十个请求在几秒内触发然后IP被限制一段时间。短评页的合理间隔至少是2秒稳妥一点是3到6秒并且用随机值。固定间隔其实也有规律反爬系统会统计请求时间差完全等间隔反而更容易被发现。我通常这样实现限速import random import time def random_delay(min_sec2.0, max_sec5.0): delay random.uniform(min_sec, max_sec) time.sleep(delay)可以在每次请求前调用。如果你要抓多个电影建议每切换一个subject_id额外等待10秒以上模拟从搜索到进入页面的过程。这里不是玄学而是从请求来源规律上降低被关联的概率。另外limit参数不要调得太大。老版短评接口单页支持20条新版可能支持到50条但拉取条数越多单次响应体越大也会更显眼。新手从一开始就养成“抓得少、间隔大”的习惯后面做大规模采集时才不会踩无线程控制层面的坑。4.3 重试与异常处理的兜底逻辑网络请求不可能永远成功超时、连接重置、响应不完整都会发生。我们最好给请求函数加一个简单的重试机制。这里不引入第三方重试库用函数本身循环处理即可def request_with_retry(session, url, params, headers, max_retries3): for attempt in range(1, max_retries 1): try: resp session.get(url, paramsparams, headersheaders, timeout15) if check_response(resp): return resp except requests.exceptions.RequestException as e: print(f第{attempt}次请求异常{e}) wait_time 30 * attempt print(f等待 {wait_time} 秒后重试) time.sleep(wait_time) raise RuntimeError(重试三次后仍然失败)这里的等待时间采用线性增长第一次失败等30秒第二次等60秒第三次等90秒。你也可以换成指数退避但短评爬取场景线性就够了因为大概率不是服务器故障而是触发了频控。重试时要注意如果响应文本里出现“验证码”再等多久都没有意义因为你的Cookie或IP已经进入风险名单这种情况下继续重试只会加重限制。应该停下来换一个会话或者手动确认后再继续。4.4 代理池什么时候需要什么时候不需要很多教程会把代理池写得很复杂。坦白说抓个几百条短评完全不需要代理。proxy只有在两种情况才需要考虑一是同一个IP有大量并发需求比如每天抓上万条二是IP已经被封需要换个出口继续。对学习项目来说代理不只是成本还会带来稳定性问题免费代理经常失效响应延迟不稳定反而让你的代码更脆。如果一定要用requests的代理参数很简单proxies { http: http://127.0.0.1:7890, https: http://127.0.0.1:7890, } session.proxies.update(proxies)但这里不推荐抓豆瓣短评时使用代理原因是豆瓣本身有地域验证代理出口如果被其他爬虫污染过你会更容易被拦截。与其依赖代理不如老老实实降低频率、用你自己的真实网络环境。5. 避坑指南短评爬取中最容易翻车的五个细节5.1 现象爬到一半IP被限制全部请求返回403原因爬取脚本没有控制请求频率或者短时间内在同一个电影的不同分页上连续请求被计量识别为异常流量。有些情况下仅仅是被解析到设置了浏览器环境指纹但你的requests库特征与浏览器不同即使有UA也容易误伤。解决立即停止程序等待30分钟到数小时。恢复后把请求间隔提升到5秒以上并且每次启动从第1页开始试跑确认能正常返回200再继续。如果是长时间大批量使用建议在每次请求前随机切换User-Agent从一批常用UA里选择不要所有请求都用同一个UA字符串。同时可以用Session维持cookie而不是每次重建会话。5.2 现象短评内容被截断只剩“展开”之前的几十字原因豆瓣为了页面美观和加载性能超过某一长度的短评默认只展示一部分在HTML中只返回截断文本完整内容需要点击“展开”后通过接口加载。我们的span.short选择器只能拿到截断后的文本。解决判断逻辑上如果短评正文末尾出现“展开”相关标识说明存在截断此时可以放弃抓取完整内容或者改抓>content_node item.select_one(span.short) content content_node.text.strip() if content_node.get(data-original): content content_node[data-original].strip()数据可用性上大多数短评不超过140字截断情况不多。如果确实需要完整文本可以转而访问每条评论对应的短评接口点击展开时网络面板里能看到但这样会让请求量翻倍需要重新评估频率限制。5.3 现象Cookie失效后请求仍返回200但数据是空的原因Cookie失效时豆瓣可能不会直接拒绝而是返回一个包含“请登录”的通用页面状态码依然是200。如果你只检查状态码脚本会继续往下跑最终解析出0条评论。解决在parse_comment_page之后加一个记录数量判断如果为0且页面文本包含“请登录”“安全验证”等关键字则抛出异常停止。同时定期手动确认浏览器中豆瓣仍是登录状态因为Cookie的session有效期通常只有几天几天前复制的一般已经失效。建议每次运行前重新复制或者使用更稳定的登录态方案但不要自动登录那会涉及更多的验证风险。5.4 现象CSV打开后中文乱码或emoji变成问号原因默认使用utf-8编码写CSV而Excel在Windows上默认以GBK打开会看到乱码如果文本包含emojiExcel的CSV格式本身不支持良好直接显示?或错误。解决用encodingutf-8-sig写CSV保存时保留BOM头Excel即可正确识别。如果希望彻底避免emoji问题可以在写入前用encode(ascii, ignore)丢弃非ASCII字符但那样会损失信息。更好的办法是直接保存为JSONL或使用SQLite不经过Excel显示。对文本分析来说JSONL是更合适的选择。5.5 现象解析出的时间全是“今天”“昨天”而非标准日期原因豆瓣页面为了用户体验列表页显示相对时间而标准时间藏在了title属性中。如果直接用span.comment-time的文本内容拿到的就是“刚刚”“昨天”这类相对时间。解决优先读取time_node.get(title)title属性通常是一个标准ISO格式。如果title不存在再后退到文本内容并自己写一个相对时间转换函数。这个坑在时间序列分析时影响很大不要以为抓到“参差不齐”的时间就是网站问题。另外提醒一个容易忽略的聚合页的短评排序每次刷新会变化所以如果你要做统计分析必须把抓取时间crawled_at也记录下来。6. 把抓到的短评变成初步分析结果评分分布与快速词频代码跑通后别急着扩大抓取范围先用已有的几百条数据验证质量。我习惯做两个最小分析一是看评分分布是否和你对该电影的认知一致二是用简单的词频统计看看高频词。评分分布可以直接用pandas完成import pandas as pd df pd.read_csv(douban_comments.csv) rating_counts df[rating].value_counts().sort_index() print(rating_counts)这里需要确认rating字段是“4.0 星”这样的字符串如果是可以提取数字df[rating_num] df[rating].str.replace(星, ).astype(float) df[rating_num].plot.bar() # 需要matplotlib词云是另一个常见展示方式。如果不想安装额外的中文分词库可以先用jieba分词并统计词频然后画词云。安装依赖pip install jieba wordcloud matplotlib代码import jieba from collections import Counter all_text .join(df[content].tolist()) words jieba.lcut(all_text) # 过滤掉单字、停用词和标点这里简单示意 filtered [w for w in words if len(w) 1 and w not in [什么, 没有, 就是, 一个, 这部, 电影]] counter Counter(filtered).most_common(30) print(counter)wordcloud做图时注意两点第一需要指定中文字体否则中文全部显示成方框第二词云的随机性比较大不要把它当作严谨的分析结论。它更适合作为验证数据可用的装饰性输出。如果你继续做情感分析可以按评分把短评分成“好评”评分4和“差评”评分2再对比两类文本的高频词。这样能简单看出观众抱怨最多的是什么。对于分析项目这个流程足够了。做完整轮之后我自己的教训是爬取这件事最宝贵的不是代码而是对目标网站节奏的尊重。以前我图快把间隔调到0.5秒结果IP被封了一天连正常上网都受影响。后来老老实实随机延迟、控制总量反而稳定跑完了计划数据。希望你也能从这篇笔记里少走这个弯路先小批量跑通再谈放大。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑