
1. 项目概述为什么要采集猫眼电影的年份数据做数据分析或者市场研究的朋友可能都动过抓取猫眼电影数据的念头。猫眼作为国内主流的电影信息与票务平台沉淀了海量的电影条目、评分、票房部分以及用户评论数据。这些数据就像一座金矿无论是想分析某个导演的风格变迁研究特定类型片的市场表现还是想做个票房预测模型都离不开对历史数据的系统性梳理。“各年份数据采集”这个需求听起来简单但背后涉及的东西可不少。它不是一个简单的“一锅端”式爬虫而是要求我们按年份维度结构化地获取电影的核心信息。这意味着我们需要找到猫眼电影列表页的规律模拟翻页解析每一部电影的详情页并把散落在各处的信息——比如电影名称、主演、类型、上映日期、评分甚至是那句吸引人的宣传语——都规整地提取出来按年份归档。这活儿我干过不止一次为团队做过竞品分析也帮朋友做过学术研究的数据支持。过程中踩过的坑、总结的技巧远比最后那几行代码值钱。今天我就把自己这套经过实战检验的方案拆开揉碎了讲给你听从思路设计到工具选型从核心代码到反爬应对最后再附上我私藏的“数据清洗”心得。目标就一个让你拿到手的不只是能跑的脚本更是一套遇到问题能自己解决的思路和一份干净、可直接分析的数据集。2. 整体思路与爬虫策略设计面对猫眼这样的大型站点直接蛮干很容易撞上南墙。我的核心思路是“化整为零层层递进”。整个采集任务可以分解为三个清晰的层次这样不仅逻辑清晰也便于调试和维护。2.1 核心策略列表页遍历 详情页抓取这是最经典也最有效的策略。猫眼电影有按年份筛选的列表页例如maoyan.com/films?year2023这个页面就是我们任务的起点。确定年份范围首先你需要想清楚要抓取哪几年的数据。是近十年还是自有记录以来确定起止年份比如2010-2024。遍历年份列表页针对每一个目标年份访问其对应的列表页URL。这里的关键在于列表页通常是分页的你需要解析出总页数或者通过观察“下一页”按钮的规律来模拟翻页遍历该年份下的所有电影条目。提取详情页链接在每一个列表页中我们的核心目标是提取出每一部电影的详情页独立URL。通常这个链接藏在电影海报或标题的a标签里。深入详情页抓取拿到详情页链接后再发起请求进入单个电影的“主场”。这里的信息最为全面是我们数据采集的主要战场。这个策略的好处是目标明确每一层的任务单一。即使某个详情页抓取出错也不会影响其他电影或其他年份的抓取进程容错性较好。2.2 技术栈选型为什么是 Requests BeautifulSoupPython生态里爬虫库很多Scrapy框架强大Selenium能处理复杂JS。但对于猫眼电影这个场景我的选择是Requests BeautifulSoup这对经典组合。Requests负责HTTP通信。它的API简洁优雅性能足够且易于设置请求头Headers、代理Proxy等来应对基础反爬。猫眼电影的大部分页面内容尤其是列表页和详情页的核心信息仍然是服务端渲染的直接通过Requests获取HTML是可行的。BeautifulSoup负责HTML解析。它就像一把瑞士军刀能根据标签、CSS类名class、ID等快速定位并提取我们需要的文本内容。对于结构相对规整的猫眼页面用起来非常顺手。不选Scrapy是因为本项目结构并不复杂无需调度、去重、管道等重型框架的特性用轻量级库更灵活。不首选Selenium是因为它效率较低需要启动浏览器资源消耗大除非页面数据完全由JavaScript动态加载且没有隐藏的接口否则没必要上来就用“大炮”。当然如果后续发现猫眼某些数据通过Ajax接口加载我们可以轻松地在Requests的基础上配合浏览器的开发者工具F12 - Network - XHR找到那个返回JSON数据的接口用Requests直接调用效率更高。这是组合方案的灵活之处。2.3 关键挑战与应对思路预设在动手写代码前必须预见到可能遇到的麻烦并想好对策。反爬虫机制这是最大的拦路虎。猫眼肯定有。User-Agent检测这是最基本的。必须为Requests设置一个常见的浏览器UA。请求频率限制如果请求太快IP可能会被暂时封禁。解决方案是在请求间加入随机延时例如time.sleep(random.uniform(1, 3))模拟真人操作。Cookie/Session某些页面可能需要维持会话。使用requests.Session()对象可以自动管理Cookie让多次请求看起来像同一个用户在操作。IP代理池对于大规模、高频采集这是终极方案。需要准备一批高质量的代理IP在请求时随机切换。但对于按年份的“中低速”采集通过控制频率和设置合理延时通常可以避免触发IP封锁。页面结构变动网站前端可能会改版。今天能用的CSS选择器明天可能就失效了。因此我们的代码不能写死提取信息的逻辑要清晰并且最好将关键的CSS选择器如.name.actor作为配置变量放在代码开头方便日后修改。数据完整性不是每部电影都有评分或票房数据。我们的代码要能处理这些缺失值用None或空字符串填充避免程序因某个字段缺失而崩溃。3. 实操步骤详解从环境搭建到数据落地理论说再多不如一行代码。我们一步步来我会把每个环节的意图和注意事项讲清楚。3.1 环境准备与依赖安装首先确保你的Python环境建议3.7以上已经就绪。然后通过pip安装我们需要的库。pip install requests beautifulsoup4 pandasrequests: 用于网络请求。beautifulsoup4: 用于解析HTML。pandas: 这不是爬虫必选但强烈建议安装。它用于将爬取的数据整理成结构化的表格DataFrame并轻松导出为CSV或Excel文件是数据清洗和分析的神器。3.2 核心代码模块拆解我不会一次性扔给你几百行代码。我们按功能模块来构建这样你更容易理解。模块一请求头与会话管理创建一个Python文件比如maoyan_crawler.py。开头我们先配置好请求头和会话。import requests import time import random from bs4 import BeautifulSoup import pandas as pd # 设置请求头模拟浏览器访问 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } # 创建一个会话对象可以自动保存cookies session requests.Session() session.headers.update(headers) # 基础URL BASE_URL https://maoyan.com注意User-Agent是关键。你可以从自己浏览器的开发者工具F12 - Network刷新页面点击任意请求查看Headers里复制一个最新的来用。这里我写了一个常见的Chrome UA作为示例。模块二获取单个年份的电影列表页链接这个函数的目标是输入一个年份如2023输出该年份下所有电影详情页的链接列表。def get_film_links_by_year(year, max_pages10): 获取指定年份的所有电影详情页链接 :param year: 年份整数 :param max_pages: 最大抓取页数防止意外死循环 :return: 电影详情页链接列表 film_links [] page 0 # 猫眼列表页页码通常从0开始 while page max_pages: # 构建列表页URL猫眼的规律通常是 films?showType1year{year}offset{page*30} # 具体参数需要观察实际页面 list_url f{BASE_URL}/films?year{year}offset{page*30} print(f正在抓取 {year} 年第 {page1} 页: {list_url}) try: resp session.get(list_url, timeout10) resp.raise_for_status() # 如果状态码不是200抛出异常 soup BeautifulSoup(resp.text, html.parser) # 核心定位电影条目。打开猫眼列表页检查元素找到每个电影块的容器。 # 例如可能是 div classmovie-item 里面的 a 标签 # 这里的选择器需要你根据实际页面结构调整 movie_items soup.find_all(div, class_movie-item) # 示例可能不准确 if not movie_items: print(f未在第 {page1} 页找到电影条目可能已到末页或页面结构已变。) break for item in movie_items: link_tag item.find(a, hrefTrue) if link_tag and link_tag[href].startswith(/films/): full_link BASE_URL link_tag[href] if full_link not in film_links: # 简单去重 film_links.append(full_link) # 判断是否还有下一页同样需要根据页面实际结构调整 # 例如查找是否存在带有“下一页”文本或特定class的链接 next_button soup.find(a, string下一页) if not next_button or disabled in next_button.get(class, []): break # 没有下一页了退出循环 page 1 # 重要随机延时避免请求过快 time.sleep(random.uniform(2, 5)) except requests.exceptions.RequestException as e: print(f请求列表页出错: {e}) break except Exception as e: print(f解析列表页出错: {e}) break print(f年份 {year} 共找到 {len(film_links)} 部电影的链接。) return film_links实操心得find_all和find里用的CSS选择器如class_movie-item是整个爬虫最脆弱的部分。你必须亲自打开猫眼电影的列表页比如maoyan.com/films?year2023按F12打开开发者工具使用元素选择器箭头图标去点击电影条目查看它外层容器的HTML结构和类名。把这个类名填到代码里。网站改版首先就要改这里。模块三解析单个电影详情页信息现在我们有了详情页链接需要从中提取出结构化的数据。def parse_film_detail(detail_url): 解析电影详情页提取核心信息 :param detail_url: 电影详情页URL :return: 包含电影信息的字典 film_info { 电影名称: , 上映年份: , 类型: , 主演: , 评分: , 宣传语: , 详情页链接: detail_url } try: resp session.get(detail_url, timeout10) resp.raise_for_status() # 注意编码有时需要指定 resp.encoding utf-8 soup BeautifulSoup(resp.content, html.parser) # 1. 电影名称 (通常在 h1 或某个特定class的span里) name_tag soup.find(h1, class_name) or soup.find(h1) if name_tag: film_info[电影名称] name_tag.get_text(stripTrue) # 2. 上映年份 (可以从名称后的括号里提取或者从特定的上映日期元素中提取年份) # 例如找到包含“上映日期”的标签 info_tags soup.find_all(li, class_ellipsis) for tag in info_tags: text tag.get_text(stripTrue) if 上映时间 in text or 上映日期 in text: date_str text.split()[-1] if in text else text # 尝试从日期字符串中提取年份如 “2023-08-01” - 2023 import re year_match re.search(r(\d{4}), date_str) if year_match: film_info[上映年份] year_match.group(1) break # 3. 类型 (通常在一组标签里用 / 分隔) genre_tag soup.find(li, class_ellipsis, textre.compile(r类型)) if genre_tag: # 可能需要找到相邻的兄弟节点获取具体类型文本 genre_text genre_tag.find_next_sibling(li, class_ellipsis) if genre_text: film_info[类型] genre_text.get_text(stripTrue).replace(/, ,) # 4. 主演 (同样找到“主演”标签然后取后面的内容) actor_tag soup.find(li, class_ellipsis, textre.compile(r主演)) if actor_tag: actor_text actor_tag.find_next_sibling(li, class_ellipsis) if actor_text: film_info[主演] actor_text.get_text(stripTrue).replace(/, ,) # 5. 评分 (可能在 div classscore 里) score_tag soup.find(div, class_score) or soup.find(span, class_score-num) if score_tag: film_info[评分] score_tag.get_text(stripTrue) # 6. 宣传语 (可能在 div classdra 里) dra_tag soup.find(div, class_dra) if dra_tag: film_info[宣传语] dra_tag.get_text(stripTrue) # 再次检查年份如果从上映日期没提取到尝试从电影名称或URL中推断 if not film_info[上映年份] and film_info[电影名称]: # 有些电影名包含年份如“流浪地球2 (2023)” year_match re.search(r\((\d{4})\), film_info[电影名称]) if year_match: film_info[上映年份] year_match.group(1) print(f已解析: {film_info[电影名称]} ({film_info.get(上映年份, N/A)})) time.sleep(random.uniform(1, 3)) # 详情页请求也加延时 except requests.exceptions.RequestException as e: print(f请求详情页 {detail_url} 出错: {e}) except Exception as e: print(f解析详情页 {detail_url} 出错: {e}) return film_info注意事项详情页的解析逻辑比列表页更复杂因为要定位多个字段。没有一成不变的选择器。你必须打开一个具体的电影详情页如《流浪地球2》的页面用开发者工具逐个找到“上映日期”、“类型”、“主演”等文本对应的HTML标签和类名。我上面代码中的class_ellipsis只是示例实际类名可能完全不同。这个过程需要耐心和细心。模块四主控流程与数据存储最后我们把所有模块串联起来并利用pandas保存数据。def main(start_year2010, end_year2024): all_films_data [] for year in range(start_year, end_year 1): print(f\n 开始采集 {year} 年电影数据 ) film_links get_film_links_by_year(year, max_pages20) # 可根据年份调整最大页数 for idx, link in enumerate(film_links): print(f[{year}] 处理第 {idx1}/{len(film_links)} 部电影...) film_data parse_film_detail(link) if film_data[电影名称]: # 只保存成功解析到名称的数据 all_films_data.append(film_data) # 每个年份抓取完后可以稍作长时间休息降低风险 if year ! end_year: wait_time random.uniform(10, 20) print(f{year}年采集完成等待{wait_time:.1f}秒后继续下一年...) time.sleep(wait_time) # 使用pandas保存数据 if all_films_data: df pd.DataFrame(all_films_data) # 重新排序列顺序让关键信息在前 column_order [电影名称, 上映年份, 类型, 主演, 评分, 宣传语, 详情页链接] df df[column_order] # 保存为CSV文件编码用utf-8-sig解决Excel打开中文乱码问题 filename fmaoyan_films_{start_year}_to_{end_year}.csv df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f\n所有数据已保存至 {filename} 共 {len(df)} 条记录。) print(df.head()) # 预览前几行数据 else: print(未抓取到任何数据。) if __name__ __main__: main(2022, 2023) # 示例抓取2022和2023年的数据正式运行可扩大范围4. 进阶技巧与反爬虫实战如果你的爬虫跑起来不顺利或者想更稳健、更高效下面这些技巧会帮到你。4.1 动态加载数据的应对猫眼的部分数据比如“想看人数”、部分评论可能是通过JavaScript异步加载的。你在HTML源码里找不到但在浏览器的“Network”面板里能看到XHR或Fetch请求。解决方法打开开发者工具F12进入“Network”选项卡勾选“Preserve log”保留日志。刷新或操作页面观察“XHR”或“Fetch”类型的请求。找到包含你所需数据如JSON格式的请求查看其“Headers”获取请求URL和参数查看“Preview”或“Response”确认数据。在你的Python代码中直接用requests.get()或requests.post()模拟这个请求。这通常比解析HTML更直接、更稳定因为接口返回的是结构化的JSON。例如电影的评分详情可能来自一个类似https://maoyan.com/ajax/film/${filmId}?__t时间戳的接口。你需要先从详情页HTML里提取出filmId然后构造这个接口URL去请求。4.2 请求头Headers的精细化伪装除了User-Agent有时网站还会检查其他Header。关键HeadersReferer表示你从哪个页面跳转过来的。对于详情页可以设置为列表页的URL。Accept-Encoding通常设为gzip, deflate, br表示接受压缩数据以节省带宽。Connection:keep-aliveUpgrade-Insecure-Requests:1Sec-Fetch-*系列头现代浏览器会发送这些头但用Requests模拟比较麻烦。如果遇到高级反爬可以考虑使用curl_cffi或undetected-chromedriver等能更好模拟浏览器指纹的库。4.3 使用IP代理池当单IP请求频率过高被禁时就需要切换IP。# 一个简单的代理使用示例需自行准备代理IP列表 proxies_list [ http://user:passip1:port, http://user:passip2:port, # ... ] def get_with_proxy(url, session): proxy random.choice(proxies_list) try: resp session.get(url, proxies{http: proxy, https: proxy}, timeout8) return resp except: # 如果代理失败可以重试或标记该代理失效 return None重要提醒免费代理大多不稳定、速度慢。商业项目或大规模采集建议使用付费代理服务。在小规模、低速采集下通过time.sleep控制频率通常足以应对。4.4 异常处理与日志记录健壮的爬虫必须能处理各种异常并记录下发生了什么。Try-Except像示例代码中那样用try-except包裹可能出错的网络请求和解析代码。日志模块使用Python内置的logging模块替代print可以输出不同级别DEBUG, INFO, WARNING, ERROR的日志到文件方便事后排查。数据持久化不要等所有数据抓完再保存。可以每抓取10部或1个年份就将all_films_data列表保存一次到CSV防止程序中途崩溃导致全部数据丢失。5. 数据清洗与后处理经验谈爬下来的原始数据往往是“脏”的直接分析会出问题。以下是我常用的清洗步骤去重尽管我们在收集链接时做了简单去重但可能因为不同年份列表页出现同一部电影如跨年上映导致数据重复。使用pandas的df.drop_duplicates(subset[电影名称, 上映年份], keepfirst)可以根据电影名和年份去重。处理缺失值评分没有评分的电影可能是未上映或太老。可以填充为NaN在分析时排除。主演/类型可能为空。根据分析需求决定是保留空值还是填充为“未知”。字段格式化上映年份确保是整数类型。从字符串中提取后用pd.to_numeric(df[上映年份], errorscoerce)转换。评分如果是字符串如“9.5”转换为浮点数。注意处理“暂无评分”这样的文本。类型/主演我们之前用逗号分隔了。检查是否有多余的空格可以用df[类型] df[类型].str.replace(, ,).str.split(,).apply(lambda x: [i.strip() for i in x])将其转换为列表便于后续做“类型统计”。异常值检查查看年份范围是否合理比如有1970年或2099年这样的异常值评分是否在合理区间如0-10分。清洗后的数据才是真正可用的“资产”。你可以用pandas进行各种分析比如各年份电影数量趋势。不同类型电影的占比和年度变化。评分分布情况。高频出现的演员等。最后也是最重要的心得爬虫是与网站维护者的一场动态博弈。你的代码今天能跑不代表明天还能跑。保持对目标网站变化的关注定期测试并理解反爬措施背后的原理是为了保护服务器负载还是保护核心数据才能让你的数据管道长久稳定。这套方法和思路不仅适用于猫眼稍作调整也能应用到其他许多类似结构的网站上去。