引言
如果你正在学习Python爬虫,那么猫眼电影TOP100榜单一定是绕不开的经典案例。它几乎出现在每一本爬虫入门教材和每一篇爬虫教程中------原因很简单:页面结构清晰、数据规整、反爬策略温和但不失代表性,非常适合初学者理解HTTP请求、HTML解析和反爬应对的完整流程。
然而,理想很丰满,现实往往会在你第一次运行代码时给你一个下马威。当你兴致勃勃地打开浏览器开发者工具,用几行Python代码发起请求时,返回的可能不是预想中的HTML页面,而是一个冷冰冰的验证页面或空白响应。猫眼平台设置了多道反爬虫防线,如果请求头配置不当,服务器会直接拒绝提供正常数据。

本文将从零开始,带你完整走一遍"Requests+正则表达式"爬取猫眼电影TOP100的全过程------从URL分析、请求伪装、正则提取,到反爬应对和隧道代理集成,一篇搞定。
一、URL分析:找到数据入口
1.1 目标站点
猫眼电影TOP100榜单的入口地址是:
https://maoyan.com/board/4
打开这个页面,你会看到排名1到10的电影信息------片名、主演、上映时间、评分等。但榜单上明明有100部电影,为什么只显示了10部?
1.2 分页规律
滚动到页面底部,你会发现一个分页导航。点击第2页,观察浏览器地址栏的变化:
第1页:https://maoyan.com/board/4
第2页:https://maoyan.com/board/4?offset=10
第3页:https://maoyan.com/board/4?offset=20
规律一目了然:offset参数控制了页面的偏移量。当offset=0时,显示第1-10名;offset=10时,显示第11-20名;以此类推。每页10部电影,总共10页。
所以,要获取完整的TOP100数据,只需要循环请求10次,每次将offset依次设置为0、10、20......90即可。
1.3 页面类型判断
在写爬虫之前,必须先确认页面是服务端渲染(SSR) 还是客户端渲染(CSR) 。打开开发者工具的Network选项卡,刷新页面,查看返回的HTML源码------如果电影信息直接出现在HTML中,说明是服务端渲染,用requests就能拿到数据;如果HTML只有空骨架、数据通过额外的AJAX请求加载,就需要动用Selenium或分析API接口。
猫眼TOP100榜单属于前者------页面是服务端渲染的,所有电影信息都直接包含在HTML源码中。这意味着我们完全可以用requests获取页面,然后用正则表达式从HTML中提取数据,无需动用浏览器自动化工具。
二、Requests请求:伪装成"真人"
2.1 为什么需要伪装?
猫眼平台的第一道反爬虫防线是User-Agent检测 。服务器会检查请求头中的User-Agent字段------如果它看起来不像一个真实的浏览器,服务器就会拒绝返回正常数据。
一个裸奔的requests.get()请求,其默认的User-Agent通常是类似python-requests/2.28.2这样的字符串。这等于直接告诉服务器:"我是爬虫,请拦截我。"
2.2 构造请求头
解决方案很简单------从真实浏览器中复制一个User-Agent,放进请求头里:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
除了User-Agent,还可以补充Referer、Accept等头部信息,让请求看起来更像一个真实的浏览器访问:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://www.maoyan.com/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive'
}
2.3 发送请求
有了请求头,就可以发送请求了:
import requests
def get_one_page(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
return None
2.4 请求频率控制
猫眼平台会检测规律性请求------如果每2秒准时发一次请求,风控系统会标记这个IP为"爬虫模式"。应对策略是在请求之间加入随机延迟,模拟人类浏览的不确定性:
import time
import random
time.sleep(random.uniform(1, 3)) # 1到3秒随机延迟
三、正则表达式:从HTML中"淘金"
3.1 为什么选正则表达式?
在解析HTML的诸多工具中------BeautifulSoup、XPath、PyQuery、正则表达式------为什么偏偏选正则?原因很简单:我们还没有系统学习HTML解析库,而正则表达式是Python内置的、无需额外安装的工具。对于结构规整的页面,正则表达式完全够用,而且执行效率更高。
3.2 查看真实源码
重要提醒 :不要直接在Elements选项卡中查看源码------那里的HTML可能经过JavaScript操作,与原始请求返回的内容不同。正确的做法是在Network选项卡中找到页面的主请求,查看Response内容。
3.3 分析HTML结构
在Network中查看源码,你会发现每部电影的信息都包裹在一个<dd>标签中:
<dd>
<i class="board-index board-index-1">1</i>
<a href="/films/..." title="霸王别姬" class="image-link" data-act="boarditem-click">
<img src="..." alt="" class="board-img" />
</a>
<div class="movie-item-info">
<p class="name"><a href="/films/..." title="霸王别姬" data-act="boarditem-click">霸王别姬</a></p>
<p class="star">主演:张国荣,张丰毅,巩俐</p>
<p class="releasetime">上映时间:1993-01-01(中国大陆)</p>
</div>
<div class="movie-item-number score-num">
<p class="score"><i class="integer">9.</i><i class="fraction">6</i></p>
</div>
</dd>
3.4 编写正则表达式
根据上面的HTML结构,我们可以逐一写出每个字段的正则表达式:
(1)排名 :在class="board-index"的<i>标签中
pattern_rank = re.compile(r'<i class="board-index.*?>(.*?)</i>', re.S)
(2)图片地址 :在<img>标签的data-src属性中
pattern_img = re.compile(r'<img.*?data-src="(.*?)"', re.S)
(3)电影名称 :在class="name"的<p>标签中的<a>标签内
pattern_name = re.compile(r'<p class="name"><a.*?>(.*?)</a></p>', re.S)
(4)主演信息 :在class="star"的<p>标签中
pattern_star = re.compile(r'<p class="star">(.*?)</p>', re.S)
(5)上映时间 :在class="releasetime"的<p>标签中
pattern_time = re.compile(r'<p class="releasetime">(.*?)</p>', re.S)
(6)评分 :由integer和fraction两部分组成
pattern_score = re.compile(r'<i class="integer">(.*?)</i>.*?<i class="fraction">(.*?)</i>', re.S)
3.5 将正则组合在一起
更高效的做法是一次性匹配整个<dd>区块,然后从中提取各个字段:
import re
pattern = re.compile(
r'<dd>.*?board-index.*?>(.*?)</i>.*?'
r'data-src="(.*?)".*?'
r'name.*?>(.*?)</a>.*?'
r'star.*?>(.*?)</p>.*?'
r'releasetime.*?>(.*?)</p>.*?'
r'integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>',
re.S
)
items = re.findall(pattern, html)
这样一次正则匹配就能拿到排名、图片、名称、主演、上映时间、整数部分和小数部分------评分由整数部分和小数部分拼接而成。
四、完整代码实现
4.1 基础版
将以上所有环节组合起来,就得到了一个完整的猫眼TOP100爬虫:
import requests
import re
import time
import random
import json
def get_one_page(url):
"""获取单页HTML"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://www.maoyan.com/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.text
except Exception as e:
print(f'请求异常: {e}')
return None
def parse_one_page(html):
"""解析单页HTML,提取电影信息"""
pattern = re.compile(
r'<dd>.*?board-index.*?>(.*?)</i>.*?'
r'data-src="(.*?)".*?'
r'name.*?>(.*?)</a>.*?'
r'star.*?>(.*?)</p>.*?'
r'releasetime.*?>(.*?)</p>.*?'
r'integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>',
re.S
)
items = re.findall(pattern, html)
for item in items:
yield {
'rank': item[0],
'image': item[1],
'title': item[2].strip(),
'star': item[3].strip() if item[3] else '',
'release_time': item[4].strip() if item[4] else '',
'score': item[5] + item[6] # 拼接整数部分和小数部分
}
def save_to_file(data, filename='maoyan_top100.txt'):
"""保存数据到文件"""
with open(filename, 'w', encoding='utf-8') as f:
for item in data:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
print(f'数据已保存到 {filename}')
def main():
"""主函数"""
all_movies = []
base_url = 'https://maoyan.com/board/4'
for offset in range(0, 100, 10):
url = f'{base_url}?offset={offset}'
print(f'正在爬取第 {offset//10 + 1} 页 (offset={offset})...')
html = get_one_page(url)
if not html:
print(f'第 {offset//10 + 1} 页获取失败')
continue
movies = list(parse_one_page(html))
all_movies.extend(movies)
print(f'第 {offset//10 + 1} 页获取 {len(movies)} 部电影,累计 {len(all_movies)} 部')
# 随机延迟,模拟人类浏览行为
time.sleep(random.uniform(1, 3))
save_to_file(all_movies)
print(f'爬取完成!共 {len(all_movies)} 部电影')
if __name__ == '__main__':
main()
4.2 代码结构解析
这段代码遵循了爬虫的经典三层架构:
| 模块 | 函数 | 职责 |
|---|---|---|
| HTML下载器 | get_one_page() |
发送HTTP请求,获取页面源码 |
| HTML解析器 | parse_one_page() |
用正则表达式提取结构化数据 |
| 数据存储器 | save_to_file() |
将数据持久化到文件 |
4.3 运行效果
运行上述代码,你会看到类似如下的输出:
正在爬取第 1 页 (offset=0)...
第 1 页获取 10 部电影,累计 10 部
正在爬取第 2 页 (offset=10)...
第 2 页获取 10 部电影,累计 20 部
...
爬取完成!共 100 部电影
数据已保存到 maoyan_top100.txt
打开maoyan_top100.txt,每一行都是一部电影的JSON数据,包含排名、片名、主演、上映时间、评分等信息。
五、反爬应对与隧道代理
5.1 猫眼的反爬体系
猫眼平台部署了多层次的反爬机制,主要包括:
| 反爬手段 | 说明 | 应对策略 |
|---|---|---|
| User-Agent检测 | 检查请求头中的UA字段 | 伪装真实浏览器UA |
| 请求频率限制 | 限制同一IP的请求频率 | 随机延迟、控制速率 |
| IP封禁 | 高频访问导致IP被封 | 使用代理IP |
| 字体反爬 | 评分/票房数据用自定义字体渲染 | 本案例不涉及 |
5.2 为什么需要代理?
即使你完美配置了User-Agent和请求延迟,当需要反复运行爬虫或采集规模扩大时,IP封禁依然不可避免。猫眼平台会检测同一IP在短时间内的访问频率,一旦触发阈值,该IP就会被暂时或永久封禁。
传统方案是自建代理IP池------从网上爬取公开的代理IP,检测可用性后保存起来,每次请求轮换使用。但这种方法存在两个核心痛点:
-
IP质量参差不齐:免费代理资源已被大量滥用,可用性低
-
手动维护繁琐:需要持续检测IP有效性,被封后手动更换
5.3 隧道代理:更优雅的解决方案
隧道代理是传统代理的升级方案。你不需要手动维护IP池,只需配置好接入信息。每次发送请求时,隧道代理会自动把流量引导至不同的出口IP------相当于给爬虫配备了一条专属的"IP安全隧道"。
站大爷隧道代理在爬虫场景中有几个突出的优势:
-
自动切换无感知:每次请求自动更换出口IP,无需手动干预
-
覆盖范围广泛:覆盖全国99%地域
-
主备双隧道:持续更新IP池,稳定性有保障
-
灵活配置:支持精细化的IP轮换周期自定义
-
多种鉴权模式:支持白名单、用户名密码等多种鉴权方式
5.4 在爬虫中集成站大爷隧道代理
在requests中集成隧道代理非常简单:
import requests
# 站大爷隧道代理配置
PROXIES = {
"http": "http://隧道代理地址:端口",
"https": "https://隧道代理地址:端口"
}
def get_one_page_with_proxy(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
try:
response = requests.get(
url,
headers=headers,
proxies=PROXIES, # 隧道代理自动切换IP
timeout=15
)
if response.status_code == 200:
return response.text
# 遇到403/429,隧道代理自动切换出口IP重试
if response.status_code in [403, 429]:
print('请求被拒绝,隧道代理自动切换IP重试...')
time.sleep(2)
except Exception as e:
print(f'请求异常: {e}')
return None
对于需要精细控制IP轮换的场景,站大爷隧道代理支持通过参数自定义IP更换频率:
# 每300秒更换一次IP
PROXIES = {
"http": "http://隧道代理地址:端口?period=300",
"https": "https://隧道代理地址:端口?period=300"
}
实际应用中,隧道代理可以将IP封禁率从80%以上降至5%以下,大幅提升采集的稳定性。
六、常见问题与避坑指南
6.1 请求返回空白或验证页面怎么办?
-
检查是否设置了正确的
User-Agent -
补充
Referer、Accept等完整的请求头 -
确认URL是否正确(注意是
maoyan.com而非www.maoyan.com)
6.2 正则表达式匹配不到数据怎么办?
-
不要在Elements中看源码,要去Network中查看原始响应
-
检查页面结构是否发生了变化------猫眼可能会更新HTML
-
使用
print(html[:500])打印部分源码,肉眼确认结构
6.3 爬取过程中被封IP怎么办?
-
增加请求间隔,使用随机延迟而非固定间隔
-
使用站大爷隧道代理自动切换IP
-
降低并发度,单线程顺序请求
6.4 评分数据出现乱码或特殊字符怎么办?
猫眼可能使用字体反爬 技术保护评分数据------评分数字用自定义字体渲染,直接提取HTML只能看到乱码。本案例中评分以integer和fraction两个<i>标签呈现,提取后拼接即可。如果遇到更复杂的字体反爬,需要额外处理字体映射。
6.5 法律与合规提醒
-
爬取前查看猫眼的
robots.txt -
控制请求频率,避免对目标服务器造成过大压力
-
仅将数据用于学习和研究目的,勿用于商业用途
七、总结
本文从猫眼电影TOP100的URL分析入手,完整走了一遍"Requests+正则表达式"爬虫的实战流程:
| 环节 | 核心知识点 | 关键代码 |
|---|---|---|
| URL分析 | 分页规律、offset参数 | range(0, 100, 10) |
| 请求发送 | User-Agent伪装、请求头构造 | requests.get(url, headers=headers) |
| HTML解析 | 正则表达式、非贪婪匹配 | re.compile(r'<dd>.*?...', re.S) |
| 数据提取 | 排名、片名、主演、时间、评分 | re.findall(pattern, html) |
| 反爬应对 | 随机延迟、代理IP | time.sleep(random.uniform(1, 3)) |
| IP封禁 | 站大爷隧道代理 | proxies={'http': '隧道地址:端口'} |
技术选型速览 :猫眼TOP100是服务端渲染的静态页面,推荐"
requests+ 正则表达式 + 站大爷隧道代理"的组合方案------请求高效、解析轻量、IP安全。
猫眼电影TOP100是爬虫入门的经典案例------页面规整、反爬温和但不失代表性、数据字段丰富。通过这个项目,你可以完整地体验到爬虫的请求→解析→存储 全流程,同时理解User-Agent伪装、请求频率控制、代理IP等核心反爬应对策略。
正则表达式虽然写起来略显繁琐,但它是每个开发者都应该掌握的基本功。当你面对一个结构规整的页面时,正则表达式依然是最轻量、最高效的解析工具之一。
希望本文能帮助你在爬虫学习的道路上迈出坚实的一步。当你成功跑通代码、看到100部电影数据整整齐齐地出现在文件里时,那种成就感------就是编程最纯粹的魅力所在。