💡 导读:我们今天在干嘛?
很多人以为写代码抓数据是高级黑客的特权。其实不然!今天咱们用大白话来拆解:其实就是写一张任务清单,派出一个“网络跑腿小哥”(也就是我们的机器人),让他帮我们去电影网站(TMDB),把前 60 部高分电影的信息全部抄下来,最后整整齐齐地写进一个 Excel 表格里。
只要你会一点点 Python,今天你就能成功雇佣这个小哥!
🛑 第一步:机器人的“法律与道德准则”(robots.txt 协议)
在派小哥出门之前,我们必须教他学会尊重别人。互联网世界有一条默认的“君子协定”,叫做 robots.txt 协议。
1. 大白话科普:什么是 robots.txt?
这就好比每一个公开的商业大楼(网站),都会在门口贴一张《外来跑腿机器人谢绝入内区域清单》。
只要你在任何网站的域名后面加上 /robots.txt(比如:https://themoviedb.org),就能看到官方给所有机器人设定的红线。
2. 怎么看懂这份协议?
User-agent: *:意思是“不管是哪家公司的跑腿小哥,都得听好了”。Disallow: /search:意思是“里面正在热火朝天搞搜索,小哥请勿入内,否则会把服务器挤爆”。Allow: /:意思是“普通的公开展示页面,欢迎正常查阅”。
💡 作者寄语:我们的机器人今天只去访问公开的高分电影榜单www.themoviedb.org/movie/top-r… 和详情页,完全符合官方的合规要求。做爬虫第一步,一定要先看眼门口的告示牌!
🏃♂️ 第二步:搭建主控制中枢(解决动态翻页难题)
我们要让小哥跑 3 趟(翻 3 页),每页拿 20 部电影,先把这 60 部电影的“详情页小地址”全部翻出来。
1. 网页结构与抓包真相
很多新手以为,翻到第二页,只要把网址(URL)后面的数字改成 page=2 就行了。但这个电影网站很贼,你点“下一页”的时候,浏览器顶部的网址根本没变!如果小哥只盯着网址看,他就会在第一页原地打转。
📸 建议配图 1:翻页抓包现场
第一页内容:
第二页内容:
2. 核心大白话原理
我们在代码里给小哥装上一个“智能判断开关”:
- 第一趟(第1页):直接走正门看公开网页(用
GET请求)。 - 后面几趟(第2、3页):带着一长串参数做成一张“秘密申请表”(Payload),去敲后门(用
POST请求)。 - 💡 避坑处理:因为我们还没教小哥怎么去单部电影抄数据,所以我们先用
print(f"发现了电影链接: {movie}")代替。这段代码你直接复制运行,完全能成功跑起来!
💻 核心代码
from lxml import html
import requests
# 跑腿小哥要去的大本营网址
ROOT_URL = "https://themoviedb.org"
def main():
# 循环 3 次,代表让小哥跑 3 趟,拿前 60 部电影
for i in range(3):
print(f"🏃♂️ 小哥报告:我正在跑第 {i+1} 趟...")
if i == 0:
# 状态 A:第一趟,走正门直接看(GET请求)
response = requests.get(f"{ROOT_URL}/movie/top-rated")
else:
# 状态 B:后面几趟,带着“秘密表单”(POST请求)去敲后门
payload = f"air_date.gte=&air_date.lte=&certification=&certification_country=HK&debug=&first_air_date.gte=&first_air_date.lte=&include_adult=false&include_softcore=false&latest_ceremony.gte=&latest_ceremony.lte=&page={i+1}&primary_release_date.gte=&primary_release_date.lte=®ion=&release_date.gte=&release_date.lte=2027-02-07&show_me=everything&sort_by=vote_average.desc&vote_average.gte=0&vote_average.lte=10&vote_count.gte=300&watch_region=HK&with_genres=&with_keywords=&with_networks=&with_origin_country=&with_original_language=&with_watch_monetization_types=&with_watch_providers=&with_release_type=&with_runtime.gte=0&with_runtime.lte=400"
response = requests.post(
f"{ROOT_URL}/discover/movie/items", data=payload
)
# 小哥把看到的网页打包成一棵方便查找的“菜单树”
data = html.fromstring(response.text)
# 仙人指路!在这页网页里,把 20 部电影的详情页“小地址”全部翻出来
movies = data.xpath(
"//*[@class='media-list-results contents']/div/div/div/a/@href"
)
# 拿着这 20 个小地址,我们先让小哥在控制台打印出来看看
for movie in movies:
print(f"📌 跑腿小哥:我发现了电影详情页链接: {movie}")
if __name__ == "__main__":
main()
🧠 语法知识点科普
requests.get():就像你直接去柜台看菜单,菜单挂在墙上(网址里),大家都能看得见。requests.post(url, data=...):就像你填了一张保密申请表塞给柜员,柜员在后台悄悄把你要的第二页数据递给你。
🎯 第三步:精准轰炸(手写核心抓取逻辑与高级 XPath)
现在小哥能拿到电影的小地址了。接下来我们要写一个新的函数 get_movie_detail(url),正式教他点进单部电影的房间,抄下 11 个维度的数据。
📸 建议配图 2:寻找电影语言的“文本锚定”
1. 核心大白话原理
很多教程会教你“在浏览器里右键复制 XPath”,那拿到的叫“绝对路径”(类似:某某路某某小区某栋某单元)。万一明天网站多加了个广告,这个地址就彻底错了。
咱们今天手写“环境不变量”指路牌:
- 锁定大路标:抓电影名时,告诉小哥:“你先找到一个挂着
id='original_header'牌子的大房间,在这个房间里,去找只要名字里包含title的区域(用contains),里面写着的字就是电影名!” - 绝活:按字找地方:我们告诉小哥:“你在墙上找找,哪一行写着‘默认语言’这四个字,那这一行里写的内容就是我们要的语言!”网页随便改,只要“默认语言”这四个字还在,小哥就绝对不会抄错!
💻 核心代码
为了让这段代码直接能跑,我们先在函数里把抓到的原始信息原封不动地打包成字典返回。
from lxml import html
import requests
ROOT_URL = "https://themoviedb.org"
def get_movie_detail(url):
# 让小哥点进这部电影的完整链接,并打包成“菜单树”
details = requests.get(f"{ROOT_URL}/{url}")
data = html.fromstring(details.text)
# 1. 电影名
movie_name = data.xpath(
"//*[@id='original_header']/div[contains(@class, 'header_poster_wrapper')]//div[contains(@class, 'title')]//a/text()"
)
# 2. 年份
movie_year = data.xpath(
"//*[@id='original_header']/div[contains(@class, 'header_poster_wrapper')]//div[contains(@class, 'title')]//span/text()"
)
# 3. 上映时间
movie_release_date = data.xpath(
"//*[@id='original_header']//span[@class='release']/text()"
)
# 4. 类型(多标签,用逗号拼接)
movie_type = ",".join(
data.xpath(
"//*[@id='original_header']//span[@class='genres']/a/text()"
)
)
# 5. 时长
movie_duration = data.xpath(
"//*[@id='original_header']//span[@class='runtime']/text()"
)
# 6. 评分(拿隐藏属性的值)
movie_rating = data.xpath(
"//*[@id='consensus_pill']//div[@class='user_score_chart' and @data-percent]/@data-percent"
)
# 7. 语言【按字找地方的绝活】
movie_language = data.xpath(
"//*[contains(@class, 'facts')]//p[.//*[contains(text(), '默认语言')]]/text()"
)
# 8. 导演
movie_director = data.xpath(
"//*[@id='original_header']//li[p[contains(text(), 'Director')]]/p/a/text()"
)
# 9. 主演(多演员,用逗号拼接)
movie_actor = ",".join(
data.xpath(
"//*[@id='cast_scroller']/ol/li/p/a[contains(@href, '/person')]/text()"
)
)
# 10. slogan(宣传语)
movie_slogan = data.xpath(
"//*[@id='original_header']/div/section/div/h3/text()"
)
# 11. 简介
movie_synopsis = data.xpath(
"//*[@id='original_header']/div/section/div/div/p/text()"
)
# 把抄回来的东西做成一个精美的“小档案”(字典)返回
# .strip() 用于拍掉两边的空格。如果没捞到,就给个空白字符串 ""
return {
"电影名": movie_name[0].strip() if movie_name else "",
"年份": movie_year[0].strip() if movie_year else "",
"上映时间": movie_release_date[0].strip() if movie_release_date else "",
"类型": movie_type.strip() if movie_type else "",
"时长": movie_duration[0].strip() if movie_duration else "",
"评分": movie_rating[0].strip() if movie_rating else "",
"语言": movie_language[0].strip() if movie_language else "",
"导演": movie_director[0].strip() if movie_director else "",
"主演": movie_actor.strip() if movie_actor else "",
"slogan": movie_slogan[0].strip() if movie_slogan else "",
"简介": movie_synopsis[0].strip() if movie_synopsis else "",
}
# 单独测试一下这个小哥抄数据的能力
if __name__ == "__main__":
# 我们随便拿个电影小地址测试一下,看看能不能成功吐出小档案
test_data = get_movie_detail("/movie/278")
print(test_data)
🧠 语法知识点科普
网页上的信息是不确定的。比如有些老电影根本没有写 slogan。如果小哥强行去摘列表第一项,代码就会报错崩溃。
movie_slogan[0].strip() if movie_slogan else "" 这句话的意思就是:“如果小哥抓到了这个宣传语,就取第 0 项并清洗交出来;如果根本没捞到(是个空列表),那就给个空白字符串,千万别死机!”
🧼 第四步:洗数据中心(用正则驯服脏数据)
小哥把刚才的测试数据拿回来了,但是里面年份带着括号 (1994),上映时间塞着地区 1994-10-14 (US),时长更是混乱的 2h 22m。
我们需要写三个“清洁洗涤剂”函数,专门把这些脏数据洗白。这段代码导入了 re 库,可以直接运行测试!
💻 核心代码
import re
def format_movie_year(year):
# 【洗涤剂 A】用 strip("()") 剥掉两边的括号,把 "(1994)" 变成 "1994"
result = year[0].strip("()") if year else ""
return result
def format_movie_release_data(release_date):
# 【洗涤剂 B】利用正则,强行提取 YYYY-MM-DD 格式,丢弃括号里的地区噪声
pattern = r"(\d{4}-\d{2}-\d{2})\s*(.*?)"
result = re.search(pattern, release_date[0].strip() if release_date else "")
return result.group(1) if result else ""
def format_movie_duration(duration):
# 【洗涤剂 C】把 "2h 22m" 统一换算成纯分钟数字
if not duration:
return "0m"
pattern = r"(?:(\d+)h)?\s*(?:(\d+)m)?"
match = re.match(pattern, duration[0].strip())
if match:
hours = int(match.group(1) or 0)
minutes = int(match.group(2) or 0)
return f"{hours * 60 + minutes}m"
else:
return "0m"
# 测试一下洗涤效果
if __name__ == "__main__":
print(format_movie_year(["(1994)"])) # 应该输出 1994
print(format_movie_release_data(["1994-10-14 (US)"])) # 应该输出 1994-10-14
print(format_movie_duration(["2h 22m"])) # 应该输出 142m
🧠 语法知识点科普
你可以把 re(正则表达式)理解为一种带有超强过滤网的漏斗。我们通过定制特定的“网眼形状”(如 \d{4} 代表四个数字),就能把混在泥沙里的纯金子(纯日期、纯数字)精准地过滤出来。
💾 第五步:完整版大合体!(完美落盘为 CSV)
现在所有的拼图都齐了!我们把前四步的函数全部拼在一起,并加上最后的数据保存功能。这就是我们的终极完整版代码。
📸 建议配图 3:最终生成的表格成果
💻 核心完整代码
import csv
from lxml import html
import re
import requests
ROOT_URL = "https://themoviedb.org"
# --- 数据清洗模块 ---
def format_movie_year(year):
return year[0].strip("()") if year else ""
def format_movie_release_data(release_date):
pattern = r"(\d{4}-\d{2}-\d{2})\s*(.*?)"
result = re.search(pattern, release_date[0].strip() if release_date else "")
return result.group(1) if result else ""
def format_movie_duration(duration):
if not duration:
return "0m"
pattern = r"(?:(\d+)h)?\s*(?:(\d+)m)?"
match = re.match(pattern, duration[0].strip())
if match:
hours = int(match.group(1) or 0)
minutes = int(match.group(2) or 0)
return f"{hours * 60 + minutes}m"
return "0m"
# --- 详情页抓取模块 ---
def get_movie_detail(url):
print(f"🔍 小哥正在深入查看电影:{url} 的详细信息...")
details = requests.get(f"{ROOT_URL}/{url}")
data = html.fromstring(details.text)
movie_name = data.xpath(
"//*[@id='original_header']/div[contains(@class, 'header_poster_wrapper')]//div[contains(@class, 'title')]//a/text()"
)
movie_year = data.xpath(
"//*[@id='original_header']/div[contains(@class, 'header_poster_wrapper')]//div[contains(@class, 'title')]//span/text()"
)
movie_release_date = data.xpath(
"//*[@id='original_header']//span[@class='release']/text()"
)
movie_type = ",".join(
data.xpath(
"//*[@id='original_header']//span[@class='genres']/a/text()"
)
)
movie_duration = data.xpath(
"//*[@id='original_header']//span[@class='runtime']/text()"
)
movie_rating = data.xpath(
"//*[@id='consensus_pill']//div[@class='user_score_chart' and @data-percent]/@data-percent"
)
movie_language = data.xpath(
"//*[contains(@class, 'facts')]//p[.//*[contains(text(), '默认语言')]]/text()"
)
movie_director = data.xpath(
"//*[@id='original_header']//li[p[contains(text(), 'Director')]]/p/a/text()"
)
movie_actor = ",".join(
data.xpath(
"//*[@id='cast_scroller']/ol/li/p/a[contains(@href, '/person')]/text()"
)
)
movie_slogan = data.xpath(
"//*[@id='original_header']/div/section/div/h3/text()"
)
movie_synopsis = data.xpath(
"//*[@id='original_header']/div/section/div/div/p/text()"
)
# 包装并调用清洗函数
return {
"电影名": movie_name[0].strip() if movie_name else "",
"年份": format_movie_year(movie_year),
"上映时间": format_movie_release_data(movie_release_date),
"类型": movie_type.strip() if movie_type else "",
"时长": format_movie_duration(movie_duration),
"评分": movie_rating[0].strip() if movie_rating else "",
"语言": movie_language[0].strip() if movie_language else "",
"导演": movie_director[0].strip() if movie_director else "",
"主演": movie_actor.strip() if movie_actor else "",
"slogan": movie_slogan[0].strip() if movie_slogan else "",
"简介": movie_synopsis[0].strip() if movie_synopsis else "",
}
# --- 数据保存模块 ---
def save_movie_file(movie_list):
print("💾 正在把小哥抄来的小档案整整齐齐地塞进文件...")
with open("data/movie.csv", "w", newline="", encoding="utf-8") as file:
headers = [
"电影名",
"年份",
"上映时间",
"类型",
"时长",
"评分",
"语言",
"导演",
"主演",
"slogan",
"简介",
]
writer = csv.DictWriter(file, fieldnames=headers)
writer.writeheader()
writer.writerows(movie_list)
print("🎉 保存数据成功!任务圆满完成!")
# --- 控制中枢模块 ---
def main():
top_movies = []
for i in range(3):
print(f"跑腿小哥:正在切入第 {i+1} 层网络空间...")
if i == 0:
response = requests.get(f"{ROOT_URL}/movie/top-rated")
else:
payload = f"air_date.gte=&air_date.lte=&certification=&certification_country=HK&debug=&first_air_date.gte=&first_air_date.lte=&include_adult=false&include_softcore=false&latest_ceremony.gte=&latest_ceremony.lte=&page={i+1}&primary_release_date.gte=&primary_release_date.lte=®ion=&release_date.gte=&release_date.lte=2027-02-07&show_me=everything&sort_by=vote_average.desc&vote_average.gte=0&vote_average.lte=10&vote_count.gte=300&watch_region=HK&with_genres=&with_keywords=&with_networks=&with_origin_country=&with_original_language=&with_watch_monetization_types=&with_watch_providers=&with_release_type=&with_runtime.gte=0&with_runtime.lte=400"
response = requests.post(
f"{ROOT_URL}/discover/movie/items", data=payload
)
data = html.fromstring(response.text)
movies = data.xpath(
"//*[@class='media-list-results contents']/div/div/div/a[1]/@href"
)
for movie in movies:
movie_details = get_movie_detail(movie)
top_movies.append(movie_details)
save_movie_file(top_movies)
if __name__ == "__main__":
main()
🧠 语法知识点科普:为什么推荐用 csv.DictWriter?
如果用普通的 csv.writer,你是按照顺序去塞数据的。万一有一部电影缺了“导演”,你后面所有的信息都会往前挪一格,整个表格就全乱套了。
而 csv.DictWriter 认的是名字标签(字典的 Key)。只要小档案里写着 {'导演': '肖申克导演'},不管前面缺了啥,它都能精准塞进“导演”那一列,绝对不会发生错位。
🛠️ 结语:代码日后失效了?小白现场自救绝学!
如果明天网站改版,小哥罢工了,报了 IndexError: list index out of range,不要慌!
-
去控制台(Console)原地给小哥探路:
在你要抓取的网页上按F12,把面板切换到Console(控制台)。 -
直接在控制台里输入
$x("你的指路牌路径")敲回车。- 例如你输入:
$x("//*[@id='original_header']//span[@class='release']/text()") - 如果回车后能吐出数组,说明路径是亮的,可以用!
- 如果吐出来是空的
[],说明网站把class='release'给改名了。你可以直接在这个控制台里一遍遍修改测试,直到它能吐出数据为止,然后把调通的路径复制回 Python 代码,小哥立刻复活!
- 例如你输入: