☰
爬虫项目: 获取高分电影的数据总结
2026/10/3 2:29:49 网站建设 项目流程
个人主页: > for_ever_love__ < (欢迎各位大佬莅临😊)
其他栏目: > 我想学python了 <

其他栏目: > iOS项目总结大全 <

其他栏目: > iOS UI <

文章目录

  • 爬虫项目: 获取高分电影的数据总结
    • 一、项目概述
    • 二、核心模块拆解
      • 1. 请求头构造(反爬基础)
      • 2. 榜单数据获取(分页处理)
      • 3. 详情页解析(XPath)
    • 三、问题发现与改进:从位置索引到 class 语义定位
      • 3.1 初始版本的写法
      • 3.2 运行后发现的问题
      • 3.3 原因分析
      • 3.4 改进方案:用 `@class` 语义定位
      • 3.5 引申:XPath 定位方式的稳定性优先级
      • 3.6 相同思路可以继续优化的地方
    • 四、数据保存(CSV)
    • 五、可继续优化点
      • 1. 字符串拼接表单参数不优雅
      • 2. 没有异常处理
      • 3. 缺少请求间隔,容易被封
      • 4. 字段抽取过散,可以抽成配置
    • 六、本项目涉及的知识点串联
    • 七、总结

爬虫项目: 获取高分电影的数据总结

一、项目概述

本项目实现了一个电影数据爬虫,从 TMDB(The Movie Database)网站抓取高分电影榜单数据,解析电影详情页,最终将数据保存为 CSV 文件。

整体流程:

获取榜单页 → 解析电影详情页 URL → 请求详情页 → XPath 解析字段 → 汇总写入 CSV

二、核心模块拆解

1. 请求头构造(反爬基础)

headers={"Connection":"close","User-Agent":"Mozilla/5.0 ... Chrome/152.0.0.0 Safari/537.36","Accept-Language":"zh-CN, zh; q = 0.9"}

几个要点:

  • User-Agent:伪装成真实浏览器,避免被服务器识别为爬虫直接拒绝
  • Accept-Language:告诉服务器返回中文内容(影响 TMDB 页面文案)
  • Connection: close:每次请求完关闭连接,避免连接池被占用过久

在详情页请求里还额外加了Referer,模拟"从榜单页点进详情页"的真实跳转来源。

反爬的第一层,就是让请求"看起来像正常用户发出的"。


2. 榜单数据获取(分页处理)

TMDB 的高分榜有两个入口:

页码URL请求方式
第 1 页/movie/top-ratedGET
第 2~5 页/discover/movie/itemsPOST(带表单参数)
forpage_numinrange(1,6):ifpage_num==1:response=requests.get(TMDB_TOP_URL_1,timeout=20,headers=headers)else:response=requests.post(TMDB_TOP_URL_2,data=f"...&page={page_num}&...",timeout=20,headers=headers)

为什么第 2 页开始要用 POST?

TMDB 的 discover 接口筛选条件非常多(日期、语言、评分区间、时长范围……),用 POST 表单提交更合适,URL 也更干净。参数里的关键项:

  • sort_by=vote_average.desc:按平均分降序
  • vote_count.gte=300:过滤掉评分人数太少的电影(防止"1 人打 10 分"登顶)
  • with_runtime.gte/lte:时长范围

分页爬取的核心是观察请求规律,找到翻页时唯一变化的参数(这里是page)。


3. 详情页解析(XPath)

movie_doc=html.fromstring(movie_response.text)movie_name=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/h2/a/text()')movie_years=movie_doc.xpath('//*[@id="original_header"]/.../h2/span/text()')...

关键点:

  • html.fromstring():把 HTML 字符串转成结构化文档对象,之后可以用 XPath 查询
  • xpath()返回的是列表,即使只有一个结果也是[xxx]
  • 取值时统一做空列表兜底:
'电影名':movie_name[0].strip()iflen(movie_name)>0else""

XPath 定位一定要先在浏览器 F12 里验证,直接抄路径很容易因为页面动态渲染而落空。


三、问题发现与改进:从位置索引到 class 语义定位

3.1 初始版本的写法

详情页头部结构里,上映日期、标签、时长这三个字段,最初是用纯位置索引定位的:

# 上映日期movie_data=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[2]/text()')# 标签movie_tags=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[3]/a/text()')# 时长movie_cost_times=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[4]/text()')

当时抓取第一页几部电影时看起来是正常的,因为这几部电影的详情页结构一致,span[2] / span[3] / span[4]恰好一一对应上映日期、标签和时长。

3.2 运行后发现的问题

爬完 5 页、上百部电影之后,检查 CSV 结果发现:

  • 部分电影的上映日期是空白
  • 部分电影的标签列内容错位,或者干脆为空
  • 部分电影的时长是空白

但同一部电影的电影名和年份都是正常的。

3.3 原因分析

回到 TMDB 详情页的结构上找原因。头部那一行div里,span的顺序并不是固定死的,它大致按这样的顺序排布:

[0] 分级认证(如 PG-13 / R / TV-MA) [1] 上映日期(release) [2] 类型标签(genres) [3] 时长(runtime) [4] 导演、编剧等其他信息

关键在于:不是每部电影都有分级认证。

  • 有分级的电影:span[2]= 上映日期 ✅,span[3]= 标签 ✅,span[4]= 时长 ✅
  • 没有分级的电影:整个序列前移一位,span[2]变成了标签,span[3]变成了时长,span[4]可能是别的东西 ❌

位置索引一旦错位,取到的要么是错的字段,要么是空字符串。这就是"部分电影信息空白/错位"的根因。

