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

文章目录

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

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

一、项目概述

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

整体流程:

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

二、核心模块拆解

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

python 复制代码
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-rated GET
第 2~5 页 /discover/movie/items POST(带表单参数)
python 复制代码
for page_num in range(1, 6):
    if page_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)

python 复制代码
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]
  • 取值时统一做空列表兜底:
python 复制代码
'电影名': movie_name[0].strip() if len(movie_name) > 0 else ""

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


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

3.1 初始版本的写法

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

python 复制代码
# 上映日期
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 属性定位:

python 复制代码
# 上映日期
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:

python 复制代码
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 改版样式时容易整体失效。可以用更稳定的结构特征:

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

四、数据保存(CSV)

python 复制代码
def save_all_movies(all_movies):
    with open(MOVIE_LIST_FILE, 'w', encoding='UTF-8', newline='') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=["电影名","年份","上映日期","时长","标签","分数","语言"])
        writer.writeheader()
        writer.writerows(all_movies)

几个细节:

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

五、可继续优化点

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

python 复制代码
data = f"air_date.gte=&...&page={page_num}&..."

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

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

2. 没有异常处理

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

python 复制代码
try:
    movie_response = requests.get(movie_info_url, headers=headers, timeout=15)
    movie_response.raise_for_status()
except requests.RequestException as e:
    print(f"请求失败: {movie_info_url}, 原因: {e}")
    return None

调用处过滤掉 None 再保存。

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

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

python 复制代码
import time, random
time.sleep(random.uniform(0.5, 1.5))

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

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

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

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

环节 用到的知识
发请求 requests.get / post、headers 伪装、timeout
拿 HTML lxml.html.fromstring
定位元素 XPath 语法(//@id@classtext()contains())
数据清洗 strip()、空列表兜底、','.join()
写文件 csv.DictWriterencoding='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 的精髓,是"用语义属性描述你要什么",而不是"用位置索引告诉程序它排第几"。

相关推荐
文人sec1 小时前
MYSQL:insert...select:为什么锁源表的所有行和间隙?怎么最快地复制一张表?
数据库·python·mysql
weixin_307779131 小时前
C++代码实现MATLAB中的ode23t函数功能
开发语言·c++·算法·matlab
SamChan901 小时前
PyMuPDF vs pdfplumber vs pypdf:PDF 文本提取实测对比(翻译预处理视角)
python·ai·pdf
君顾11 小时前
24小时自助健身房系统开发实战与完整指南
java·开发语言·健身房
辰辉创聚2 小时前
炎症与免疫相关细胞因子:信号通路、分类及科研检测应用
python·oracle·nycodenz·重组il-6蛋白·抗tnf-α抗体·il-1β蛋白
ai小陈2 小时前
LTX2.5音视频生成任务验收实战:批量记录与音画质量检查
人工智能·python·深度学习·ai·音视频·gpu算力
今儿敲了吗2 小时前
04英文文本关键词提取(TF-IDF)
笔记·python
syagain_zsx2 小时前
算法基础篇 · 02 高精度(C++ 题解)
开发语言·c++·学习·高精度
白远山2 小时前
无人自助健身平台搭建:从架构设计到设备联动的完整实战
java·开发语言·架构·需求分析