尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python爬虫实战:豆瓣电影TOP250数据抓取与解析全流程详解

Python爬虫实战:豆瓣电影TOP250数据抓取与解析全流程详解 1. 项目概述与核心价值最近在整理个人观影记录想建立一个更结构化的电影数据库豆瓣电影TOP250这个经典片单自然成了首要目标。手动一部部去抄录信息显然不现实250部电影每部要记录排名、评分、导演、演员等十几个字段工作量巨大且容易出错。于是一个自动化抓取数据的想法就诞生了写个脚本让程序去访问豆瓣TOP250的页面把每部电影的中文片名、排名、评分、详情页链接以及点进去之后的导演、编剧、主演、类型、上映日期、片长、评分人数和剧情简介统统给“搬”回来。这听起来像是一个典型的网络爬虫任务但它背后的价值远不止于“抓数据”。对于影迷来说你可以基于这个数据集做很多有趣的分析比如导演的作品评分分布、不同类型电影的评分规律、高评分电影在时长上的共性等等。对于学习者而言这更是一个绝佳的练手项目它涵盖了网页请求、HTML解析、数据清洗、结构化存储以及应对反爬策略等多个核心环节是检验和提升Python数据处理能力的实战场景。接下来我就把自己实现这个项目的完整思路、踩过的坑以及一些优化技巧分享出来。2. 整体设计与技术选型考量2.1 目标分析与页面结构预研在动手写代码之前必须先搞清楚我们要从哪抓、怎么抓。豆瓣TOP250的入口页面是一个列表页URL是固定的https://movie.douban.com/top250。这个页面以分页形式展示每页25部电影共10页。我们的首要任务是从这个列表页中提取出每部电影的核心概览信息排名、片名、评分、详情页链接。注意直接访问豆瓣页面时务必检查网页源代码确认所需数据是直接渲染在HTML中的即服务器端渲染而不是通过JavaScript动态加载的。这对于选择技术方案至关重要。通过浏览器开发者工具查看列表页的HTML结构我发现每部电影的信息都包裹在一个classitem的div标签内。排名信息藏在classpic的div里的em标签中片名和详情链接在classhd的div下的a标签里评分则在classstar的div下的span属性中。这意味着我们可以通过解析静态HTML来获取这些数据无需执行复杂的JavaScript。而更详细的字段如导演、编剧、主演等则存在于每部电影的独立详情页中。我们需要先从列表页拿到这250个详情页的URL再逐个访问并解析。详情页的HTML结构相对复杂信息分散在不同的div和span中并且经常夹杂着无关的空白字符和换行数据清洗会是一个重点。2.2 技术栈选择与工具链搭建基于上述分析我选择了以下技术栈这也是Python生态中处理这类任务的经典组合请求库Requests为什么选它Requests库以其简单优雅的API著称是处理HTTP请求的事实标准。对于豆瓣这种无需处理复杂会话和动态JS的静态页面抓取它完全够用。相比于urllib它的代码更简洁直观。解析库BeautifulSoup4 (bs4)为什么选它BeautifulSoup提供了非常灵活和强大的HTML/XML解析能力支持多种解析器如lxml,html.parser。它的查找语法如find(),find_all(),select()对于从复杂HTML中提取特定标签的内容极其方便。虽然lxml在解析速度上更快但bs4结合lxml解析器在易用性和性能上取得了很好的平衡。数据存储Pandas CSV/JSON为什么选它我们的最终目标是将250部电影、每条10多个字段的数据结构化地保存下来。Pandas的DataFrame对象是处理表格数据的利器可以方便地进行数据清洗、转换并一键导出为CSV或JSON文件。CSV格式通用便于用Excel打开进一步分析JSON则更适合网络传输或作为Web应用的数据源。这里我选择CSV作为主要输出格式。辅助工具Time Random为什么需要这是应对网站反爬机制的关键。短时间内发起大量请求很容易被服务器识别为爬虫并封锁IP。因此必须在请求之间插入随机延时例如time.sleep(random.uniform(1, 3))模拟人类浏览的间隔这是最基本的礼貌和自我保护策略。整个项目的流程可以概括为循环遍历TOP250的10个列表页 - 从每个列表页解析出25部电影的概览信息及详情页链接 - 循环遍历每个详情页链接 - 解析详情页获取详细信息 - 将所有数据整合并存入DataFrame- 导出为CSV文件。3. 核心实现步骤与代码详解3.1 环境准备与基础请求函数首先确保你的Python环境已经安装了必要的库。可以通过pip安装pip install requests beautifulsoup4 pandas lxmllxml是一个高效的解析器作为BeautifulSoup的依赖或后端能显著提升解析速度。接下来我们构建最基础的请求函数。这个函数需要处理请求头User-Agent和可能的异常。import requests from bs4 import BeautifulSoup import pandas as pd import time import random def get_page(url): 发送HTTP GET请求返回BeautifulSoup解析对象。 加入基本的异常处理和请求头模拟。 # 模拟浏览器请求头这是绕过基础反爬的必要步骤 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 } try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 # 使用lxml解析器速度比默认的html.parser快 soup BeautifulSoup(response.content, lxml) return soup except requests.RequestException as e: print(f请求 {url} 时出错: {e}) return None这个get_page函数是整个爬虫的基石。设置一个合理的User-Agent是关键的第一步它让我们的请求看起来更像来自一个普通的浏览器。异常处理则保证了程序的健壮性不会因为某个页面暂时无法访问而整体崩溃。3.2 解析列表页获取电影概览与链接TOP250列表页的URL有规律https://movie.douban.com/top250?start{start}filter其中start参数是每页起始电影的序号0, 25, 50, ...。我们需要编写一个函数来解析单个列表页提取出该页上25部电影的信息。def parse_list_page(soup): 解析一个TOP250列表页提取电影排名、中文片名、评分、详情链接。 movie_list [] # 找到所有class为‘item’的div每个div对应一部电影 items soup.find_all(div, class_item) for item in items: movie_info {} # 1. 排名 (在 classpic 的 div 里的 em 标签中) rank_tag item.find(div, class_pic).find(em) movie_info[rank] rank_tag.get_text() if rank_tag else N/A # 2. 片名和链接 (在 classhd 的 div 里的第一个 a 标签中) hd_div item.find(div, class_hd) if hd_div: a_tag hd_div.find(a) if a_tag: movie_info[detail_url] a_tag.get(href) # 片名可能在span里也可能直接是a标签的title属性这里取title更稳定 title_span a_tag.find(span, class_title) movie_info[title_cn] title_span.get_text().strip() if title_span else a_tag.get(title, N/A) else: movie_info[detail_url] N/A movie_info[title_cn] N/A else: movie_info[detail_url] N/A movie_info[title_cn] N/A # 3. 评分 (在 classstar 的 div 里第二个 span 的文本) star_div item.find(div, class_star) if star_div: rating_span star_div.find_all(span)[1] if len(star_div.find_all(span)) 1 else None movie_info[rating] rating_span.get_text().strip() if rating_span else N/A else: movie_info[rating] N/A movie_list.append(movie_info) return movie_list这里有几个实操心得片名提取豆瓣列表页上中文片名通常在一个classtitle的span标签里。但为了代码健壮性我加了一个回退机制如果找不到这个span就尝试获取a标签的title属性。a标签的title属性通常也包含完整的片名。评分提取classstar的div里有多个span分别代表“星级”、“评分”、“评价人数”。通过find_all(span)[1]可以稳定地取到第二个span即评分。异常处理每一步查找都使用了if...else进行判断并为缺失项赋值为N/A。这是因为网页结构可能微调或者个别电影信息缺失这样做可以防止程序因某个标签找不到而中断。3.3 解析详情页获取完整元数据详情页的信息更丰富也更具挑战性。所有信息基本都集中在idinfo的这个div里但它是用换行和冒号分隔的纯文本需要仔细拆分。def parse_detail_page(soup): 解析电影详情页提取导演、编剧、主演、类型、上映日期、片长、评分人数、简介。 detail_info {} # 主要信息区域 info_div soup.find(div, idinfo) if not info_div: return detail_info # 获取整个信息块的文本并按换行符分割 info_text info_div.get_text(\n, stripTrue) info_lines info_text.split(\n) # 初始化字段 fields [director, writer, starring, genre, release_date, runtime] for field in fields: detail_info[field] N/A # 遍历每一行根据冒号‘:’前的关键词进行匹配 for line in info_lines: if 导演 in line: # 分割后取后半部分并去除可能的多余空格 detail_info[director] line.split(:, 1)[-1].strip() elif 编剧 in line: detail_info[writer] line.split(:, 1)[-1].strip() elif 主演 in line: detail_info[starring] line.split(:, 1)[-1].strip() elif 类型: in line: # 注意类型后面有冒号更精确 detail_info[genre] line.split(:, 1)[-1].strip() elif 上映日期 in line: detail_info[release_date] line.split(:, 1)[-1].strip() elif 片长 in line: detail_info[runtime] line.split(:, 1)[-1].strip() # 评分人数 (在 classrating_sum 的 span 里) rating_sum_span soup.find(span, class_rating_sum) if rating_sum_span: # 提取数字部分例如“123456人评价” - “123456” import re num_text rating_sum_span.get_text(stripTrue) match re.search(r(\d,?\d*), num_text.replace(,, )) detail_info[rating_people] match.group(1) if match else N/A else: detail_info[rating_people] N/A # 剧情简介 (在 propertyv:summary 的 span 里通常 classall hidden 或 无class) # 先找隐藏的完整简介 summary_span soup.find(span, class_all hidden) if not summary_span: # 如果没找到隐藏的找显示的简短简介 summary_span soup.find(span, propertyv:summary) if summary_span: # 获取文本并清理首尾空格和换行 summary_text summary_span.get_text(stripTrue) # 替换掉简介开头可能出现的“简介”等字样 detail_info[summary] summary_text.replace(简介:, ).strip() else: detail_info[summary] N/A return detail_info解析详情页是最容易出错的环节原因如下文本结构不规则idinfo里的内容虽然人类一眼就能看懂但对程序来说字段顺序不固定分隔符可能不止冒号还可能夹杂着空格、斜杠等。我采用按行分割、关键词匹配的方式虽然代码稍长但容错性更好。评分人数提取文本是“123456人评价”我们需要的是数字“123456”。这里使用了正则表达式re.search(r(\d,?\d*), ...)来匹配数字部分并去掉了千分位逗号以便后续转为数值。剧情简介的两种状态豆瓣的简介有时是折叠的classall hidden需要点击“展开”才能看到全文有时是直接显示的。我们的代码逻辑是优先获取完整的隐藏简介如果没有再获取显示的简介。propertyv:summary是一个更稳定的属性选择器。3.4 主流程控制与数据整合现在我们把所有部分串联起来并加入礼貌的延时和进度提示。def scrape_douban_top250(): 主函数抓取豆瓣TOP250所有电影信息。 base_url https://movie.douban.com/top250 all_movies_data [] print(开始抓取豆瓣电影TOP250...) # 1. 遍历10个列表页 for page in range(10): start page * 25 list_url f{base_url}?start{start}filter print(f正在抓取列表页: {page 1}/10, URL: {list_url}) list_soup get_page(list_url) if not list_soup: print(f 列表页 {page1} 抓取失败跳过。) continue movie_previews parse_list_page(list_soup) print(f 解析到 {len(movie_previews)} 部电影预览信息。) # 2. 遍历当前页的每部电影抓取详情 for idx, movie in enumerate(movie_previews): detail_url movie.get(detail_url) if detail_url N/A: print(f 电影 {movie.get(title_cn, 未知)} 无详情链接跳过。) # 即使没有详情链接也保存已有的预览信息 all_movies_data.append(movie) continue print(f ({idx1}/{len(movie_previews)}) 抓取详情: {movie.get(title_cn)}) # 关键请求间隔模拟人类操作避免被封 time.sleep(random.uniform(2, 4)) # 间隔2到4秒 detail_soup get_page(detail_url) if not detail_soup: print(f 详情页抓取失败跳过。) # 详情页失败仍保存列表页信息 all_movies_data.append(movie) continue detail_info parse_detail_page(detail_soup) # 将详情信息合并到电影预览信息中 full_info {**movie, **detail_info} all_movies_data.append(full_info) # 每抓完一页列表也稍作休息 if page 9: # 最后一页不需要再等待 time.sleep(random.uniform(3, 6)) print(f 列表页 {page1} 完成等待片刻...\n) # 3. 转换为DataFrame并保存 print(\n所有页面抓取完成开始整理数据...) df pd.DataFrame(all_movies_data) # 定义理想的列顺序 column_order [rank, title_cn, rating, director, writer, starring, genre, release_date, runtime, rating_people, summary, detail_url] # 只保留DataFrame中存在的列 existing_columns [col for col in column_order if col in df.columns] df df[existing_columns] # 保存为CSV文件 csv_filename douban_top250_movies.csv df.to_csv(csv_filename, indexFalse, encodingutf-8-sig) # utf-8-sig 确保Excel打开不乱码 print(f数据已保存至: {csv_filename}) print(f共抓取 {len(df)} 条电影记录。) # 预览前几行数据 print(\n数据预览:) print(df.head()) return df这个主函数控制了整个抓取的节奏分页循环通过改变start参数构造10个列表页的URL。双层循环外层循环列表页内层循环每页的电影。内层循环中对每个详情页的访问都加入了time.sleep(random.uniform(2, 4))这是至关重要的反爬措施。错误处理与数据合并即使某个详情页抓取失败程序也不会崩溃而是跳过该电影并保留已从列表页获取的信息排名、片名、评分、链接。最后使用{**movie, **detail_info}将两个字典合并。数据清洗与保存使用Pandas的DataFrame整理数据并按照一个清晰的列顺序输出。使用utf-8-sig编码保存CSV这是为了兼容Windows系统下的Excel否则直接使用utf-8在Excel中打开中文可能会显示乱码。运行scrape_douban_top250()函数泡杯咖啡等待十几二十分钟一份完整的豆瓣TOP250电影数据集就到手了。4. 常见问题、反爬策略与优化技巧在实际运行中你几乎一定会遇到一些问题。下面是我踩过坑后总结的经验。4.1 请求被拒绝与反爬应对策略最直接的问题是收到403 Forbidden或Connection closed等错误。这意味着豆瓣的服务器识别出了你的爬虫并采取了限制措施。根本原因你的请求行为频率过高、无有效User-Agent、无Cookie等与正常浏览器差异太大。解决方案降低请求频率这是最有效的方法。我代码中设置的time.sleep(random.uniform(2, 4))是基础。对于豆瓣甚至可以延长到5-10秒。牺牲速度换取稳定性是值得的。完善请求头Headers除了User-Agent可以尝试添加Referer通常设置为上一个页面的URL和Accept-Language等字段让请求看起来更“自然”。headers { User-Agent: ..., Referer: https://movie.douban.com/, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, # 注意requests自动处理解码这里声明即可 Connection: keep-alive }使用会话Sessionrequests.Session()可以保持Cookie across多个请求模拟一个真实的会话过程有时能提高成功率。session requests.Session() session.headers.update(headers) # 为会话设置统一的headers response session.get(url, timeout10)设置代理IP高级如果单个IP被封锁可以考虑使用代理IP池。但这涉及到付费服务或自建代理复杂度较高对于豆瓣TOP250这种规模的项目通常通过“降频完善Headers”就能解决。重要提示在请求之间加入延时不仅是技术需要也是对网站资源的尊重。过于频繁的请求会增加服务器负载可能违反网站的服务条款。请务必以合理、节制的频率进行抓取。4.2 数据解析失败与字段错乱有时代码运行完了但打开CSV发现某些电影的导演信息跑到了编剧栏或者简介是空的。原因分析网页结构变化豆瓣前端偶尔会微调HTML的class或结构。这是爬虫需要维护的主要原因。信息缺失或格式特殊有些老电影可能没有“编剧”信息或者“上映日期”有多个不同国家。我们的简单关键词匹配可能失效。编码或空格问题提取的文本可能包含不可见的换行符\n、制表符\t或全角空格导致数据看起来混乱。排查与解决打印调试当解析失败时将出错的info_text或整个soup打印出来与浏览器中看到的实际HTML结构对比。这是最直接的调试方法。使用更稳健的选择器优先使用propertyv:directedBy、propertyv:genre这类带有语义化的属性选择器它们比普通的class更稳定。例如导演信息通常有a relv:directedBy。我们可以更新parse_detail_page函数# 使用property属性查找导演 director_links soup.find_all(a, relv:directedBy) if director_links: detail_info[director] /.join([link.get_text(stripTrue) for link in director_links])加强数据清洗对提取后的字符串使用.strip()、replace(\n, )、 .join(text.split())合并多余空格等方法进行清理。增加容错逻辑像之前代码那样为每个字段设置默认值N/A避免因单个字段缺失导致程序中断或数据结构错误。4.3 性能优化与代码健壮性提升当脚本稳定运行后可以考虑以下优化异常记录将抓取失败的URL和原因记录到日志文件或一个单独的列表中方便事后手动补抓或分析问题。failed_urls [] # 在抓取详情页的异常处理中 if not detail_soup: print(f详情页抓取失败: {detail_url}) failed_urls.append(detail_url) # ... 其他处理 # 最后保存失败记录 with open(failed_urls.txt, w, encodingutf-8) as f: for url in failed_urls: f.write(url \n)增量抓取如果数据需要更新可以设计一个机制只抓取排名有变动或新上榜的电影。这需要将已抓取的数据如电影ID或详情URL持久化存储并在下次运行时进行比对。使用更快的解析器我们已经使用了lxml这是Python中速度最快的HTML/XML解析器之一。确保你已正确安装 (pip install lxml)。异步请求高级对于大量独立页面的抓取可以使用aiohttp和asyncio进行异步请求能极大提升效率。但异步编程复杂度高且需要更小心地控制请求频率避免对目标网站造成过大压力。4.4 数据后续处理与应用建议拿到douban_top250_movies.csv后你可以用Excel、Pandas或任何数据分析工具进行探索。数据清洗在Pandas中import pandas as pd df pd.read_csv(douban_top250_movies.csv) # 将评分和评分人数转为数值类型 df[rating] pd.to_numeric(df[rating], errorscoerce) df[rating_people] pd.to_numeric(df[rating_people], errorscoerce) # 拆分上映日期可能包含多个国家 df[release_date_primary] df[release_date].str.split(/).str[0] # 拆分类型多个类型用‘/’分隔 df[genre_list] df[genre].str.split(/)简单分析示例# 评分最高的10部电影 top10_rating df.nlargest(10, rating)[[rank, title_cn, rating, rating_people]] # 统计哪位导演上榜作品最多 director_counts df[director].str.split(/).explode().str.strip().value_counts() # 计算不同类型电影的平均评分 # 需要先将genre列展开explode df_exploded df.assign(genre_singledf[genre].str.split(/)).explode(genre_single) df_exploded[genre_single] df_exploded[genre_single].str.strip() avg_rating_by_genre df_exploded.groupby(genre_single)[rating].mean().sort_values(ascendingFalse)这个项目从构思到实现最深的体会是网络爬虫是七分“协议分析”和“异常处理”三分“代码编写”。花在分析页面结构、设计稳健的解析逻辑、处理各种边界情况上的时间远多于写核心抓取代码的时间。它也让我更加意识到在获取公开数据时保持友好和节制的态度至关重要。希望这份详细的总结能帮你顺利拿到想要的数据并打开电影数据分析的大门。如果在运行中遇到任何问题不妨回头仔细检查页面结构是否发生了变化或者适当调整你的请求间隔时间耐心往往是解决爬虫问题最好的工具。
返回列表