资讯动态

Python爬虫实战:豆瓣电影Top250数据采集完整指南

发布时间:2026/10/13 4:49:01 来源:尧图企业网站定制
做爬虫开发这几年我前前后后写过不少练手项目但要说最经典、最适合拿来入门和温习的“豆瓣电影Top250爬虫”绝对排得上号。这个榜单数据规整、字段完整、分页清晰页面结构多年来保持稳定抓取下来做数据分析、做推荐系统、做个人电影清单都非常顺手。不过这两年豆瓣陆续调整了访问策略很多老教程里的代码直接跑已经拿不到数据了网上还流传着各种过时的请求头写法新手照着抄踩坑无数。这篇博文我会把当前可用、实测稳定的完整代码和思路一次讲清楚从页面分析、请求策略、解析逻辑到数据存储最后附上完整可运行的脚本适合刚接触Python爬虫的初学者也适合想快速拿一份干净Top250数据的老手直接复用。1. 项目定位与整体思路1.1 为什么选豆瓣Top250作为爬虫练手目标爬虫这个技能听起来高大上本质上就是“用代码代替人去浏览器里点开页面、把想要的信息复制下来”。既然是练手目标网站的选择非常关键。豆瓣电影Top250几乎是公认的黄金样例原因有三点第一数据质量极高。榜单里每部电影都有排名、片名、评分、评语、导演主演信息、上映年份、国家、类型等结构化字段抓下来直接就能进Excel或者数据库不需要做太多清洗。对比那些需要从JSON接口里拼拼凑凑才能拿到完整信息的站点豆瓣的老式HTML页面反而干净得可爱。第二反爬门槛适中。它不要求登录、不要求复杂加密参数但也不是完全不设防。UA校验、访问频率控制、响应内容校验这些机制足以让新手体会到“写爬虫不是写完就完事”的工程感又不至于劝退。处理它积累的经验可以平移到绝大多数内容型网站上。第三页面结构非常稳定。Top250的列表页和详情页字段布局多年没大变过意味着你写的解析规则可以长时间复用。这对学习“选择器怎么写才不容易被更新搞坏”特别有价值真正工作里你会遇到更多一言不合就改版的前端这种稳定站点能让新手把精力集中在爬虫本身而不是跟反爬斗智斗勇。1.2 目标页面与数据结构拆解在写代码之前先把目标拆清楚。Top250榜单的URL结构是这样的https://movie.douban.com/top250?start0filterstart参数从0开始每隔25个跳一次对应10个页面分别返回排名第1-25、26-50、51-75直到第226-250的电影。这个规律很直观本质就是一个支持分页的普通GET请求没有登录态没有动态加载没有复杂的翻页交互。这种页面结构在任何爬虫项目里都是最基础的那一类非常适合做整套流程的样本。每个列表页里包含25个电影条目li每个条目里我能拿到这些字段排名条目内em标签中的数字电影中文名.title中的第一个值评分.rating_num中的数字精确到小数一句话短评.quote .inq中的文案部分条目没有评价人数span中“xx人评价”的文本详情页链接.hd a中的href其中前四个字段是Top250列表页直接提供的精华信息后两个如果想深入挖掘还可以顺着详情页链接继续抓导演、主演、剧情简介、上映日期等。本篇博文先把列表页的字段抓完整把整个过程跑通后续扩展方向在文末也会提到。这个数据结构的核心特征是“规整”同一部电影的同类信息永远在同类标签里选择器可以稳定复用。用生活化的类比来说这就像去一家每个文件都用统一命名规则的档案室我只需要记住“抽屉第几层第几个格子放什么类型文件”就能把整屋档案快速抄完。1.3 技术选型与方案权衡选技术栈这件事我见过太多人一上来就上重型框架结果光配置环境就折腾了一整天。其实这个量级的项目一套极简组合就能做得很舒服请求库requests。Python生态里最经典的HTTP库API简洁、文档齐全、Session机制好用。对于这种简单的GET请求它毋庸置疑是首选。HTML解析BeautifulSoup4lxml解析器。BS4的选择器语法非常直观新手友好lxml作为底层解析器速度比纯Python的html.parser快不少。数据存储标准库csv如果要输出Excel再加一个openpyxl或pandas。为什么不推荐Scrapy不是说它不好而是这个项目实在没必要。Scrapy的优点在于分布式、管道、中间件、并发调度这是百万级页面采集才需要反复权衡的东西。爬250条数据用Scrapy就像开着卡车去买菜——能装但停车、点火、加油门的成本都摆在那里。先用手动循环把逻辑吃透后续真遇到大规模需求再切Scrapy理解起来反而更容易。有人可能还会问为什么不用Selenium这类浏览器自动化工具它确实能绕过很多JS渲染问题但Top250列表页是纯服务端渲染根本没有动态加载用浏览器模拟纯属浪费资源。而且浏览器自动化对内存、CPU的占用是普通HTTP请求的几十倍250条数据跑下来慢得令人抓狂。记住一个原则能用JSON接口解决的不用HTML解析能用HTML解析解决的不用浏览器能轻则轻这是爬虫工程师的基本素养。2. 反爬机制分析与合规访问策略2.1 当前页面常见的访问限制很多从旧教程抄代码的人最常遇到的困惑是代码明明没变昨天还能跑今天怎么突然403了这是因为站点侧的防爬规则是动态调整的它会根据请求特征决定“你是真人还是机器”。豆瓣Top250当前最常见的限制手段有这么几类一是请求头校验。服务器会检查User-Agent、Accept、Accept-Language等头信息判断请求是否来自常见浏览器。如果你用默认的python-requests/2.x.x几乎必被识别直接返回403。二是高频访问控制。同一个IP在短时间内发起过多请求即使UA没问题也会被临时限制通常表现为响应内容变成验证页或者状态码直接403。三是行为特征分析。正常人不会每0.1秒刷一页而且访问节奏会有随机性。爬虫如果固定间隔或完全不间隔在服务端的访问日志里看起来就像一台机器在精准操作。四是请求参数校验。部分接口会校验Referer头是否来自站内页面如果缺失或者来自外部也可能拒绝响应。这些机制单独看都不复杂但组合在一起就能筛掉绝大多数“裸奔”的爬虫。应对思路倒也很明确把自己伪装得像个正常人而不是把自己武装成黑客。不需要搞什么逆向破解老老实实把请求头补全、把请求节奏放慢数据就能拿到。2.2 请求头与会话维护的细节请求头里最关键的是User-Agent。很多老教程直接写死一个Mozilla/5.0的UA这在以前很管用但现在很多站点会校验UA对应的浏览器版本是否合理。比如你写个Chrome 76的UA当前市面上早就更新到120了配合其他特征一看就是假的。建议的做法是查询当前主流浏览器的版本号在UA里使用合理的新版本号把Accept、Accept-Language、Connection等头一起配上形成一个完整的“浏览器指纹”如果网站对Cookie有要求用requests.Session()维护会话自动处理Set-Cookie等流程用Session还有一个额外的好处就是同一个会话实例内TCP连接可以复用Sent请求会通过Connection: keep-alive保持连接既符合浏览器的行为特征也能降低频繁建立连接的超时风险。代码里直接session.get(...)比裸requests.get(...)更像真实浏览器。2.3 限速、重试与异常退避策略绝大多数爬虫被限制不是技术不行是太贪心。Top250只有10个页面哪怕每页等3秒总共也就半分钟的事根本不影响效率。把访问频率主动降下来反而让脚本的稳定性提升好几个量级。这里有两组参数值得用心调一是请求间隔。固定间隔虽然简单但反而“不太像人”。我习惯用随机间隔比如random.uniform(2, 4)在2到4秒之间随机抖动。这样做的好处是访问节奏分布不均匀监控侧更难用规则判定你是程序。二是重试机制。requests的网络请求受网络波动、服务端临时拒绝等因素影响一次失败不代表永远失败。我会给每个页面设置最多3次重试第一次失败等2秒第二次失败等4秒第三次失败等8秒。这个“指数退避”策略可以让脚本在轻微触发限流后自动恢复而不是崩到只剩一个异常栈。重要提醒重试时如果碰到403不要立刻无限重试先停下来检查UA和访问频率适当增加等待时间。爬虫工程的真谛不是写得快而是写得稳。3. 完整代码实现与逐段拆解3.1 请求函数的正确写法首先是我最常用的请求封装。它不是单纯把requests.get包一层而是把超时、UA、重试、状态码校验全部结构化管理import random import time import requests from requests.adapters import HTTPAdapter BASE_URL https://movie.douban.com/top250 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.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.5, Connection: keep-alive, Upgrade-Insecure-Requests: 1, } def make_session(): session requests.Session() session.headers.update(HEADERS) # 挂载适配器把连接池数和重试次数统一管理 adapter HTTPAdapter(pool_connections10, pool_maxsize10, max_retries0) session.mount(https://, adapter) session.mount(http://, adapter) return session这里有两个细节值得解释第一User-Agent里我写的是当前主流内核的合理版本号而不是随便抄一个。如果你的代码拿到之后过两三年再跑记得先核对一下版本号是否还在合理范围。第二HTTPAdapter的max_retries我设置成了0因为我想用自己写的带退避策略的循环而不是依赖requests默认的盲目重试。requests内置重试遇到403也会傻傻重试3次既浪费时间又增加被限制的风险。make_session()创建好的会话对象我会在后面的main()里复用而不是每个页面重建。一个会话从头用到尾更贴近浏览器的状态特征。超时设置非常重要绝不能省略。没加timeout的请求遇到服务器卡住可能无限等下去整个脚本就挂在那里。我这里的请求函数会沿用3.1的session做GET请求def fetch_page(session, start): params { start: start, filter: , } for attempt in range(3): try: resp session.get(BASE_URL, paramsparams, timeout10) if resp.status_code 403: # 被限制的表现不要死磕直接等待后进入下一轮尝试 wait_time random.uniform(3, 6) print(f[警告] 收到403等待 {wait_time:.1f}s 后重试) time.sleep(wait_time) continue resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: wait_time 2 ** attempt random.uniform(0.5, 1.5) print(f[错误] 请求失败(start{start}): {e}等待 {wait_time:.1f}s) time.sleep(wait_time) return None2 ** attempt random.uniform(0.5, 1.5)这个写法就是指数退避第一次失败等2.5到3.5秒第二次等4.5到5.5秒第三次等8.5到9.5秒。配合随机抖动既不显得机械化又能有效降低限流风险。3.2 解析逻辑准确写出选择器请求拿到HTML后解析代码就不该再反复用正则去硬抠了。BS4的选择器不仅可读性强而且当页面结构微调时改起来也方便。Top250的列表项结构非常清晰每一部电影都包在li标签里外层是ol.grid_view。from bs4 import BeautifulSoup def parse_page(html): soup BeautifulSoup(html, lxml) movie_list soup.select(ol.grid_view li) page_data [] for item in movie_list: rank item.select_one(.pic em).get_text(stripTrue) title_element item.select_one(.title) title title_element.get_text(stripTrue) if title_element else rating_element item.select_one(.rating_num) rating rating_element.get_text(stripTrue) if rating_element else quote_element item.select_one(.quote .inq) quote quote_element.get_text(stripTrue) if quote_element else # 附加信息评价人数 people_element item.select_one(.star span:nth-of-type(4)) people_count people_element.get_text(stripTrue) if people_element else # 详情页链接 link_element item.select_one(.hd a) detail_url link_element.get(href) if link_element else page_data.append({ 排名: rank, 片名: title, 评分: rating, 一句话短评: quote, 评价人数: people_count, 详情页链接: detail_url, }) return page_data关于选择器有几个容易踩坑的地方我多说一句。.title在页面上会有中文名、英文名或原名、其他语种名而select_one(.title)只返回第一个匹配项正好是中文名。如果某部电影没有中文译名第一个.title可能就是原名所以不写死第几个更稳妥。.quote .inq有部分电影没有短评所以要用条件判断否则取到None再调.get_text会直接报错。现实中写解析器最忌讳的就是“假设所有数据都存在”宁可多写一个if也不要让一条脏数据毁掉整个任务。关于评价人数我用的是.star span:nth-of-type(4)。这个页面结构细节不同时间段可能有微调如果取不到值调试时打开浏览器开发者工具对着元素检查一下对应的span序号调整一下选择器即可。正是因为这类细节我强烈建议所有解析代码都写成“取不到就用空字符串兜底”的防御性写法而不是直接让整个脚本崩溃。3.3 主流程与CSV/Excel双存储输出主流程就是把10页跑完、解析完、汇总后落盘。这里也顺带给出两套存储方案一套是纯标准库写CSV零额外依赖另一套是pandas写Excel做数据分析更方便。先看最精简但完整的存储函数import csv def save_to_csv(movies, filenamedouban_top250.csv): if not movies: print(没有数据可存储) return fieldnames [排名, 片名, 评分, 一句话短评, 评价人数, 详情页链接] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(movies) print(f数据已保存至 {filename})编码用utf-8-sig不是utf-8这里是个关键经验带BOM的UTF-8编码直接双击用Excel打开时才不会乱码。如果你用纯utf-8Excel默认按ANSI解析中文会变成一堆问号。这个细节能帮你省下大把“为什么中文乱码”的排查时间。如果用pandas输出Excel代码也非常直白import pandas as pd def save_to_excel(movies, filenamedouban_top250.xlsx): if not movies: print(没有数据可存储) return df pd.DataFrame(movies) df.to_excel(filename, indexFalse) print(f数据已保存至 {filename})pandas的to_excel底层依赖openpyxl所以需要先手动安装pip install pandas openpyxl对比两种格式CSV轻量、通用、任何工具都能打开适合当存档和交换格式Excel自带列宽、筛选、多工作表能力适合做可视化分析。日常我两个都生成反正也不费事。最后是main()函数核心是分页循环和频率控制def main(): session make_session() all_movies [] for page in range(10): start page * 25 print(f正在抓取第 {page 1} 页start{start}) html fetch_page(session, start) if html is None: print(f第 {page 1} 页抓取失败跳过) continue movies parse_page(html) print(f本页抓到 {len(movies)} 条记录) all_movies.extend(movies) # 随机延时让访问节奏更像真人 if page 9: delay random.uniform(2, 4) print(f随机等待 {delay:.1f}s) time.sleep(delay) print(f总计抓取 {len(all_movies)} 条记录) if all_movies: save_to_csv(all_movies) # save_to_excel(all_movies) # 需要 pandas 时解除注释 else: print(本次运行未抓到有效数据请检查网络或页面结构变化) if __name__ __main__: main()这段代码的可读性很强10页循环、每页间隔随机延时2到4秒、抓取失败跳过不中断。运行完你的目录下会出现一个douban_top250.csv里面就是完整的250条电影榜单数据。第一次跑通后建议打开CSV检查一下“排名”列是否严格从1排到250这个简单的校验能快速发现分页逻辑有没有出问题。4. 运行验证、结果检查与优化扩展4.1 数据质量检查别急着欢呼代码跑通、文件生成只能说流程走完了数据质量还不能打包票。我每次写完爬虫落盘后的第一件事不是看总数而是抽查几个维度的完整性。第一个维度是记录数。250条一条不多一条不少是基准线但偶尔会因为网络抖动漏掉一页只有225条。这时不要拿“能跑就行”来安慰自己漏一页会导致榜单整体错位、排名对不上号数据就没意义了。第二个维度是字段完整性。重点看“评分”和“片名”如果存在空字符串定位到具体页码先确认是页面本身缺失还是解析逻辑漏了。Top250的条目里“短评”可以为空但“片名”和“评分”绝不可能为空只要出现空值九成是解析代码在选择器上出了问题。第三个维度是重复性。可以用一个极短的脚本检查import csv with open(douban_top250.csv, r, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) ranks [row[排名] for row in rows] titles [row[片名] for row in rows] print(f总记录数: {len(rows)}) print(f排名是否唯一: {len(set(ranks)) len(ranks)}) print(f片名是否重复: {len(set(titles)) ! len(titles)})如果排名有重复说明分页循环里start计算错了或者翻页逻辑出现了重复抓取如果片名重复但排名不同反而要警惕是不是榜单本身更新了某些电影存在多个版本。这种小检查脚本一次性写好以后每次跑完都顺带跑一遍比眼睛刷Excel可靠得多。4.2 断点续抓与失败重试的改造思路当前版本的主流程是“失败就跳过再继续”已经保证了单次运行不会因为某一页失败而整体崩盘。但如果你是拿这份数据做月度追踪可能希望“没抓到的页自动补回来”这就涉及断点续抓的改造思路。方案很朴素设计两个列表success_pages和failed_pages。每页抓完并解析成功就往success_pages里记页码抓取失败就记进failed_pages。末尾再对failed_pages做一轮重试或者干脆把失败页写入一个pending.json下次运行脚本时自动检查之前是否有未完成页面先补完再抓新的。实际工作中这种“记录断点、补抓失败”的微创新比单纯加并发线程带来的体验提升还要大。因为你真正踩坑时会发现失败集中在网络波动和限流瞬间慢而稳地补抓比用几十个并发硬刚更容易成功。4.3 继续延伸多线程、定时任务与榜单追踪如果你已经掌握了上面这套基础爬虫的完整流程下一步的扩展方向我建议按顺序走第一个方向是“采集深度扩展”。Top250列表页只是入口顺着详情页链接可以抓更多字段比如导演、主演、上映日期、制片国家、类型、片长、剧情介绍甚至短评。用深度优先遍历的思路去抓250个详情页数据量不大但能极大扩充分析维度。这时候会自然需要引入简单的去重集合和URL队列爬虫框架的雏形就出来了。第二个方向是“自动化与定时”。用系统的计划任务工具每晚定时跑一次脚本配合CSV存档里的时间戳就能形成“每日Top250快照”。这个快照数据用来做榜单趋势分析特别有意思今天谁是第一、上个月谁跌出榜单、评分波动是怎么发生的。我把之前一个周更的快照数据做成了简单图表发现榜单其实比想象中活跃很多这种个人数据积累的成就感是直接抄现成数据集完全比不了的。第三个方向才是“性能优化”。真的需要更快采集时优先考虑concurrent.futures.ThreadPoolExecutor控制两三个线程并发抓取首页同时维持随机延时。注意线程池的规模千万不要贪大爬虫的瓶颈永远在对方服务器的容忍度而不在你的本机速度。一次把并发拉到10个以上结果大概率不是数据翻倍而是全部403。5. 常见问题与排查技巧实录5.1 高频问题速查表以下是我在实际运行中见过最多的几类问题找了一张表整理出来方便直接对照排查问题现象可能原因解决方案运行即返回403User-Agent缺失或版本过旧更换为当前主流浏览器的真实UA补全请求头列表页能打开但解析为空列表CSS选择器与当前页面结构不匹配打开浏览器开发者工具重新确认标签层级中文乱码读写文件编码不一致写入CSV时用utf-8-sig用Excel打开不乱码偶尔一页没抓到网络波动或触碰限流加大请求间隔增加指数退避重试总共只有225条数据分页循环的页数或start计算不对确认总共10页start从0开始、步长25抓取过程越来越慢直至全挂访问频率过高被临时限制停止运行等待一小段时间再继续同时降低频率某字段显示为空但页面有内容选择器不够准确细查当前标签是否有多个匹配项调整nth-of-type等选择器这张表基本覆盖了新手跑Top250爬虫90%的问题。排错思路有一个共性永远先去浏览器里手动打开相同URL对照真实页面结构再回头看代码。很多人在代码里反复改选择器不如直接F12看一眼来得快。5.2 我踩过的几个典型坑第一个坑是过度追求“高并发”。刚学爬虫的时候总觉得一堆线程满天飞很酷结果就是跑不了几条就把自己IP玩进去最后只能等限制解除。后来我把“并发量”三个字从脑子里删掉给自己的规矩是“稳优先、快其次”。事实证明对一个250条数据的榜单来说串行加随机延时整体耗时不超过50秒所谓并发优化完全没必要。第二个坑是没有做好防御式解析。早期我在解析短评时直接用item.select_one(.quote .inq).get_text(stripTrue)某天突然发现有一部电影的短评为空然后整个任务直接中断。从那以后我把“取不到数据就给默认值”当成了铁律。爬虫是处理脏数据的活别对页面结构抱有不切实际的信任。第三个坑是忽视了编码细节。CSV文件用纯utf-8保存用Excel打开就是乱码。如果你也是习惯生成数据后直接用Excel看结果的记得用utf-8-sig。这个改动只需要一行代码带来的体验提升是巨大的。第四个坑是拿到数据不做校验。跑完看打印250条就以为大功告成后来分析时才发排名列出现乱序原来是某一页解析时把列表顺序搞乱了。数据落盘前先看头尾、看排名连续性、看总数这些都在4.1里讲过成本极低但收益极高。5.3 关于合规与长期可用的建议写爬虫这件事我一直坚持几个原则请求频率必须克制、采集数据只用于学习与个人分析、不给目标网站造成任何负担。Top250本来就是公开榜单抓取它写进个人项目没有任何问题但也要注意不要将拿到的数据用于商业用途或大规模传播。每个人都有权维护自己的服务器稳定运行爬虫工程师更应该将心比心用合理的方式获取公开信息。同时要意识到任何站点的页面结构都可能调整代码失效是常态而非异常。正因如此我把本文中的每个选择器都解释了“为什么这么写”而不是只丢一段黑盒代码。当你理解了页面结构背后的逻辑即使某天选择器失效你也能快速跟进改版迭代而不是束手无策。这也是爬虫技术最值得掌握的核心能力不是永远能跑而是永远知道怎么排查、怎么修、怎么让它继续跑。这份代码从设计到落地贯穿的主线就是“稳”。合理伪装请求头、随机延时、指数退避、防御式解析、双重存储、落盘校验每一步都朴实但有效。如果你只是想要一份最新的Top250数据直接复制上面的完整代码装好依赖就能跑如果你想通过这个项目把爬虫基本功扎扎实实打通那就顺着拆解思路自己动手把每一行代码敲一遍把每一个选择器用浏览器开发者工具对照着看一遍。250条数据不多但从小处把细节磨到位遇到更大规模、更复杂的采集项目时你才会感谢今天这份“慢工出细活”的底子。

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

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

免费获取报价 →
↑