换句话说:位置索引把"结构假设"写死了。它假设"上映日期永远是第 2 个 span",但这个假设在没有分级的电影上不成立。

3.4 改进方案:用@class语义定位

把位置索引换成@class属性定位:

# 上映日期movie_data=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[@class = "release"]/text()')# 标签movie_tags=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[@class = "genres"]/a/text()')# 时长movie_cost_times=movie_doc.xpath('//*[@id="original_header"]/div[2]/section/div[1]/div/span[@class = "runtime"]/text()')

对比一下:

字段改进前(位置索引)改进后(class 定位)
上映日期div/span[2]/text()div/span[@class="release"]/text()
标签div/span[3]/a/text()div/span[@class="genres"]/a/text()
时长div/span[4]/text()div/span[@class="runtime"]/text()

为什么 class 定位更稳?

  • span[2]表示"父节点下第 2 个 span",依赖兄弟节点的数量和顺序
  • span[@class="release"]表示"父节点下 class 为 release 的那个 span",依赖语义,不依赖位置

只要 TMDB 不改变字段的 class 名,无论前面有没有分级,无论顺序怎么变,都能精确命中。再次运行后,空白字段的问题消失。

3.5 引申:XPath 定位方式的稳定性优先级

按稳定性从高到低排序:

  1. @id+@class语义定位← 最稳,推荐
  2. @id+ 层级结构定位://*[@id="original_header"]/.../h2/a/text()
  3. 文本内容定位://span[text()="Runtime"]/following-sibling::span/text()
  4. 纯位置索引:div[2]/span[3]← 最脆,容易翻车

一句话原则:能靠"属性"和"语义"定位的,就别靠"位置"定位。位置索引描述的是"它在第几个",语义定位描述的是"我要什么"——后者才是页面结构和字段语义之间的稳定契约。

3.6 相同思路可以继续优化的地方

榜单列表项的选择器也是靠一串长长的 class:

movies_list=document.xpath('//*[@class="w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-colors hover:border-gray-300"]')

这类 Tailwind 生成的 class 组合,TMDB 改版样式时容易整体失效。可以用更稳定的结构特征:

# 抓所有指向 /movie/ 的链接,再回溯父级卡片movies_list=document.xpath('//a[contains(@href, "/movie/")]')

四、数据保存(CSV)

defsave_all_movies(all_movies):withopen(MOVIE_LIST_FILE,'w',encoding='UTF-8',newline='')ascsvfile:writer=csv.DictWriter(csvfile,fieldnames=["电影名","年份","上映日期","时长","标签","分数","语言"])writer.writeheader()writer.writerows(all_movies)

几个细节:

  • encoding='UTF-8':中文必须指定,否则 Windows 下默认 GBK 会乱码
  • newline='':防止 Windows 下每行之间多出空行
  • DictWriter:按字典键写入,fieldnames 顺序决定列顺序

五、可继续优化点

1. 字符串拼接表单参数不优雅

data=f"air_date.gte=&...&page={page_num}&..."

改进:用字典让requests自动编码:

data={"sort_by":"vote_average.desc","vote_count.gte":300,"page":page_num,}response=requests.post(url,data=data,...)

2. 没有异常处理

单个详情页请求失败(超时、404)会直接中断整个爬虫。建议:

try:movie_response=requests.get(movie_info_url,headers=headers,timeout=15)movie_response.raise_for_status()exceptrequests.RequestExceptionase:print(f"请求失败:{movie_info_url}, 原因:{e}")returnNone

调用处过滤掉None再保存。

3. 缺少请求间隔,容易被封

连续 5 页 × 每页 ~20 部电影 = 上百次请求。建议加:

importtime,random time.sleep(random.uniform(0.5,1.5))

4. 字段抽取过散,可以抽成配置

现在每加一个字段都要改三处(定义、xpath、dict)。建议用列表配置:

FIELDS=[("电影名",'//*[@id="original_header"]/.../h2/a/text()',"first"),("年份",'//*[@id="original_header"]/.../h2/span/text()',"first"),...]

六、本项目涉及的知识点串联

环节用到的知识
发请求requests.get / post、headers 伪装、timeout
拿 HTMLlxml.html.fromstring
定位元素XPath 语法(//、@id、@class、text()、contains())
数据清洗strip()、空列表兜底、','.join()
写文件csv.DictWriter、encoding='UTF-8'、newline=''
流程控制for 循环分页、字典组装

七、总结

这个案例虽小,但把爬虫的完整链路(请求 → 解析 → 清洗 → 存储)走了一遍。

本次改进最大的收获是:XPath 定位应当尽量依赖"语义属性(class/id)“,而不是"位置索引”。

  • 初始版本用span[2] / span[3] / span[4]定位,在结构一致的页面上勉强能用
  • 但 TMDB 详情页并非所有电影都有分级认证,导致span序列错位
  • 一错位,位置索引取到的就是错误字段或空字符串,于是出现"部分电影信息空白"
  • 换成span[@class="release"]这种语义定位后,无论有没有分级、顺序怎么变,都能稳定命中

位置索引是脆弱的,语义定位才是稳定的——这是本次踩坑最核心的反思。

后续可以从三个方向继续深化:

  1. 健壮性:异常捕获、重试机制、限速
  2. 扩展性:字段配置化、XPath 选择器从外部配置读取
  3. 持久化:从 CSV 升级到 SQLite / MySQL,便于后续查询和增量更新

一句话总结:
爬虫的本质,是"用程序模拟人类浏览网页的行为,再把结构化的信息提取出来"。
XPath 的精髓,是"用语义属性描述你要什么",而不是"用位置索引告诉程序它排第几"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询