
1. 项目概述从零到一用Python抓取豆瓣电影Top 250最近在整理个人观影记录想看看还有哪些经典没看过直接去豆瓣翻Top 250列表太麻烦了一页页翻还得手动记录。作为一个Python爱好者第一反应就是写个脚本把它“搬”下来。这个“Python爬取豆瓣Top 250保存至csv文件”的项目听起来简单但麻雀虽小五脏俱全它几乎涵盖了入门级网络爬虫的所有核心环节请求网页、解析数据、应对反爬、数据清洗和持久化存储。对于刚学完Python语法想找个实战项目练手的朋友来说这再合适不过了。它不仅能让你巩固requests、BeautifulSoup、pandas这些库的使用更能让你亲身体会从互联网上自动化获取并结构化数据的完整流程。今天我就把自己实现这个项目的详细过程、踩过的坑以及一些优化思路分享出来你可以跟着一步步操作最终得到一个包含电影排名、名称、评分、评价人数、经典台词等信息的整洁CSV文件。2. 核心思路与工具选型解析2.1 项目目标与需求拆解我们的核心目标是自动化获取豆瓣电影Top 250榜单的所有信息并以结构化的格式CSV保存到本地。拆解开来需要完成以下几个子任务遍历所有页面Top 250榜单分页展示共10页每页25部电影。我们需要能自动拼接出所有页面的URL。发送网络请求模拟浏览器访问这些页面获取页面的HTML源代码。解析HTML内容从复杂的HTML代码中精准地提取出每部电影的排名、名称、评分、评价人数、导演、主演、年份、地区、类型、简介、经典台词等信息。数据清洗与存储将提取出的文本信息进行清洗如去除多余空格、处理缺失值然后以行每部电影和列每个字段的形式保存到CSV文件中。2.2 技术栈选择与理由为什么选择以下工具这是基于简单、高效、社区支持度高的原则。请求库requests。这是Python中最简单易用的HTTP库相比原生urllib其API设计非常人性化几行代码就能完成请求发送和响应获取是爬虫入门的不二之选。解析库BeautifulSoup。我们的目标页面是静态HTML结构相对清晰。BeautifulSoup提供了非常直观的“选择器”方式来定位元素例如通过标签名、CSS类名来查找对于新手来说学习曲线平缓远比正则表达式友好。虽然lxml解析速度更快但BeautifulSoup的易用性在初期更具优势。数据存储csv模块与pandas。Python内置的csv模块足以完成基本的写入操作。但这里我选择用pandas因为它不仅能轻松写入CSV其DataFrame数据结构更便于我们在内存中进行数据清洗、分析和预览是数据处理的事实标准。辅助工具time与User-Agent。time.sleep()用于在请求间插入延迟是尊重网站、避免被封IP的基本礼仪。而设置随机的User-Agent请求头是为了让我们的请求看起来更像来自不同的浏览器是应对基础反爬的常见手段。注意在开始任何爬虫项目前务必查看目标网站的robots.txt文件通常是https://www.douban.com/robots.txt和其服务条款。虽然豆瓣对Top 250页面的爬取相对宽容用于个人学习但我们必须控制请求频率严禁高并发、高频次访问以免对服务器造成压力。3. 环境准备与页面结构分析3.1 安装必要的Python库打开你的命令行终端CMD、PowerShell或Terminal使用pip命令安装我们所需的库。建议在虚拟环境中进行。pip install requests beautifulsoup4 pandas如果安装速度慢可以使用国内镜像源例如清华源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 pandas。3.2 分析豆瓣Top 250页面结构这是爬虫最关键的一步决定了我们如何写解析代码。我们打开豆瓣电影Top 250的第一页https://movie.douban.com/top250?start0。观察URL规律注意URL中的参数start0。点击第二页URL变为start25第三页是start50。由此可知每页显示25部电影start参数表示从第几部开始显示。我们可以用循环生成10个URLstart (page-1) * 25。使用开发者工具定位元素在浏览器页面右键选择“检查”或“审查元素”打开开发者工具。点击左上角的箭头图标或按CtrlShiftC然后去点击页面上第一部电影《肖申克的救赎》的整个条目区域。在Elements面板中你会发现这个条目被包裹在一个div classitem的标签内。所有250部电影都被包含在多个div classitem中。这是我们提取数据的循环单元。在每一个.item内部我们再细找排名通常在em class标签里。电影名称在span classtitle里注意可能有中文名和英文名/别名。评分在span classrating_num里。评价人数在div classstar里的最后一个span。简介Quote在span classinq里但不是每部电影都有。其他信息导演、演员、年份等在div classbd里的p class中这是一段混合的文本需要用字符串方法进行分割提取。通过以上分析我们得到了一个清晰的“地图”先找到所有.item然后在每个.item里像查字典一样根据特定的CSS类名去提取对应的信息。4. 分步实现爬虫核心代码4.1 构建请求头与实现单页抓取首先我们导入库并设置请求头这是为了让自己看起来更像一个普通的浏览器访问。import requests from bs4 import BeautifulSoup import pandas as pd import time import random # 定义请求头模拟浏览器访问 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 }User-Agent字符串可以从你浏览器的开发者工具Network标签中找到任意一个请求复制过来。接下来我们写一个函数fetch_one_page(url)用于获取单页HTML并解析出该页25部电影的数据。def fetch_one_page(url): 抓取并解析单个页面 # 发送HTTP GET请求 resp requests.get(url, headersheaders) # 如果状态码不是200抛出异常 resp.raise_for_status() # 设置正确的编码豆瓣使用utf-8 resp.encoding utf-8 # 将响应文本转换为BeautifulSoup对象指定解析器为html.parser soup BeautifulSoup(resp.text, html.parser) movie_list [] # 用于存储当前页所有电影信息的列表 # 找到当前页面所有的电影项目每个div classitem代表一部电影 for item in soup.find_all(div, class_item): movie {} # 用字典存储一部电影的所有信息 # 1. 提取排名 rank_tag item.find(em) movie[排名] rank_tag.text if rank_tag else # 2. 提取标题可能包含中文名和英文名 title_tag item.find(span, class_title) movie[标题] title_tag.text if title_tag else # 提取英文名/别名可能在第二个class为title的span里 other_title_tag title_tag.find_next_sibling(span, class_title) if title_tag else None movie[其他标题] other_title_tag.text.replace(/, ).strip() if other_title_tag else # 3. 提取评分 rating_tag item.find(span, class_rating_num) movie[评分] rating_tag.text if rating_tag else 0.0 # 4. 提取评价人数 star_div item.find(div, class_star) if star_div: # 找到star div下所有的span最后一个包含评价人数 spans star_div.find_all(span) if len(spans) 4: # 例如“1254564人评价” 我们需要提取数字 eval_text spans[3].text movie[评价人数] eval_text.replace(人评价, ).strip() else: movie[评价人数] 0 else: movie[评价人数] 0 # 5. 提取简介经典台词/短评 quote_tag item.find(span, class_inq) movie[简介] quote_tag.text if quote_tag else # 6. 提取其他信息导演、主演、年份、地区、类型 # 这些信息都在 div classbd 下的 p class 里是一个混杂的字符串 bd_p item.find(div, class_bd).find(p, class_) if bd_p: info_text bd_p.get_text(stripTrue) # 获取纯文本并去除两端空格 # 示例字符串“导演: 弗兰克·德拉邦特 Frank Darabont 主演: 蒂姆·罗宾斯 Tim Robbins /...\n1994 / 美国 / 犯罪 剧情” # 我们用换行符‘\n’分割字符串 parts info_text.split(\n) if len(parts) 2: # 第一部分包含导演和主演信息 director_actor parts[0] # 第二部分包含年份、地区、类型 year_country_genre parts[1] # 进一步分割导演和主演以“主演:”为界 if 主演: in director_actor: director_part, actor_part director_actor.split(主演:, 1) movie[导演] director_part.replace(导演:, ).strip() movie[主演] actor_part.strip() else: movie[导演] director_actor.replace(导演:, ).strip() movie[主演] # 处理年份、地区、类型 # 格式可能是“1994 / 美国 / 犯罪 剧情” 用‘/’分割 ycg_parts year_country_genre.split(/) ycg_parts [p.strip() for p in ycg_parts] movie[年份] ycg_parts[0] if len(ycg_parts) 0 else movie[地区] ycg_parts[1] if len(ycg_parts) 1 else movie[类型] ycg_parts[2] if len(ycg_parts) 2 else else: # 如果分割失败全部放入一个字段 movie[其他信息] info_text movie[导演] movie[主演] movie[年份] movie[地区] movie[类型] else: movie[导演] movie[主演] movie[年份] movie[地区] movie[类型] movie_list.append(movie) return movie_list这个函数是核心中的核心。它完成了从定位元素到提取文本的所有脏活累活。注意其中使用了.find()和.find_all()方法以及.text属性获取标签内文本。对于混杂的“其他信息”我们用了字符串分割的方法来提取结构化字段这是一种非常实用的技巧。4.2 实现多页遍历与数据汇总单页抓取函数写好之后遍历所有页面就很简单了。我们构造所有页面的URL循环调用fetch_one_page并将结果合并。def fetch_all_pages(): 抓取所有10页数据 base_url https://movie.douban.com/top250?start{} all_movies [] # 存储所有250部电影 for page in range(10): # 0到9共10页 start page * 25 url base_url.format(start) print(f正在抓取第 {page1} 页URL: {url}) try: one_page_movies fetch_one_page(url) all_movies.extend(one_page_movies) print(f 成功抓取 {len(one_page_movies)} 部电影信息。) except Exception as e: print(f 抓取第 {page1} 页时出错: {e}) # 非常重要的延迟避免请求过快 # 随机睡眠2-5秒模拟人类操作 time.sleep(random.uniform(2, 5)) print(f所有页面抓取完成总计 {len(all_movies)} 部电影。) return all_movies这里有两个关键点异常处理用try...except包裹单页抓取这样即使某一页出错如网络波动程序也不会崩溃而是记录错误后继续抓取下一页。请求延迟time.sleep(random.uniform(2, 5))是爬虫的道德底线。不加延迟的快速连续请求极易触发网站的反爬机制导致IP被暂时封禁。随机延迟让请求行为更接近真人浏览。4.3 数据清洗与保存至CSV抓取到的原始数据可能包含一些空白、格式不一致的问题用pandas处理起来非常方便。def save_to_csv(movie_list, filenamedouban_top250.csv): 将电影列表保存为CSV文件 # 将字典列表转换为DataFrame df pd.DataFrame(movie_list) # 数据清洗示例 # 1. 确保“评价人数”是数值类型用于后续排序或计算 df[评价人数] pd.to_numeric(df[评价人数], errorscoerce).fillna(0).astype(int) # 2. 确保“评分”是浮点数类型 df[评分] pd.to_numeric(df[评分], errorscoerce).fillna(0.0) # 3. 处理可能的空字符串替换为NaN或特定占位符 df.replace(, pd.NA, inplaceTrue) # 选择我们想要的列顺序 columns_order [排名, 标题, 其他标题, 评分, 评价人数, 导演, 主演, 年份, 地区, 类型, 简介] df df[columns_order] # 保存到CSV文件不保存行索引使用UTF-8编码确保中文不乱码 df.to_csv(filename, indexFalse, encodingutf-8-sig) # ‘utf-8-sig’能让Excel正确识别编码 print(f数据已成功保存至文件: {filename}) print(df.head()) # 打印前几行预览一下使用utf-8-sig编码是一个重要技巧它会在文件开头添加一个BOM字节顺序标记这样用微软Excel打开CSV文件时中文字符就不会显示为乱码。如果你只用文本编辑器或Python处理用utf-8也可以。4.4 主函数与完整脚本整合最后我们将所有功能整合到一个主函数中。def main(): print(开始抓取豆瓣电影Top 250...) all_movies fetch_all_pages() if all_movies: save_to_csv(all_movies) print(任务完成) else: print(未抓取到任何数据。) if __name__ __main__: main()将上述所有代码块按顺序保存到一个.py文件中例如douban_top250_spider.py在终端运行python douban_top250_spider.py就可以看到程序开始一页页抓取并在完成后生成一个douban_top250.csv文件。5. 进阶优化与反爬策略应对基础的爬虫写好了但它还很脆弱。在实际运行中你可能会遇到各种问题。下面分享一些进阶优化和应对策略。5.1 处理动态加载与请求失败我们当前的脚本假设每次请求都能成功。但网络是不稳定的。增加重试机制可以使用requests的适配器或自己写循环在请求失败时如遇到连接超时、状态码5xx重试几次。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session(retries3, backoff_factor0.5): session requests.Session() retry_strategy Retry( totalretries, backoff_factorbackoff_factor, # 重试等待时间{backoff factor} * (2 ** ({retry number} - 1)) status_forcelist[500, 502, 503, 504] # 遇到这些状态码才重试 ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter) return session # 使用session代替直接的requests.get session create_session() resp session.get(url, headersheaders, timeout10) # 设置超时使用代理IP如果你的IP因请求过快被封可能需要使用代理IP池。但这对于豆瓣Top 250这种低频、礼貌的爬取通常不是必须的优先级低于控制频率。5.2 数据解析的健壮性提升网页结构可能会微调我们的解析代码不能太“硬编码”。使用更灵活的CSS选择器BeautifulSoup支持CSS选择器语法soup.select()有时比find更简洁。# 例如提取所有电影项目 items soup.select(div.item) # 提取单个电影的标题 title item.select_one(span.title).text增加字段缺失的容错处理我们在基础代码中已经用了if tag:的判断这是很好的习惯。可以进一步为每个字段提供默认值。5.3 将数据存入数据库可选CSV文件适合一次性分析和存档但如果数据需要频繁查询或更新存入SQLite或MySQL数据库是更好的选择。import sqlite3 def save_to_sqlite(movie_list, db_namedouban_top250.db): conn sqlite3.connect(db_name) cursor conn.cursor() # 创建表 cursor.execute( CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, rank INTEGER, title TEXT, other_title TEXT, rating REAL, eval_count INTEGER, director TEXT, actors TEXT, year TEXT, region TEXT, genre TEXT, quote TEXT ) ) # 插入数据 for movie in movie_list: cursor.execute( INSERT INTO movies (rank, title, other_title, rating, eval_count, director, actors, year, region, genre, quote) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( movie.get(排名), movie.get(标题), movie.get(其他标题), movie.get(评分), movie.get(评价人数), movie.get(导演), movie.get(主演), movie.get(年份), movie.get(地区), movie.get(类型), movie.get(简介) )) conn.commit() conn.close() print(f数据已保存至SQLite数据库: {db_name})6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。这里是我的排查记录。6.1 请求被拒绝返回403或418错误现象requests.get()抛出异常状态码是403 Forbidden或418 I‘m a teapot。原因网站识别出你是爬虫程序。最常见的原因是请求头User-Agent被识别为Python脚本。解决检查并完善headers确保User-Agent是有效的浏览器字符串。可以多准备几个每次请求随机选择一个。添加更多请求头模拟更真实的浏览器例如添加Referer告诉服务器你从哪个页面跳转过来、Accept-Language等。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)..., Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.douban.com/, Connection: keep-alive, }终极方案使用Selenium模拟浏览器。如果网站有复杂的JavaScript验证或动态加载requests无法获取完整页面。这时需要Selenium控制Chrome或Firefox等真实浏览器去渲染页面再获取源码。但这会大大增加复杂度和运行时间仅作为最后手段。6.2 抓取到的数据是空的或乱码现象movie_list为空或者中文显示为乱码。原因1网页结构已更新原来的CSS类名或标签路径失效。排查1重新打开豆瓣Top 250页面用开发者工具再次检查.item、.title等关键元素的类名是否变化。有时网站会进行A/B测试或改版。解决1更新代码中的选择器。尝试使用更通用的选择器比如item.find(‘span’, attrs{‘class’: ‘title’})。原因2编码问题。排查2打印resp.text的前500个字符看是否是乱码。解决2确保设置了resp.encoding ‘utf-8’。如果不行可以尝试resp.encoding resp.apparent_encoding让requests自动判断。6.3 程序运行中途停止或变慢现象抓取了几页后程序卡住或报错停止。原因最可能的原因是触发了反爬机制IP被限流或临时封禁。解决立即增加延迟将time.sleep的时间延长比如random.uniform(5, 10)。检查请求频率确保每页之间都有延迟不要在循环内快速连续请求。暂停程序等待一段时间如半小时再运行。对于豆瓣礼貌的、低频率的爬取通常不会被永久封禁。考虑使用更分布式的抓取如果是大规模抓取需要设计IP池、用户代理池但这已超出本入门项目的范畴。6.4 生成的CSV文件在Excel中打开中文乱码现象用记事本或代码编辑器打开CSV正常但用Excel打开中文是乱码。原因Excel默认使用系统区域编码如中文Windows是GBK打开CSV而我们的文件是UTF-8编码。解决在保存CSV时使用encoding‘utf-8-sig’。这个sig代表BOM它能让Excel自动识别为UTF-8编码。6.5 “评价人数”提取失败或为0现象评价人数全部是0或空。原因解析div class“star”下的结构不准确。网页结构可能微调或者我们用的索引不对。排查与解决打印出star_div的内容看看。star_div item.find(div, class_star) print(star_div) # 观察其内部HTML结构找到包含“人评价”文本的那个span标签。 # 可能需要改用更精确的查找方式例如 eval_span star_div.find(span, stringlambda text: text and 人评价 in text) if eval_span: movie[‘评价人数’] eval_span.text.replace(‘人评价’, ‘’)这种基于文本内容查找的方式比依赖固定索引更健壮。这个项目虽然基础但贯穿了爬虫的核心逻辑。我建议你在成功运行基础版本后尝试自己添加新功能比如抓取电影的封面图片链接、影片时长或者将评分和评价人数转换为数值类型后尝试用pandas做简单的数据分析例如评分分布、哪个年份的高分电影最多。这些练习能让你对数据抓取和处理的流程有更深的体会。记住爬虫的世界里耐心和尊重规则比技术炫技更重要。