爬虫学习——第一个代码,逐行解析。
本文介绍了Python爬虫开发的核心流程和技术要点。首先讲解了依赖库的导入(如requests、BeautifulSoup等)及其作用,然后详细说明了全局配置的设定,包括URL模板、请求头、反爬策略等关键参数。接着展示了爬虫函数的具体实现,涵盖HTTP请求发送、HTML解析、数据提取和异常处理机制。最后介绍了数据存储(CSV格式)和可视化(Matplotlib图表)的方法,并解释了程序入口if n

第一步、导入依赖库
# ===================== 第一步:导入依赖库(类比C语言的#include) =====================
# requests库:用于发送HTTP请求(核心!爬虫向网站服务器要数据的工具)
# 类比C语言的socket库(网络通信),但requests封装了复杂逻辑,更易用
import requests
# BeautifulSoup库:用于解析HTML页面(把服务器返回的乱码HTML转成可提取的结构化数据)
# 类比C语言的字符串处理函数,但专门针对HTML标签解析
from bs4 import BeautifulSoup
# pandas库:用于数据存储和处理(把爬取的零散数据整理成表格,类比C的数组/结构体)
import pandas as pd
# matplotlib库:用于绘制图表(把数据可视化,类比C的绘图库,但更简单)
import matplotlib.pyplot as plt
# random库:生成随机数(用于随机休眠,反反爬)
import random
# time库:用于休眠(控制爬取速度,避免被网站封禁)
import time
什么是依赖库?
依赖库(Dependency Libraries)是软件开发中一个非常重要的概念。你可以把它理解为:在构建自己的“主项目”时,所需要“借用”或“依赖”的、由他人已经编写好的、可复用的代码集合。
简单来说,就是你不需要从零开始造轮子,而是使用别人已经造好的、高质量的轮子、发动机或方向盘,来组装自己的汽车。
依赖库的关键特点:
可复用性:一个库可以被无数个项目使用,避免了重复劳动。
特定功能:每个库通常专注于解决一个特定领域的问题。
网络请求:
axios,requests(Python)日期处理:
moment.js,date-fns用户界面组件:
React,Vue.js(它们本身也是库/框架)数据可视化:
D3.js,ECharts数学计算:
NumPy(Python)包管理:现代语言有专门的工具来管理这些库。
JavaScript/Node.js:
npm,yarn,pnpmPython:
pip,condaJava:
Maven,GradlePHP:
Composer依赖声明文件:项目里会有一个文件(如
package.json,requirements.txt,pom.xml)来显式声明所有依赖库的名字和版本,确保其他人能一键安装完全相同的环境。
介绍from和import关键词的用法?
在 Python 中,
from和import是用于导入模块 / 对象的核心关键字,代码片段from bs4 import BeautifulSoup是典型的 “从模块中导入指定对象” 的用法,下面分维度详细解释:一、核心作用
import:本质是 “引入”,用于加载整个模块(或模块中的特定对象)到当前代码的命名空间中;from:限定 “导入的来源”,指定从哪个模块中导入对象,常与import配合使用。二、基础用法拆解
1. 单独使用
import(导入整个模块)语法:
import 模块名 [as 别名]作用:将整个模块加载到当前命名空间,使用模块内的对象时需要加模块名.前缀。示例(对应你的场景):
# 导入整个 bs4 模块 import bs4 # 使用 bs4 模块中的 BeautifulSoup 类,必须加 bs4. 前缀 soup = bs4.BeautifulSoup(html_text, "html.parser")2.
from ... import ...(导入模块中的指定对象)语法:
from 模块名 import 对象名 [as 别名]作用:仅将模块内的指定对象(类、函数、变量等)加载到当前命名空间,使用时无需加模块名前缀。示例(你的代码就是这种用法):
# 从 bs4 模块中,只导入 BeautifulSoup 这个类 from bs4 import BeautifulSoup # 直接使用 BeautifulSoup,无需加 bs4. 前缀 soup = BeautifulSoup(html_text, "html.parser")3.
from ... import *(导入模块中的所有对象)语法:
from 模块名 import *作用:将模块内的所有公开对象(不以_开头的)加载到当前命名空间,不推荐(易命名冲突)。示例:
# 导入 bs4 模块的所有公开对象(不推荐) from bs4 import * # 直接使用 BeautifulSoup、Tag 等对象 soup = BeautifulSoup(html_text, "html.parser")三、关键补充(别名
as)如果对象名 / 模块名过长,或与当前命名空间的变量冲突,可通过
as给对象 / 模块起别名。示例:
# 给模块起别名 import bs4 as b4 soup = b4.BeautifulSoup(html_text, "html.parser") # 给对象起别名 from bs4 import BeautifulSoup as BS soup = BS(html_text, "html.parser")四、为什么你的代码用
from bs4 import BeautifulSoup?
bs4是一个功能丰富的模块(包含BeautifulSoup、Tag、NavigableString等多个类 / 函数),而你只需要用到核心的BeautifulSoup类来解析 HTML,用这种方式可以:
- 减少命名空间冗余(只加载需要的对象);
- 简化代码(无需每次写
bs4.前缀);- 提高代码可读性。
总结
用法 语法示例 使用对象时的写法 适用场景 导入整个模块 import bs4bs4.BeautifulSoup需要使用模块多个对象时 导入指定对象 from bs4 import BeautifulSoupBeautifulSoup仅需模块中少数特定对象时(推荐) 导入所有对象 from bs4 import *BeautifulSoup临时测试(不推荐正式代码)
matplotlib.pyplot 解析。
1. 先理清模块结构:
matplotlibvspyplot
matplotlib是 Python 绘图的核心库,而pyplot是matplotlib下的一个子模块(可以理解为 “绘图快捷工具包”),专门提供类似 MATLAB 风格的简易绘图接口。2. 句点(
.)的核心作用:访问模块 / 对象的属性 / 方法
matplotlib.pyplot.xxx里的.是 Python 通用语法,作用是:从matplotlib库中找到pyplot子模块,再从pyplot中调用 / 访问xxx(函数、变量、类等)。
.是 Python 全场景通用的 “属性访问符”:比如math.pi(从 math 模块访问圆周率)、list.append()(从列表类访问 append 方法),逻辑完全一致。3. 补充:为什么大家习惯写
import matplotlib.pyplot as plt?
matplotlib.pyplot名称太长,用as plt简写是为了代码简洁,是行业通用写法,和.的语法功能无关。
什么是HTTP?
HTTP(超文本传输协议)是一种用于在网络上传输数据的应用层协议,它是互联网上数据通信的基础。简单来说,它定义了客户端(如浏览器)和服务器之间如何交换信息。
状态码
服务器用状态码表示请求结果:
500:服务器内部错误。
404:资源未找到。
200:成功。URL定位资源
通过统一资源定位符(URL)指定资源地(如
https://example.com/page)。工作流程示例:
浏览器输入
https://www.example.com。浏览器发送HTTP请求到Example的服务器。
服务器处理请求,返回HTML、CSS等文件。
浏览器解析文件,渲染成网页。
第二步、全局配置
# ===================== 第二步:全局配置(类比C语言的#define宏定义) =====================
# 豆瓣Top250的基础URL,{}是占位符,后续替换成页码(start=0是第1页,start=25是第2页)
BASE_URL = "https://movie.douban.com/top250?start={}&filter="
# 爬取页数:2页(每页25部,共50部),新手先爬少量数据避免被封
PAGE_NUM = 2
# 最小休眠时间(秒):爬取每一页前等待的最短时间,延长时间降低被封风险
MIN_SLEEP = 12
# 最大休眠时间(秒):随机休眠的上限,模拟真人操作(反爬核心)
MAX_SLEEP = 18
# 爬取数据保存的CSV文件路径(CSV是简易表格,比Excel更通用)
SAVE_CSV = "douban_top50_view_count.csv"
# 生成的散点图保存路径
SCATTER_PNG = "top50_view_count_scatter.png"
# 请求头(关键!伪装成浏览器,避免被网站识别为爬虫)
# 类比C语言的HTTP请求头构造,告诉服务器“我是正常浏览器,不是爬虫”
HEADERS = {
# User-Agent:浏览器身份标识(必须!不同浏览器有不同值,这里是Chrome)
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
# Cookie:用户登录标识(豆瓣部分数据需要Cookie才能爬取,替换成自己的)
# 类比C语言的登录凭证,没有Cookie可能返回403/空数据
"Cookie": 'bid=MynUe3dExLk; ll="118239"; __utmc=30149280; __utmt=1; __utmz=30149280.1765478156.1.1.utmcsr=cn.bing.com|utmccn=(referral)|utmcmd=referral|utmcct=/'
}
# 中文字体配置(解决matplotlib绘图时中文乱码问题)
# 类比C语言的字体设置,不配置的话图表里的中文会显示成方块
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置默认字体为“黑体”
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示异常问题
这些大写的标识符(如BASE_URL、PAGE_NUM、MIN_SLEEP等)是什么?
这些大写的标识符(如
BASE_URL、PAGE_NUM、MIN_SLEEP等)属于Python 中的常量(约定俗成的),也可以称为配置项 / 参数项。下面详细拆解:一、为什么用大写?—— 编程的命名规范(约定俗成,本质是变量)
在 Python 中,并没有语法层面的 “常量”(不像 C/C++ 有
const关键字,定义后无法修改),但开发者们形成了一个通用的命名约定:全部字母大写,单词之间用下划线分隔(蛇形命名法),表示这个变量是 **“常量”,即不建议在代码中修改其值 ** 的配置项 / 固定参数。
这么做的目的是:
- 视觉区分:大写的标识符在代码中非常醒目,一眼就能看出是 “配置参数”,而非业务逻辑中的临时变量(如
page、data、headers等小写 / 驼峰命名的变量)。- 语义明确:告诉其他开发者(或未来的自己):“这个值是程序的配置项,是核心参数,不要随意修改”。
- 统一维护:把所有关键配置集中定义在代码开头,后续要调整时(比如想爬 5 页而非 2 页、修改休眠时间),只需要改这里的常量值,不用在业务逻辑代码中到处找,便于维护。
二、这些大写标识符的具体含义(结合你的爬虫代码)
代码中的这些大写项都是豆瓣 Top250 爬虫的核心配置参数,各司其职:
标识符 作用 BASE_URL豆瓣 Top250 的请求 URL 模板(带格式化占位符 {},用于拼接分页的start参数)PAGE_NUM爬取的页数(控制爬取数据量,新手爬少量数据避免被封 IP) MIN_SLEEP爬取每页前的最短休眠时间(反爬机制:避免请求过快被识别为爬虫) MAX_SLEEP随机休眠的最长时间(模拟真人操作,进一步降低反爬风险) SAVE_CSV爬取数据保存的 CSV 文件路径(指定数据存储位置) SCATTER_PNG生成的散点图保存路径(指定可视化结果的存储位置) HEADERS请求头配置(伪装浏览器的核心参数,如 User-Agent、cookie 等) 三、补充:和 C 语言的对比
如果你有 C/C++ 基础,可以这样理解:
- Python 中这种大写命名的 “常量” ≈ C 语言中用
#define定义的宏常量(如#define PAGE_NUM 2),都是配置项的集中定义,便于修改和维护。- 区别是:C 的
#define是预处理阶段替换,语法上不可修改;Python 的大写变量本质还是普通变量,只是约定不修改(如果强行修改,语法上也允许,但不推荐)。四、拓展:
HEADERS的补充定义(你的代码中未完成)
HEADERS作为请求头配置,通常需要定义为字典类型,示例如下:# 请求头(关键!伪装成浏览器,避免被网站识别为爬虫) # 类比C语言的HTTP请求头构造,告诉服务器“我是正常浏览器,不是爬虫” 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://movie.douban.com/", "Accept-Language": "zh-CN,zh;q=0.9" }这里的
HEADERS同样是大写的配置项,存储请求头的键值对,供爬虫的requests库使用。总结:这些大写字体是Python 开发者约定俗成的常量 / 配置项,核心作用是集中管理参数、提升代码可读性和可维护性。
他们是不用定义直接就可以用的吗?
这些大写的标识符(如
BASE_URL、PAGE_NUM)和 Python 中普通变量一样,必须先定义(赋值),才能在代码中使用,否则会直接抛出NameError(名称未定义)异常。一、为什么会有 “不用定义就能用” 的错觉?
你看到的代码片段里,这些大写标识符已经做了赋值定义(除了
HEADERS只写了变量名,没赋值),比如:# 这就是「定义+赋值」的过程,相当于给变量绑定了一个值 BASE_URL = "https://movie.douban.com/top250?start={}&filter=" PAGE_NUM = 2 # 定义并赋值为2 MIN_SLEEP = 12 # 定义并赋值为12而
HEADERS只写了变量名,没有赋值,这在 Python 中是不完整的,直接使用会报错:# 错误示例:只写了HEADERS,没赋值 HEADERS print(HEADERS) # 运行后会报:NameError: name 'HEADERS' is not defined二、定义的本质:和普通变量完全一致
这些大写标识符本质上就是 Python 的变量,只是遵循 “全大写命名 = 常量(不建议修改)” 的约定。它们的定义规则和普通变量一模一样:
- 必须赋值才能使用:变量名 = 值(如
MAX_SLEEP = 18)。- 定义位置:通常放在代码开头(便于维护),也可以放在函数外(全局变量)或函数内(局部变量)。
- 如果未定义直接用,必然报错:
# 未定义的情况 print(UNDEFINED_CONST) # 抛出NameError: name 'UNDEFINED_CONST' is not defined三、补充:Python 中 “真正不用定义就能用” 的情况
只有以下几种场景,标识符不用自己定义就能用,和这些大写配置项完全不同:
- Python 内置关键字:如
if、for、def、import等,是 Python 语法的一部分,天然存在(但不能作为变量名)。- Python 内置函数 / 对象:如
print()、len()、range()、list()等,是 Python 自带的工具,可直接调用。- 导入的模块 / 对象:如
import requests后,requests就能用;from math import pi后,pi(圆周率)就能直接用(这是别人定义好的常量)。四、结合你的代码,补全
HEADERS的定义(必须做的)你代码中的
HEADERS是未完成的,需要赋值为字典类型才能使用,示例:# 正确的定义方式:赋值为字典(请求头的键值对) 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://movie.douban.com/" }定义后,后续在使用
requests.get()请求时,就能传入这个HEADERS:import requests response = requests.get(BASE_URL.format(0), headers=HEADERS) # 此时HEADERS已定义,可正常使用总结
这些大写的配置项不是天生存在的,必须通过
变量名 = 值的方式完成定义(赋值)后才能使用;它们和普通变量的唯一区别,只是命名约定(全大写表示 “不建议修改的常量”)。
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置默认字体为“黑体”
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示异常问题 这两行代码的作用?
这两行代码是Matplotlib(Python 的绘图库,通常简写为 plt)解决中文显示乱码 / 负号显示异常的核心配置,下面详细拆解其作用、原理和使用细节:
一、代码的核心作用
Matplotlib 的默认字体是英文字体(如 DejaVu Sans),不支持中文字符的渲染,直接绘制包含中文的图表时,中文会显示为方框(□)或乱码;同时,默认配置下负号(-)也可能出现显示异常(比如变成方块)。这两行代码就是为了修复这两个问题。
二、逐行解析代码
1.
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams:Matplotlib 的运行时配置参数(runtime configuration),是一个字典,用于全局设置 Matplotlib 的绘图样式(字体、颜色、分辨率等)。
font.sans-serif:配置项的键,表示无衬线字体(日常显示最常用的字体类型,如黑体、微软雅黑等)。
['SimHei']:设置无衬线字体的优先级列表,这里指定为 **“SimHei”(黑体)**,表示 Matplotlib 绘图时优先使用黑体渲染文字,黑体支持中文显示。补充:其他常用的中文字体(可替换 SimHei)
字体名称(英文) 字体名称(中文) 适用系统 SimHei 黑体 Windows 系统 Microsoft YaHei 微软雅黑 Windows 系统 Arial Unicode MS Arial Unicode Mac 系统 PingFang SC 苹方 - 简 Mac/iOS 系统 WenQuanYi Micro Hei 文泉驿微米黑 Linux 系统 示例:如果想使用微软雅黑,可改为:
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']2.
plt.rcParams['axes.unicode_minus'] = False
axes.unicode_minus:配置项的键,表示是否使用 Unicode 的负号字符。
False:关闭 Unicode 负号,改用 ASCII 的负号(-)。为什么需要这行?当设置了中文字体(如 SimHei)后,部分中文字体对 Unicode 负号的渲染支持不佳,会导致负号(比如图表中的 - 5、-10)显示为方块或异常。将该参数设为
False可以解决这个问题,让负号正常显示。三、使用注意事项
配置时机:这两行代码需要放在导入 matplotlib 之后、绘制图表之前,否则配置不会生效。正确的代码顺序:
import matplotlib.pyplot as plt import pandas as pd # 若用到pandas读取数据 # 第一步:配置中文和负号显示 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 第二步:后续绘图操作(比如你的豆瓣Top50散点图) df = pd.read_csv("douban_top50_view_count.csv") plt.scatter(df['排名'], df['观看人数']) plt.xlabel('电影排名') # 中文标签正常显示 plt.ylabel('观看人数') plt.title('豆瓣Top50电影观看人数散点图') # 中文标题正常显示 plt.savefig("top50_view_count_scatter.png") plt.show()字体不存在的问题:如果指定的字体(如 SimHei)在当前系统中不存在,仍然会出现中文乱码。
- 解决方案:
- 查看系统中已有的中文字体(比如在 Windows 的 “控制面板→字体” 中查看,Mac 在 “字体册” 中查看)。
- 改用系统中存在的中文字体名称。
- 若系统无合适中文字体,可下载安装后再配置。
局部字体设置(替代方案):如果不想全局修改字体,也可以在单个图表的标签、标题中单独指定字体,示例:
import matplotlib.pyplot as plt # 不设置全局字体,而是在局部指定 plt.scatter([1,2,3], [4,5,6]) # xlabel的fontproperties参数指定字体为黑体 plt.xlabel('电影排名', fontproperties='SimHei', fontsize=12) plt.ylabel('观看人数', fontproperties='SimHei') plt.title('豆瓣Top50散点图', fontproperties='SimHei') plt.show()这种方式的缺点是需要为每个中文标签单独设置,不如全局配置方便。
四、总结
这两行代码是 Matplotlib 绘图中处理中文显示的标准操作,通过全局配置中文字体和负号渲染规则,确保图表中的中文和负号能正常显示。在你的豆瓣爬虫项目中,绘制散点图时加入这两行配置,就能让图表的中文标题、坐标轴标签正常显示,不会出现乱码或方块。
rcParams如何理解?
rc: 代表 “runtime configuration”(运行时配置)。
这个命名传统源自早期的 Unix 系统和一些古老软件(如
bash的.bashrc文件),其中rc通常被认为是 “run commands” 或 “run control” 的缩写。在 Matplotlib 的语境下,它引申为 “配置文件” 或 “可配置的设置”。
rc设置定义了 Matplotlib 在绘制图形时所有元素的默认样式和行为。Params: 是 “parameters”(参数)的缩写。
因此,
rcParams直译就是“运行时配置参数”。 你可以把它理解为一个存储了 Matplotlib 所有默认绘图设置的中心字典。rcParams (Runtime Configuration Parameters)是 Matplotlib 的“控制中心”或“默认设置仓库”。通过修改它,你可以全局性地定制图表的外观和风格,无需在每次调用绘图函数时重复设置相同的参数(如颜色、线宽、字体等)。这是实现 Matplotlib 图表风格统一和批量定制的最核心工具。
简要介绍Matplotlib。
Matplotlib(绘图库) 的强大之处在于其丰富的定制能力:
绘制多种图表:除了基本的线图(
plot),你还可以轻松绘制散点图(scatter)、柱状图(bar)、直方图(hist)、饼图(pie)等。创建子图:使用
plt.subplots()或plt.subplot()可以在一个画布上创建多个子图,用于对比展示不同数据。精细控制样式:你可以控制图表的几乎所有元素,如颜色、线宽、标记样式、字体、坐标轴范围、网格线等,以满足出版或展示的特定要求。这正是你之前了解的
rcParams字典的用武之地,它可以全局设置这些样式。
第三步、定义休眠函数
# ===================== 第三步:定义工具函数(类比C语言的自定义函数) =====================
def random_sleep():
"""
自定义函数:生成随机休眠时间,强化反爬伪装
作用:爬取每一页前随机等待一段时间,模拟真人浏览的停顿
"""
# 生成MIN_SLEEP到MAX_SLEEP之间的随机浮点数(比如12.5秒、17.8秒)
# random.uniform(a,b) 类比C的rand()%((b-a)*100)/100 + a(生成随机浮点数)
sleep_time = random.uniform(MIN_SLEEP, MAX_SLEEP)
# 打印休眠时间(方便调试,看当前等待了多久)
print(f" 随机间隔 {sleep_time:.1f} 秒...") # .1f表示保留1位小数
# 执行休眠(程序暂停sleep_time秒)
# time.sleep() 类比C的sleep(),但C的sleep单位是秒,Python也是
time.sleep(sleep_time)
第四步、定义爬取函数
def crawl_top50_movie():
"""
核心函数:爬取豆瓣Top50电影的名称和观看次数
新增失败重试机制,提升爬取稳定性(应对临时的403/网络错误)
返回值:pandas的DataFrame(类比C的二维数组,存储电影名称+观看次数)
"""
# 初始化空列表,用于存储每部电影的数据(类比C的结构体数组)
movie_data = []
# 打印提示信息,告诉用户开始爬取
print("开始爬取豆瓣Top50电影名称和观看次数...")
# 循环爬取指定页数(PAGE_NUM=2,所以page取0和1,对应第1、2页)
# range(PAGE_NUM) 生成0到PAGE_NUM-1的整数,类比C的for(int page=0;page<PAGE_NUM;page++)
for page in range(PAGE_NUM):
# 计算每页的start参数:第0页start=0,第1页start=25(豆瓣Top250每页25条)
start = page * 25
# 替换BASE_URL中的{}为start值,生成当前页的完整URL
# 比如page=0时,url = "https://movie.douban.com/top250?start=0&filter="
# 类比C的sprintf拼接字符串
url = BASE_URL.format(start)
# 调用随机休眠函数,爬取当前页前先等待
random_sleep()
# 单页重试机制:最多重试2次(应对临时的网络错误/403封禁)
retry_count = 0 # 初始化重试计数器(类比C的int retry_count=0)
max_retry = 2 # 最大重试次数(失败2次就放弃该页)
# 循环重试:只要重试次数没超过最大值,就继续尝试爬取
while retry_count <= max_retry:
try:
# ========== 核心:发送HTTP请求获取页面数据 ==========
# requests.get():向指定URL发送GET请求(类比C的socket发送GET请求)
# 参数说明:
# url:目标网址
# headers:请求头(伪装浏览器)
# timeout=60:超时时间60秒(60秒没响应就判定为超时)
response = requests.get(url, headers=HEADERS, timeout=60)
# 检查请求是否成功:如果返回403/500等错误,直接抛出异常
# 类比C的判断HTTP响应码是否为200
response.raise_for_status()
# 设置响应编码:优先用页面自动识别的编码,否则用utf-8(解决中文乱码)
# 类比C的字符编码转换,避免解析HTML时中文变成乱码
response.encoding = response.apparent_encoding or "utf-8"
# 用BeautifulSoup解析HTML:把response.text(HTML字符串)转成可操作的对象
# "html.parser"是解析器(内置),类比C的XML解析器
soup = BeautifulSoup(response.text, "html.parser")
# ========== 解析页面:提取电影列表 ==========
# 查找class="grid_view"的ol标签(豆瓣Top250的电影列表容器)
# soup.find():查找第一个匹配的标签,类比C的字符串查找strstr()
movie_list = soup.find("ol", class_="grid_view")
# 如果没找到电影列表(比如页面结构变了),打印提示并跳出循环
if not movie_list:
print(f"第{page + 1}页未找到数据,跳过") # page+1是因为page从0开始
break
# 遍历电影列表中的每一个li标签(每一个li对应一部电影)
# movie_list.find_all("li"):查找所有li子标签,类比C的循环查找所有子节点
for item in movie_list.find_all("li"):
# 提取电影名称:查找class="title"的span标签(豆瓣电影名称的标签)
title_tag = item.find("span", class_="title")
# 如果没找到名称标签,跳过当前电影(避免报错)
if not title_tag:
continue
# 获取标签的文本内容,并去除首尾空格(比如"\n 肖申克的救赎 " → "肖申克的救赎")
movie_name = title_tag.text.strip()
# 提取观看次数:查找包含“人评价”的span标签(豆瓣的观看/评价数标签)
# string=lambda x: x and "人评价" in str(x):匿名函数筛选包含“人评价”的标签
# 类比C的循环判断字符串是否包含指定子串
view_tag = item.find("span", string=lambda x: x and "人评价" in str(x))
# 处理观看次数:如果找到标签,就去掉“人评价”和逗号(比如“1,800,000人评价” → “1800000”)
# 否则赋值为"0"
view_count = view_tag.text.strip().replace("人评价", "").replace(",", "") if view_tag else "0"
# 转换为整数:如果是数字字符串就转int,否则赋值为0(避免报错)
# 类比C的atoi()函数,加了异常判断
view_count = int(view_count) if view_count.isdigit() else 0
# 将当前电影的名称和观看次数存入列表(类比C的结构体赋值)
movie_data.append({"电影名称": movie_name, "观看次数": view_count})
# 打印爬取进度(方便查看当前爬取了哪部电影)
print(f"已爬取:《{movie_name}》,观看次数:{view_count}")
# 本页爬取成功,跳出重试循环(不需要再重试了)
break
# 捕获所有异常(网络错误、解析错误等)
# 类比C的try-catch(C++)/信号处理,避免程序崩溃
except Exception as e:
# 重试计数器+1
retry_count += 1
# 如果重试次数超过最大值,打印失败信息并跳出循环
if retry_count > max_retry:
print(f"第{page + 1}页爬取失败(已达最大重试次数):{str(e)}")
break
# 否则打印重试提示和错误信息
print(f"第{page + 1}页爬取失败,{max_retry - retry_count}次重试机会... 错误信息:{str(e)}")
# 失败后延长间隔(20秒)再重试(避免频繁请求被封)
time.sleep(20)
# 继续下一次重试
continue
# ========== 保存数据 ==========
# 将movie_list转换为pandas的DataFrame(二维表格),类比C的二维数组转CSV文件
df = pd.DataFrame(movie_data)
# 保存为CSV文件:index=False不保存行号,encoding="utf-8"确保中文正常
# 类比C的fprintf写入文件,pandas封装了复杂的文件操作
df.to_csv(SAVE_CSV, index=False, encoding="utf-8")
# 打印爬取完成提示,显示爬取的电影数量
print(f"\n爬取完成!共{len(movie_data)}部电影,数据保存至 {SAVE_CSV}")
# 返回DataFrame,供后续绘图使用
return df
format()方法的核心作用
1. 基础用法:替换占位符
# 定义带占位符的基础URL BASE_URL = "https://movie.douban.com/top250?start={}&filter=" # 场景1:start=0,生成第一页URL start = 0 url = BASE_URL.format(start) print(url) # 输出:https://movie.douban.com/top250?start=0&filter= # 场景2:start=25,生成第二页URL start = 25 url = BASE_URL.format(start) print(url) # 输出:https://movie.douban.com/top250?start=25&filter=这里
format(start)会把start的数值(0/25/50 等)直接替换到BASE_URL中{}的位置,生成不同分页的 URL。2. 进阶用法:多个占位符
如果 URL 中有多个参数需要替换,
format()可以按顺序传入多个值,或通过索引 / 关键字指定替换位置:# 多个占位符(按顺序替换) BASE_URL = "https://movie.douban.com/top250?start={}&limit={}&filter=" url = BASE_URL.format(0, 25) # 第一个{}填0,第二个{}填25 print(url) # 输出:https://movie.douban.com/top250?start=0&limit=25&filter= # 按索引替换(可打乱顺序) BASE_URL = "https://movie.douban.com/top250?start={1}&limit={0}&filter=" url = BASE_URL.format(25, 0) # {0}填25,{1}填0 print(url) # 输出:https://movie.douban.com/top250?start=0&limit=25&filter= # 按关键字替换(更清晰,推荐多参数场景) BASE_URL = "https://movie.douban.com/top250?start={page_start}&limit={page_limit}&filter=" url = BASE_URL.format(page_start=0, page_limit=25) print(url) # 输出:https://movie.douban.com/top250?start=0&limit=25&filter=三、和其他字符串格式化方式的对比
在 Python 中,除了
format(),还有两种常见的字符串格式化方式,你可以根据场景选择:1. f-string(Python3.6+,最推荐)
语法更简洁,可读性更高,直接在字符串中嵌入变量(前面加
f):start = 0 url = f"https://movie.douban.com/top250?start={start}&filter=" print(url) # 效果和format()一致2. % 格式化(老旧方式,不推荐)
属于 Python 早期的格式化方式,语法较繁琐,易出错:
start = 0 url = "https://movie.douban.com/top250?start=%d&filter=" % start print(url) # 效果一致,但多参数时需要用元组,如%(0, 25)
response = requests.get(url, headers=HEADERS, timeout=60)
soup = BeautifulSoup(response.text, "html.parser"),解释这两句。
这两行代码是Python 爬虫从 “获取网页数据” 到 “解析网页内容” 的核心流程,前者通过
requests库发送 HTTP 请求获取网页源代码,后者通过BeautifulSoup库将杂乱的 HTML 字符串转换成可操作的结构化对象。下面逐行拆解含义、作用和关键细节:第一行:
response = requests.get(url, headers=HEADERS, timeout=60)这行代码的作用是向指定的 URL 发送 HTTP GET 请求,获取服务器返回的响应数据,是爬虫获取网页内容的第一步。
各部分拆解:
requests.get():requests库的核心方法,用于发送HTTP GET 请求(最常用的请求方式,对应 “获取数据” 的场景,比如浏览网页、查询信息)。除了get,requests还提供post(提交数据)、put(更新数据)、delete(删除数据)等方法,对应 REST API 的不同操作。url:必填参数,是要爬取的网页地址(比如豆瓣 Top250 的分页 URL:https://movie.douban.com/top250?start=0&filter=)。headers=HEADERS:可选参数,用于传入请求头(HTTP Headers),核心作用是伪装成浏览器,避免被网站识别为爬虫。
HEADERS通常是一个字典,至少包含User-Agent(标识客户端类型),也可补充Referer(请求来源)、Accept-Language(语言偏好)等字段,示例:HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/129.0.0.0 Safari/537.36", "Referer": "https://movie.douban.com/" # 模拟从豆瓣首页跳转 }- 如果不传入
headers,requests会使用默认的User-Agent(python-requests/xx.xx.xx),大部分网站会直接屏蔽这种请求。timeout=60:可选参数,设置请求的超时时间(单位:秒)。
- 含义:如果 60 秒内服务器没有返回响应,就抛出
requests.exceptions.Timeout异常,避免程序无限等待(比如网站卡顿、网络故障时)。- 建议:爬虫中必须设置超时时间,否则可能导致程序卡死。
response:接收requests.get()返回的Response对象,包含服务器返回的所有数据(状态码、网页内容、响应头等)。
常用属性:
response.json():解析 JSON 格式的响应(用于 API 接口数据)。response.content:原始的二进制数据(用于下载图片、视频、文件等)。response.text:经过编码后的网页HTML 字符串(这是后续解析的原材料)。response.status_code:HTTP 状态码(200 表示请求成功,404 表示页面不存在,500 表示服务器错误,403 表示被禁止访问)。关键注意事项:
在使用
response.text前,建议先判断请求是否成功,避免因状态码异常导致后续解析出错:response = requests.get(url, headers=HEADERS, timeout=60) if response.status_code == 200: # 继续解析 soup = BeautifulSoup(response.text, "html.parser") else: print(f"请求失败,状态码:{response.status_code}")第二行:
soup = BeautifulSoup(response.text, "html.parser")这行代码的作用是将
response.text中的 HTML 字符串转换成结构化的树形解析对象(通常称为soup对象),是爬虫从 “拿到 HTML 字符串” 到 “提取有效数据” 的关键桥梁。各部分拆解:
BeautifulSoup:Python 的第三方库(需安装:pip install beautifulsoup4),专门用于解析 HTML/XML 文档,能把杂乱的 HTML 代码(标签嵌套、未闭合、格式不规范)整理成清晰的树形结构。response.text:传入的解析原材料,即上一步获取的网页 HTML 字符串。
- 注意:
response.text的编码依赖于requests自动识别的编码,如果编码错误(比如中文乱码),需要先通过response.encoding = "utf-8"(或其他编码)手动设置,否则解析后的soup也会包含乱码。"html.parser":指定 BeautifulSoup 使用的解析器,这里是 Python内置的解析器(无需额外安装)。
- 其他常用解析器(各有优劣):
解析器 优点 缺点 安装方式 html.parser内置、无需额外安装 速度中等、容错性一般 Python 自带 lxml速度最快、容错性强(处理畸形 HTML) 需要额外安装 pip install lxmlhtml5lib兼容性最好(完全模拟浏览器解析) 速度最慢 pip install html5lib- 建议:新手用
html.parser,处理大型 / 畸形 HTML 时用lxml。soup:接收 BeautifulSoup 返回的解析对象,后续所有的数据提取操作(比如找电影名称、评分、简介)都基于这个对象。总结
这两行代码构成了爬虫的基础工作流:
- 请求阶段:
requests.get()发送请求,拿到 HTML 字符串(response.text);- 解析阶段:
BeautifulSoup将 HTML 字符串转换成可操作的soup对象,为后续数据提取做准备。
df = pd.DataFrame(movie_data)
df = df.head(50)
df.to_csv(SAVE_CSV, index=False, encoding="utf-8-sig") # utf-8-sig解决Excel打开中文乱码
print(f"\n爬取完成!共{len(df)}部电影,数据保存至 {SAVE_CSV}")
return df
这段代码是将爬虫提取的电影数据转换成 Excel/CSV 可读取的表格文件并保存的核心逻辑,同时包含数据截取、编码处理等实用优化,是爬虫数据持久化的典型操作。下面逐行拆解含义、作用和关键细节:
前置说明
代码中依赖两个关键前提:
movie_data:是一个列表,其中每个元素是字典(对应一部电影的各项数据,如{"rank": "1", "name": "肖申克的救赎", "score": "9.7"}),这是 Pandas 创建 DataFrame 的常用数据格式。SAVE_CSV:是一个字符串,代表 CSV 文件的保存路径(如"douban_top250.csv")。逐行代码解析
1.
df = pd.DataFrame(movie_data)作用:将爬虫提取的
movie_data数据转换成 Pandas 的DataFrame对象(二维表格结构,类似 Excel 的工作表)。
- Pandas DataFrame:是 Python 中处理表格数据的核心数据结构,行对应每部电影,列对应数据字段(如排名、名称、评分)。
- 数据格式要求:
movie_data必须是可迭代的序列(如列表),序列中的每个元素是字典 / 列表 / 元组:
- 若为字典(推荐):字典的键会成为 DataFrame 的列名,字典的值会成为对应列的行数据。示例
movie_data:movie_data = [ {"rank": "1", "name": "肖申克的救赎", "score": "9.7"}, {"rank": "2", "name": "霸王别姬", "score": "9.6"}, # ... 更多电影 ]- 转换后,
df的结构如下:
rank name score 1 肖申克的救赎 9.7 2 霸王别姬 9.6 2.
df = df.head(50)作用:截取 DataFrame 的前 50 行数据,丢弃后续行(防止爬取的数据过多,或测试时只保留少量数据)。
df.head(n):Pandas 的方法,返回 DataFrame 的前n行数据(默认n=5)。
- 对应还有
df.tail(n):返回后n行数据。- 使用场景:
- 爬虫测试阶段:只保留少量数据,快速验证数据格式和保存效果。
- 限制数据量:比如豆瓣 Top250 共 10 页(250 条),若只想保存前 50 条,用此方法。
- 注意:如果
movie_data的长度小于 50,这行代码不会报错,只会返回全部数据。3.
df.to_csv(SAVE_CSV, index=False, encoding="utf-8-sig")作用:将 DataFrame 数据保存为 CSV 文件(逗号分隔值文件,可直接用 Excel / 记事本打开),是数据持久化的核心步骤。
- 核心参数详解:
参数 含义与作用 SAVE_CSV必选参数,指定 CSV 文件的保存路径(如 "./data/douban.csv"),若路径不存在会报错(需提前创建文件夹)。index=False可选参数,默认 True。设置为False时,不保存 DataFrame 的行索引(行索引是 Pandas 自动生成的 0、1、2...),避免 CSV 文件中出现多余的列。encoding="utf-8-sig"可选参数,指定文件编码。 utf-8-sig是解决Excel 打开 CSV 文件时中文乱码的关键(普通utf-8编码在 Excel 中会显示乱码,utf-8-sig包含 BOM 头,Excel 能正确识别)。- 补充参数(可选):
sep="\t":指定分隔符为制表符(默认是逗号","),适合数据中包含逗号的场景。na_rep="无":将空值(NaN)替换为 “无”,避免 CSV 中出现空单元格。columns=["rank", "name"]:只保存指定的列,过滤其他列。4.
print(f"\n爬取完成!共{len(df)}部电影,数据保存至 {SAVE_CSV}")作用:打印提示信息,告知用户爬取完成,显示数据条数和文件保存路径,提升用户体验。
len(df):获取 DataFrame 的行数(即保存的电影数量)。5.
return df作用:将处理后的 DataFrame 对象返回,便于后续对数据进行二次处理(如数据分析、可视化)。
- 若这段代码是在函数中(爬虫通常封装为函数),返回
df后,调用函数时可接收数据并继续操作:# 调用函数并接收数据 movie_df = crawl_douban() # 后续分析:比如统计评分大于9.5的电影 high_score_movies = movie_df[movie_df["score"] > 9.5]关键问题与解决方案
- Excel 打开 CSV 中文乱码?
解决方案:使用encoding="utf-8-sig"(不要用utf-8或gbk,gbk只适用于简体中文,跨平台兼容性差)。- 保存后出现多余的索引列?
解决方案:设置index=False。- 保存路径不存在报错?
解决方案:提前用os库创建文件夹:
import os # 定义保存路径 SAVE_CSV = "./data/douban_top250.csv" # 创建文件夹(如果不存在) os.makedirs(os.path.dirname(SAVE_CSV), exist_ok=True) # 再保存 df.to_csv(SAVE_CSV, index=False, encoding="utf-8-sig")总结
这段代码的核心逻辑是:爬虫数据(列表 + 字典)→ Pandas DataFrame(表格化)→ 数据截取(前 50 条)→ CSV 文件保存(解决编码和索引问题)→ 提示信息与数据返回。这是 Python 爬虫中处理和保存结构化数据的标准流程,既保证了数据的可读性(CSV 文件),又便于后续的数据分析(Pandas 操作)。
第五步、数据可视化
def draw_view_scatter(df):
"""
自定义函数:根据爬取的数据绘制观看次数散点图
参数df:crawl_top50_movie返回的DataFrame(电影数据)
作用:把枯燥的数字转换成可视化图表,更直观看到观看次数分布
"""
# 打印提示信息
print("\n开始绘制散点图...")
# 准备绘图数据:
# x轴:电影的索引(0到49),类比C的数组下标
x = range(len(df))
# y轴:每部电影的观看次数,类比C的数组取值
y = df["观看次数"]
# 创建绘图窗口:figsize=(12,6)设置窗口大小(宽12英寸,高6英寸)
# 类比C的绘图窗口初始化
plt.figure(figsize=(12, 6))
# 绘制散点图:
# x/y是坐标,color="purple"设置颜色为紫色,s=60设置点的大小,alpha=0.7设置透明度
# plt.scatter() 类比C的绘图函数,封装了散点绘制逻辑
plt.scatter(x, y, color="purple", s=60, alpha=0.7)
# 图表标注:
plt.xlabel("电影(Top50排序)", fontsize=12) # x轴标签,字体大小12
plt.ylabel("观看次数(人)", fontsize=12) # y轴标签
plt.title("豆瓣Top50电影观看次数散点图", fontsize=14, fontweight="bold") # 标题,加粗
# 设置x轴刻度:用电影名称替换数字索引,rotation=45旋转45度,ha="right"右对齐(避免重叠)
plt.xticks(x, df["电影名称"], rotation=45, ha="right")
plt.grid(True, alpha=0.3) # 显示网格,透明度0.3(辅助看数据)
plt.tight_layout() # 自动调整布局(避免标签被截断)
# 保存图表:dpi=300设置分辨率(越高越清晰)
plt.savefig(SCATTER_PNG, dpi=300)
# 打印绘制完成提示
print(f"散点图绘制完成!保存至 {SCATTER_PNG}")
# ===================== 第四步:主函数(类比C语言的main()函数) =====================
def main():
"""
主函数:串联整个爬虫流程
逻辑:爬取数据 → 绘制图表 → 提示完成
"""
# 调用爬取函数,获取电影数据(DataFrame格式)
df = crawl_top50_movie()
# 调用绘图函数,传入爬取的数据绘制散点图
draw_view_scatter(df)
# 打印全流程完成提示
print("\n全流程完成!")
# ===================== 第五步:程序入口(类比C语言的main()入口) =====================
# 这是Python的固定写法:如果该脚本是直接运行(不是被导入),就执行main()函数
# 类比C的int main(){...},确保只有直接运行脚本时才执行爬取逻辑
if __name__ == "__main__":
main()
if __name__ == "__main__": main()的核心作用是:
- 指定程序入口:当脚本直接运行时,执行
main()函数,触发整个爬虫 / 可视化流程;- 支持模块复用:当脚本被导入时,不会自动执行逻辑,只暴露函数 / 类供其他代码使用。

更多推荐


所有评论(0)