个人主页:
> 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 的高分榜有两个入口:
| 第 1 页 | /movie/top-rated | GET |
| 第 2~5 页 | /discover/movie/items | POST(带表单参数) |
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)
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() if len(movie_name) > 0 else ""
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()')
对比一下:
| 上映日期 | 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 定位方式的稳定性优先级
按稳定性从高到低排序:
一句话原则:能靠"属性"和"语义"定位的,就别靠"位置"定位。位置索引描述的是"它在第几个",语义定位描述的是"我要什么"——后者才是页面结构和字段语义之间的稳定契约。
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)
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. 字符串拼接表单参数不优雅
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()
except requests.RequestException as e:
print(f"请求失败: {movie_info_url}, 原因: {e}")
return None
调用处过滤掉 None 再保存。
3. 缺少请求间隔,容易被封
连续 5 页 × 每页 ~20 部电影 = 上百次请求。建议加:
import time, 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 |
| 拿 HTML | lxml.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"] 这种语义定位后,无论有没有分级、顺序怎么变,都能稳定命中
位置索引是脆弱的,语义定位才是稳定的——这是本次踩坑最核心的反思。
后续可以从三个方向继续深化:
一句话总结: 爬虫的本质,是"用程序模拟人类浏览网页的行为,再把结构化的信息提取出来"。 XPath 的精髓,是"用语义属性描述你要什么",而不是"用位置索引告诉程序它排第几"。
网硕互联帮助中心






评论前必须登录!
注册