实战-电影榜单爬取

实战-电影榜单爬取

2026年9月19日·#编程学习/python学习笔记Python/爬虫·1718 字 9 分钟
浏览量加载中...
AI 摘要

综合实战:爬取 TMDB 高分电影榜单 Top100,提取 11 个字段、翻页、用正则做数据清洗并存入 CSV

项目目标#

获取高分电影榜单(Top100)数据,并保存在 CSV 文件中。字段包括:电影名、年份、上映时间、类型、时长、评分、语言、导演、作者、宣传语、简介。

目标网站:https://www.themoviedb.org(TMDB)

开发步骤(五步)#

  1. 明确网站的 robots.txt 中的抓取规则
  2. 查看页面结构,拆解具体的操作步骤,按步骤开发
  3. 获取高分电影列表数据
  4. 遍历电影列表,获取每一部电影的详情信息,并提取数据
  5. 将电影详情信息保存到 csv 文件

版本演进#

版本解决了什么
1(05)搭骨架:请求榜单页 → 解析出电影卡片列表 → 遍历取详情 url → 空函数占位
2(06)get_movie_info():从详情页提取 11 个字段,拼成字典返回
3(07)save_all_movies():用 csv.DictWriter 把全部电影写入 CSV,加过程日志
4(08)翻页:第 1 页用 GET,第 2 页起换接口用 POST 提交表单数据,循环 1~5 页
5(09)定位更稳:详情页字段改用 span[@class='release'] 这种按 class 属性的写法
优化(12)数据清洗:新增三个函数,用正则把年份、上映时间、时长统一成规范格式

核心代码拆解#

1. 保存到 CSV#

import csv
MOVIE_LIST_FILE = "csv_data/movie_list.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) # 一次写入所有行

2. 从列表页到详情页#

import requests
from lxml import html
TMDB_BASE_URL = "https://www.themoviedb.org"
TMDB_TOP_URL_1 = "https://www.themoviedb.org/movie/top-rated" # 第 1 页(GET)
TMDB_TOP_URL_2 = "https://www.themoviedb.org/discover/movie/items" # 第 2 页起(POST)
response = requests.get(TMDB_TOP_URL_1, timeout=60)
document = html.fromstring(response.text)
# 每张电影卡片
movie_list = document.xpath(f"//*[@id='page_1']/div[@class='card style_1']")
for movie in movie_list:
movie_urls = movie.xpath("./div/div/a/@href") # 详情页的相对地址
if movie_urls:
movie_info_url = TMDB_BASE_URL + movie_urls[0] # 拼成完整 url
Note

详情页里 11 个字段各对应一条 Xpath,写法都一样(定位元素 → /text() 取文本,或 /@data-percent 取属性)。例如评分取的是圆环图表的 @data-percent 属性。完整的 11 条见课程文件 12. 网络机器人-案例-电影榜单(优化).py

3. 翻页:第 1 页 GET,第 2 页起 POST#

for page_num in range(1, 6):
if page_num == 1:
response = requests.get(TMDB_TOP_URL_1, timeout=60)
else:
# 第 2 页起换了一个接口,用 POST 提交一长串表单参数(其中 page=页码)
response = requests.post(TMDB_TOP_URL_2, f"......&page={page_num}&......", timeout=60)
document = html.fromstring(response.text)
movie_list = document.xpath(f"//*[@id='page_{page_num}']/div[@class='card style_1']")

4. 数据清洗(正则实战)#

网页上拿到的原始数据不规范:年份是 1994 但混在别的文字里、时长是 2h 20m。用三个函数清洗:

import re
# 获取电影年份(取 4 位数字)
def get_movie_year(movie_years):
movie_year = movie_years[0].strip() if movie_years else ''
year_res = re.search(r"\d{4}", movie_year)
return year_res.group() if year_res else ''
# 获取上映时间(只留 yyyy-mm-dd 部分)
def get_movie_publish_date(movie_dates):
movie_date = movie_dates[0].strip() if movie_dates else ''
return re.search(r"\d{4}-\d{2}-\d{2}", movie_date).group() # 1957-04-10
# 获取电影时长(统一转换为分钟, 如: 2h 20m --> 140)
def get_movie_cost_time(movie_cost_times):
movie_cost_time = movie_cost_times[0].strip() if movie_cost_times else '' # 2h 20m / 40m / 2h
h_res = re.search(r"(\d+)h", movie_cost_time)
m_res = re.search(r"(\d+)m", movie_cost_time)
h = int(h_res.group(1)) if h_res else 0
m = int(m_res.group(1)) if m_res else 0
return h * 60 + m
Note

