Python爬虫实战:豆瓣电影TOP250数据采集与反爬策略详解

📅 2026/8/7 10:52:57
Python爬虫实战:豆瓣电影TOP250数据采集与反爬策略详解
1. 项目概述与核心价值豆瓣电影TOP250榜单对于任何一个对电影感兴趣的人来说都是一个绕不开的“宝藏片单”。它不仅是影迷的观影指南更是数据分析、内容运营乃至学术研究的重要数据源。这个项目就是通过技术手段自动化地获取这份榜单的全部精华信息。我们不仅要拿到电影名和评分更要深入到每一部电影的详情页提取导演、编剧、主演、类型、上映时间、片长、评分人数和剧情简介等结构化数据。听起来像是简单的网页抓取没错核心确实是网络爬虫。但实际操作起来你会发现从简单的“拿到数据”到“稳定、准确、完整地拿到高质量数据”中间隔着无数个需要精心处理的细节。比如如何应对豆瓣的反爬机制如何处理页面结构的微小变动如何将非结构化的HTML文本清洗成规整的结构化数据这些才是这个项目的真正挑战和价值所在。我做过很多次类似的数据采集豆瓣是其中“教学意义”非常强的一个目标。它既不像某些纯静态网站那样毫无防备也不像一些大型平台那样铜墙铁壁它的反爬策略恰到好处地覆盖了爬虫工程师需要掌握的大部分核心技能。通过完成这个项目你不仅能得到一份珍贵的电影数据集更能系统地掌握一套应对中等强度反爬网站的实战方法。无论你是想用这些数据做推荐系统冷启动、做电影市场分析还是单纯想练手这个项目都能让你满载而归。2. 技术方案选型与整体设计思路面对这样一个需求技术选型是第一步也是决定后续开发效率和稳定性的关键。我们需要一个能够发送HTTP请求、解析HTML、提取数据并最终存储的方案。2.1 核心工具链选择编程语言Python。这几乎是爬虫领域的“官方语言”生态极其丰富从简单的脚本到复杂的分布式系统都能胜任。其简洁的语法和强大的库支持能让我们快速实现想法。HTTP请求库Requests。相比于Python内置的urllibRequests库的API设计更加人性化会话保持、代理设置、请求头定制都非常方便是我们与豆瓣服务器对话的“嘴巴”。HTML解析库BeautifulSoup4 (bs4)。当我们需要从复杂的HTML标签森林中精准地找到目标数据时BeautifulSoup就像一把瑞士军刀。它支持多种解析器如lxml能让我们通过标签名、CSS类名、ID等属性轻松定位元素对于豆瓣这种结构相对清晰的页面来说是性价比最高的选择。也有人喜欢用PyQueryjQuery风格或lxml直接进行XPath解析但BeautifulSoup在易读性和上手速度上优势明显。数据存储CSV文件。考虑到TOP250只有250条记录每条记录包含十多个字段数据量很小使用轻量级的CSV逗号分隔值文件是最佳选择。它无需安装数据库直接用Excel或文本编辑器就能打开查看便于后续的数据交换和初步分析。如果数据量巨大或需要复杂查询才会考虑SQLite或MySQL。可选但重要的库time/random用于在请求间插入随机延时模拟人类操作避免请求过快被服务器封禁。json豆瓣的部分数据如评分可能通过AJAX加载返回JSON格式需要此库解析。pandas虽然不是必须但用pandas来最终处理和保存为CSV会非常优雅和高效。2.2 爬取策略设计分步走稳扎稳打豆瓣TOP250的页面是分页的每页25部电影。我们的爬虫需要像翻书一样一页一页地抓取。整体流程设计如下抓取列表页循环访问https://movie.douban.com/top250?start{page_num}其中page_num从0开始每次增加25直到抓完10页。解析列表页获取详情页链接在每一页的HTML中找到每部电影对应的详情页URL例如/subject/1292052/。访问详情页对于获取到的每一个详情页链接构造完整的URL如https://movie.douban.com/subject/1292052/并进行访问。解析详情页提取目标字段在详情页的HTML中定位并提取我们需要的所有信息片名、导演、评分等。数据清洗与存储将提取到的文本信息进行清洗去除多余空格、换行符等然后以结构化的格式例如字典暂存起来最终统一写入CSV文件。礼貌爬取与异常处理在每一步请求之间设置合理的延时对可能出现的网络错误、页面结构变化、反爬导致的访问失败等情况进行捕获和处理确保程序不会意外崩溃。这个设计的关键在于分离关注点列表页抓取器只负责获取链接详情页解析器只负责提取数据。这样的代码结构清晰易于维护和调试。3. 核心细节解析与关键实操要点理论说完我们进入实战中最容易出问题的环节。豆瓣的反爬机制虽然不是最严苛的但足够让一个粗心的爬虫新手寸步难行。3.1 请求头Headers的伪装艺术直接使用Requests.get()而不做任何伪装你的请求很可能在第一步就被识别为爬虫并被拒绝。服务器通过HTTP请求头中的User-Agent等字段来判断客户端类型。关键技巧必须设置一个常见的浏览器User-Agent。你可以使用Chrome、Firefox等浏览器的标准UA字符串。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } response requests.get(url, headersheaders)但这还不够。一个真实的浏览器请求会携带更多的头信息。为了提高成功率建议至少加上Referer表明你从哪个页面跳转过来对于豆瓣可以设为列表页URL和Accept-Language。headers { User-Agent: ..., Referer: https://movie.douban.com/top250, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }3.2 应对频率限制与封禁策略即使伪装了头部高频访问同一网站仍然危险。豆瓣会对短时间内来自同一IP的过多请求进行限制可能返回403错误或者更“温柔”地返回一个验证页面。核心策略降低请求频率。固定延时在每个请求无论是列表页还是详情页之后使用time.sleep()暂停几秒。例如time.sleep(3)。随机延时更推荐固定延时模式化明显。更好的做法是在一个区间内随机休眠例如time.sleep(random.uniform(2, 5))这样更接近人类不规律的点击行为。使用会话Sessionrequests.Session()可以保持会话自动处理cookies在某些需要登录态或反爬策略基于会话的场景下很有用。虽然豆瓣TOP250无需登录但使用Session是一个好习惯。重要警告绝对不要尝试多线程或异步并发疯狂抓取豆瓣这几乎肯定会导致你的IP被暂时或永久封禁。这个项目的目的在于学习和获取数据而非挑战网站的反爬极限。“慢就是快”在爬虫伦理和稳定性上是金科玉律。3.3 页面解析与数据定位的“寻宝图”这是数据提取的核心也是最需要耐心和技巧的部分。你需要使用浏览器的开发者工具F12来仔细分析豆瓣页面的HTML结构。以电影《肖申克的救赎》详情页为例电影标题通常位于span propertyv:itemreviewed标签内或者是一个id为content的div下的第一个h1标签。注意标题可能包含上映年份需要清洗。导演、编剧、主演这些信息通常在一个id为info的div中。每一行信息由span标签标注属性名如“导演”后面跟着对应的链接或文本。解析时需要按“属性名”来查找其后的兄弟节点。难点info区域的结构并非完全规范的键值对span的类名也可能变化。最稳健的方法是先找到idinfo的整个区块然后将其文本按换行符分割再对每一行用“:”或空格进行分割和判断。评分与评分人数评分通常在strong propertyv:average标签内。评分人数在span propertyv:votes标签内。这两个标签一般相邻可以通过属性精准定位。剧情简介简介可能有多段并且有时会有“展开全部”的隐藏内容。通常位于span propertyv:summary标签内或者类名包含summary的div中。需要处理换行符和首尾空格。类型、上映日期、片长这些信息同样在idinfo的div中并且有对应的property属性例如类型对应propertyv:genre上映日期对应propertyv:initialReleaseDate片长对应propertyv:runtime。利用这些属性可以非常精确地定位。实操心得不要试图用一个复杂的XPath或CSS选择器一次性定位所有内容。分而治之是更好的策略。先定位到大的信息容器如#info再在这个容器里逐一查找小项目。这样即使页面局部结构微调你的解析器也不至于完全崩溃。另外所有提取到的文本都要经过.strip()处理去除多余空白字符。4. 完整代码实现与分步详解下面我将结合代码详细拆解每一个步骤。请注意豆瓣的页面结构可能会随时间更新以下代码基于某个时间点的页面结构编写核心思路是通用的。4.1 环境准备与依赖安装首先确保你的Python环境建议3.6以上已经就绪然后安装必要的库pip install requests beautifulsoup4 pandas如果安装lxml解析器速度更快可以一并安装pip install lxml4.2 核心代码实现我们将代码分为几个函数保持模块化。import requests from bs4 import BeautifulSoup import time import random import pandas as pd import re def get_list_page_urls(): 生成TOP250所有列表页的URL base_url https://movie.douban.com/top250 urls [] for start in range(0, 250, 25): # 共10页每页25条 url f{base_url}?start{start} urls.append(url) return urls def get_movie_detail_links(list_url, headers): 从一个列表页中解析出所有电影的详情页链接 detail_links [] try: resp requests.get(list_url, headersheaders) resp.raise_for_status() # 如果状态码不是200抛出异常 resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 找到所有class为‘hd’的div里面的第一个a标签的href就是详情页链接 for item in soup.find_all(div, class_hd): a_tag item.find(a) if a_tag and a_tag.has_attr(href): detail_links.append(a_tag[href]) # 随机延时模拟人工 time.sleep(random.uniform(1, 3)) except requests.RequestException as e: print(f请求列表页失败 {list_url}: {e}) except Exception as e: print(f解析列表页失败 {list_url}: {e}) return detail_links def parse_movie_detail(detail_url, headers): 解析单个电影详情页提取所需信息 movie_info { 排名: None, 中文片名: None, 评分: None, 评分人数: None, 导演: None, 编剧: None, 主演: None, 类型: None, 上映时间: None, 片长: None, 剧情简介: None, 链接: detail_url } try: resp requests.get(detail_url, headersheaders) resp.raise_for_status() resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 1. 提取电影标题 (排名需要在列表页获取这里先处理片名) title_tag soup.find(span, propertyv:itemreviewed) if title_tag: # 标题可能形如“肖申克的救赎 The Shawshank Redemption (1994)”我们取第一部分 full_title title_tag.text movie_info[中文片名] full_title.split()[0] if full_title else None # 2. 提取评分和评分人数 rating_tag soup.find(strong, propertyv:average) if rating_tag: movie_info[评分] rating_tag.text votes_tag soup.find(span, propertyv:votes) if votes_tag: movie_info[评分人数] votes_tag.text # 3. 提取info区域的所有信息 info_div soup.find(div, idinfo) if info_div: info_text info_div.get_text(|, stripTrue) # 用‘|’替换换行便于分割 # 这是一个简化的处理更稳健的方法是遍历info_div下的所有span # 这里演示通过属性查找 # 导演 directors info_div.find_all(a, relv:directedBy) if directors: movie_info[导演] /.join([d.text for d in directors]) # 编剧 (注意编剧可能没有特定的property结构不稳定) # 一种方法是找到“编剧”文本所在的span然后获取其后的所有a标签 writer_span info_div.find(span, stringre.compile(r编\s*剧)) if writer_span: writers [] for sibling in writer_span.find_next_siblings(): if sibling.name a: writers.append(sibling.text) elif sibling.name span: # 可能有多个编剧用/分隔 break if writers: movie_info[编剧] /.join(writers) # 主演 actors info_div.find_all(a, relv:starring) if actors: movie_info[主演] /.join([a.text for a in actors[:5]]) # 取前5位 # 类型 genres info_div.find_all(span, propertyv:genre) if genres: movie_info[类型] /.join([g.text for g in genres]) # 上映时间 release_date info_div.find(span, propertyv:initialReleaseDate) if release_date: movie_info[上映时间] release_date.text # 片长 runtime info_div.find(span, propertyv:runtime) if runtime: movie_info[片长] runtime.text # 4. 提取剧情简介 summary_span soup.find(span, propertyv:summary) if summary_span: # 清理简介文本去除多余空格和换行 summary summary_span.text.strip().replace(\n, ).replace(\r, ).replace( , ) movie_info[剧情简介] summary # 再次随机延时 time.sleep(random.uniform(2, 4)) except requests.RequestException as e: print(f请求详情页失败 {detail_url}: {e}) except Exception as e: print(f解析详情页失败 {detail_url}: {e}) return movie_info def main(): 主函数串联整个流程 all_movies_data [] headers { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://movie.douban.com/, } print(开始获取列表页链接...) list_urls get_list_page_urls() movie_rank 1 # 初始化排名计数器 for list_url in list_urls: print(f正在处理列表页: {list_url}) detail_links get_movie_detail_links(list_url, headers) for link in detail_links: print(f 正在抓取排名第{movie_rank}的电影: {link}) movie_info parse_movie_detail(link, headers) movie_info[排名] movie_rank # 添加排名信息 all_movies_data.append(movie_info) movie_rank 1 # 每抓取一部电影后稍作休息 time.sleep(random.uniform(3, 6)) # 详情页访问间隔可以稍长 # 保存数据到CSV if all_movies_data: df pd.DataFrame(all_movies_data) # 调整列顺序让排名和片名在前 columns_order [排名, 中文片名, 评分, 评分人数, 导演, 编剧, 主演, 类型, 上映时间, 片长, 剧情简介, 链接] df df[columns_order] df.to_csv(douban_top250_movies.csv, indexFalse, encodingutf-8-sig) # utf-8-sig解决Excel中文乱码 print(f数据抓取完成共抓取{len(all_movies_data)}部电影。数据已保存到 douban_top250_movies.csv) else: print(未抓取到任何数据。) if __name__ __main__: main()代码要点解析get_list_page_urls函数利用豆瓣分页规律?start参数生成10个列表页URL。get_movie_detail_links函数发送请求时传入了精心构造的headers。使用BeautifulSoup查找所有class为hd的div标签再取其下的第一个a标签的href属性。这是定位详情链接最稳定的方法之一。加入了异常处理和随机延时。parse_movie_detail函数核心初始化一个字典用于存储单部电影的所有信息。标题提取通过propertyv:itemreviewed定位并做了简单清洗。评分提取通过propertyv:average和v:votes定位非常精准。info区域提取这是最复杂的部分。代码展示了两种方法属性定位法推荐用于有明确属性的字段如导演(v:directedBy)、类型(v:genre)、片长(v:runtime)。这种方法最稳定。文本查找法用于无明确属性的字段如“编剧”通过查找包含“编”和“剧”的span标签再获取其后的兄弟节点中的链接。这种方法比较脆弱因为页面上的“编剧”二字可能变化。简介提取通过propertyv:summary定位并进行文本清洗。函数返回一个填充好的字典。main函数串联整个流程获取所有列表页 - 遍历每个列表页获取详情链接 - 遍历每个详情链接解析数据。添加了排名字段并在循环中递增。使用pandas.DataFrame将数据列表转换为表格并保存为CSV文件。utf-8-sig编码确保用Excel打开时不会出现中文乱码。延时策略在列表页请求后、详情页请求后都设置了随机延时且详情页之间的延时3-6秒比列表页1-3秒更长模拟更仔细的浏览行为。5. 常见问题、反爬应对与排查技巧实录即使代码写好了在运行过程中你几乎一定会遇到各种问题。下面是我踩过坑后总结的排查清单。5.1 请求被拒绝返回403或验证页面症状requests.get()返回的状态码是403或者返回的HTML内容包含“检测到异常请求”、“请输入验证码”等字样。原因请求头伪装不足或请求频率过高触发了反爬。解决方案检查并丰富Headers确保User-Agent是有效的并添加Referer和Accept-Language。有时甚至需要添加Accept-Encoding和Connection。显著降低请求频率将time.sleep的间隔时间加大例如详情页间隔改为random.uniform(5, 10)。在深夜或凌晨运行脚本服务器压力较小。使用IP代理如果本地IP已被封禁这是最终手段。你可以寻找一些免费的HTTP代理IP池但免费代理往往不稳定。在Requests中使用代理proxies {http: http://your-proxy:port, https: https://your-proxy:port}然后在get请求中传入proxiesproxies。注意使用代理需谨慎确保其合法性。模拟更完整的浏览器行为考虑使用Selenium这类浏览器自动化工具它能完全模拟真实用户操作但速度极慢资源消耗大仅作为最后的选择。5.2 解析不到数据返回None症状程序没报错但提取到的导演、评分等字段都是None。原因页面结构已经发生变化你代码中的CSS选择器或属性定位失效了。排查步骤手动打开目标页面用浏览器访问你正在抓取的URL按F12打开开发者工具。确认元素是否存在在“元素”Elements面板使用搜索功能CtrlF搜索你代码中使用的关键属性如propertyv:average。如果搜不到说明属性名变了。寻找新的定位方式观察目标数据如评分在HTML结构中的新位置。它可能被包裹在新的标签或类名里。尝试使用更通用的父级容器结合文本内容来定位。更新解析逻辑根据新的页面结构修改parse_movie_detail函数中的查找逻辑。永远不要假设网页结构是永恒不变的这是爬虫需要维护的根本原因。5.3 数据错乱或重复症状CSV文件中某一行的导演信息跑到了片名栏或者同一部电影出现了两次。原因定位不精确你的选择器可能匹配到了多个元素只取了第一个但这个元素并不是你想要的。列表页链接去重不彻底理论上豆瓣不会重复但你的解析逻辑可能从不同地方拿到了同一个链接。排名计数逻辑错误如果在循环中重置了movie_rank变量会导致排名重复。解决方案在解析函数中每提取一个字段后立即打印出来看看是否正确。使用print(f“导演: {directors}”)进行调试。使用更精确的选择器组合例如soup.find(div, idinfo).find(span, text导演).find_next(a)。在将detail_links加入总列表前可以先用set()去重虽然对于这个有序列表可能没必要但这是个好习惯。检查movie_rank变量的作用域和递增逻辑确保它在整个主循环中只初始化一次并持续递增。5.4 保存的CSV文件在Excel中中文乱码症状用文本编辑器打开正常用Excel打开中文是乱码。原因Excel默认使用的编码不是UTF-8。解决方案使用pandas的to_csv方法时指定encodingutf-8-sig。utf-8-sig会在文件开头添加一个BOM字节顺序标记Excel识别到这个标记后就会以UTF-8编码打开文件。5.5 程序意外中断如何断点续爬对于250条数据一次性跑完问题不大。但如果抓取几万条数据网络波动可能导致程序中断重新开始会浪费时间和流量。简易断点续爬策略保存进度在抓取每部电影成功后立即将该条数据追加写入CSV文件使用modea而不是等所有数据抓完再一次性写入。这样即使中断也已保存的数据不会丢失。记录已抓取的URL将成功抓取的详情页URL保存到一个单独的文本文件或集合中。在每次开始抓取新链接前先检查该链接是否已经存在于这个“已抓取集合”中如果存在则跳过。更复杂的方案可以使用数据库记录每一条任务的状态待抓取、抓取中、已完成、失败然后有一个调度程序从“待抓取”队列中取任务。这适用于大型爬虫项目。最后请务必记住爬虫行为应遵守网站的robots.txt协议虽然豆瓣对TOP250的限制相对宽松并尊重网站的资源。将请求频率控制在合理范围避免对目标服务器造成不必要的负担。你通过这个项目学到的技术思路和问题解决方法其价值远大于最终得到的那250行数据。