时长那段的 h_res.group(1) 取的是括号分组里的数字((\d+)h 里括号包住的 \d+)。h_res 可能是 None(比如只有 40m 没有小时),所以用三目表达式兜底为 0。

完整代码#

完整最终版见课程文件 12. 网络机器人-案例-电影榜单(优化).py:把”保存 CSV + 详情提取(11 条 Xpath)+ 翻页循环 + 三个清洗函数”四块组合起来即可,main() 的流程就是入门程序那五步。

Warning

运行前记得先看目标网站的 robots.txt;请求统一加 timeout;对频率保持克制(Crawl-delay 的精神)。

相关#

练习题#

一、回忆填空(写完再展开对答案)#

  1. 电影数据要保存的格式是 ____(逗号分隔值),用 ____ 模块按字典写入
  2. 榜单第 1 页用 ____ 请求;第 2 页起换接口用 ____ 请求提交表单参数
  3. 从电影卡片里取详情页地址:movie.xpath("./div/div/a/@____"),再和 TMDB_BASE_URL ____
  4. 评分字段取的是圆环图的 @____ 属性,不是文本
  5. 清洗年份用正则 ____(取 4 位数字);清洗上映时间用 ____(取日期部分)
  6. 时长 2h 20m 清洗成分钟:(\d+)h 取小时、(\d+)m 取分钟,结果 = ____ × 60 + ____
  7. h_res.group(1) 里的 1 表示取第 ____ 个括号分组的内容
填空答案(做完再点开)
  1. CSV / csv 2. GET、POST 3. href、拼接 4. data-percent 5. \d{4}\d{4}-\d{2}-\d{2} 6. 小时 h、分钟 m 7. 1(第一个括号)

二、裸写题(全部用假数据,不需要真的爬网站)#

  • 2-1 清洗年份get_movie_year(movie_years):传入如 ["肖申克的救赎 (1994)"],返回 4 位年份字符串。

    提示(先自己想,实在想不出再点开)

    一级 · 思路:从一堆文字里抠出”连续 4 位数字” 二级 · 方法re.search(r"\d{4}", 文本).group()(先判空) 三级 · 骨架res = re.search(...); return res.____() if res else ''

  • 2-2 清洗时长get_movie_cost_time(movie_cost_times)["2h 20m"] 返回 140["40m"] 返回 40["2h"] 返回 120

    提示

    一级 · 思路:小时和分钟分开找,找不到按 0 处理,最后换算成分钟相加 二级 · 方法re.search(r"(\d+)h", s) / re.search(r"(\d+)m", s) / group(1) 三级 · 骨架h = int(h_res.group(1)) if h_res else 0

  • 2-3 清洗上映时间get_movie_publish_date(movie_dates):传入如 ["1994-09-23 (CN)"],返回 1994-09-23

    提示

    一级 · 思路:日期部分是固定模式 yyyy-mm-dd 二级 · 方法re.search(r"\d{4}-\d{2}-\d{2}", s).group() 三级 · 骨架:模式串里 \d{4} 后面跟 - 再跟 \d{2}

三、综合题#

  • 3-1 组装一个小流程 准备 3 条假电影数据(含不规范的年份/时长),把 2-1、2-2、2-3 的清洗函数用上,再用 csv.DictWriter 全部写入 movie_list_test.csv,最后读回来打印检查。

    提示

    一级 · 思路:清洗(逐条过三个函数)→ 存盘 → 读回验证,就是爬虫五步里的”清洗 + 存储” 二级 · 方法:三个清洗函数 + csv.DictWriter / writerows 三级 · 骨架for m in raw_movies: m["时长"] = get_movie_cost_time(...)

参考答案(做完再点开)
import re
import csv
def get_movie_year(movie_years):
movie_year = movie_years[0].strip() if movie_years else ''
year_res = re.search(r"\d{4}", movie_year)
return year_res.group() if year_res else ''
def get_movie_cost_time(movie_cost_times):
movie_cost_time = movie_cost_times[0].strip() if movie_cost_times else ''
h_res = re.search(r"(\d+)h", movie_cost_time)
m_res = re.search(r"(\d+)m", movie_cost_time)
h = int(h_res.group(1)) if h_res else 0
m = int(m_res.group(1)) if m_res else 0
return h * 60 + m
def get_movie_publish_date(movie_dates):
movie_date = movie_dates[0].strip() if movie_dates else ''
return re.search(r"\d{4}-\d{2}-\d{2}", movie_date).group()
raw_movies = [
{"电影名": "肖申克的救赎", "年份": ["肖申克的救赎 (1994)"], "上映时间": ["1994-09-23 (CN)"], "时长": ["2h 22m"]},
{"电影名": "教父", "年份": ["教父 (1972)"], "上映时间": ["1972-03-24 (US)"], "时长": ["2h 55m"]},
{"电影名": "悬崖上的金鱼姬", "年份": ["悬崖上的金鱼姬 (2008)"], "上映时间": ["2008-07-19 (JP)"], "时长": ["1h 40m"]},
]
movies = []
for m in raw_movies:
movies.append({
"电影名": m["电影名"],
"年份": get_movie_year(m["年份"]),
"上映时间": get_movie_publish_date(m["上映时间"]),
"时长": get_movie_cost_time(m["时长"]),
})
with open("movie_list_test.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["电影名", "年份", "上映时间", "时长"])
writer.writeheader()
writer.writerows(movies)
with open("movie_list_test.csv", "r", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row)

评论区

[ 标签 ]
# AI37# AI 编程2# AI工具1# Ajax2# Apifox1# AstrBot3# Astro2# CC Switch1# CDN2# Claude Code1# claudecode2# ClaudeCode1# Cloudflare2# CloudFlare2# CloudFlare-ImgBed3# coc3# CSS6# DeepSeek6# deepseek2# DELETE1# Docker1# EdgeOne3# Gist1# git1# GitHub1# hexo-circle-of-friends1# HTML6# HTTP5# ImageManager1# Java23# java13# JavaScript5# JDBC3# JSON2# JUnit1# LangChain25# Logback1# Maven6# Muse Spark1# Mybatis1# MyBatis4# MySQL28# MySql1# NapCat1# Node.js1# obsidian2# Obsidian5# OpenCode4# ORM1# PathVariable1# PicGo1# PyCharm1# Python65# RequestBody1# RequestMapping1# RESTful风格1# skills1# Slf4j1# SpringBoot11# SQL2# Streamlit5# Svelte2# TailwindCSS1# Telegram3# Tlias2# Vercel1# vscode2# Vue7# Waline3# WebDAV1# Web基础6# Web开发6# WinSCP1# YAML1# 三层架构1# 中二宣言1# 书籍1# 使用文档10# 写作1# 函数2# 刷步数1# 前端32# 动态1# 动漫1# 包1# 单词2# 博客7# 博客工作流1# 博客开发2# 参数接收1# 友链1# 反思2# 图床6# 地图1# 备份2# 大模型1# 奇思妙想1# 存储1# 学习方法6# 学校1# 宝塔面板3# 宝宝10# 对象1# 导航栏1# 工具2# 开发1# 开发工具1# 开发规范1# 开心1# 异常处理1# 影视2# 微信1# 性能优化2# 总结1# 想法15# 感受1# 感悟11# 指南1# 提示词工程1# 插件5# 故障排除1# 效率工具2# 教程10# 数据分析9# 数据库27# 数据结构1# 文件操作2# 斩神1# 日常92# 日志框架1# 朋友圈1# 朱元璋1# 模块1# 模板1# 正则表达式2# 测试1# 游戏2# 爬虫7# 生活迁移1# 电影2# 电脑1# 碎碎念1# 视觉识别1# 类1# 类型注解1# 网络基础2# 网络教室1# 羊毛2# 脚本2# 脚本工具1# 自动化2# 蓝奏云1# 订阅推荐2# 记录2# 评论系统1# 词根1# 词缀1# 说说1# 足迹1# 跑步2# 路径参数1# 转载2# 运动1# 部落冲突1# 配置1# 随机图1# 面向对象5# 音乐3# 音标1# 饮食1# 驼峰命名1# 高德地图1
[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

暂未播放

0:000:00
暂无歌词
找不到相关结果。
[ contents ]
[ 全部文章 ]