第一步、导入依赖库

# ===================== 第一步:导入依赖库(类比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)是软件开发中一个非常重要的概念。你可以把它理解为:在构建自己的“主项目”时,所需要“借用”或“依赖”的、由他人已经编写好的、可复用的代码集合。

简单来说,就是你不需要从零开始造轮子,而是使用别人已经造好的、高质量的轮子、发动机或方向盘,来组装自己的汽车。

依赖库的关键特点:

  1. 可复用性:一个库可以被无数个项目使用,避免了重复劳动。

  2. 特定功能:每个库通常专注于解决一个特定领域的问题。

    • 网络请求axiosrequests (Python)

    • 日期处理moment.jsdate-fns

    • 用户界面组件ReactVue.js (它们本身也是库/框架)

    • 数据可视化D3.jsECharts

    • 数学计算NumPy (Python)

  3. 包管理:现代语言有专门的工具来管理这些库。

    • JavaScript/Node.jsnpmyarnpnpm

    • Pythonpipconda

    • JavaMavenGradle

    • PHPComposer

  4. 依赖声明文件:项目里会有一个文件(如 package.jsonrequirements.txtpom.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 是一个功能丰富的模块(包含 BeautifulSoupTagNavigableString 等多个类 / 函数),而你只需要用到核心的 BeautifulSoup 类来解析 HTML,用这种方式可以:

  1. 减少命名空间冗余(只加载需要的对象);
  2. 简化代码(无需每次写 bs4. 前缀);
  3. 提高代码可读性。

总结

用法 语法示例 使用对象时的写法 适用场景
导入整个模块 import bs4 bs4.BeautifulSoup 需要使用模块多个对象时
导入指定对象 from bs4 import BeautifulSoup BeautifulSoup 仅需模块中少数特定对象时(推荐)
导入所有对象 from bs4 import * BeautifulSoup 临时测试(不推荐正式代码)

matplotlib.pyplot 解析。

1. 先理清模块结构:matplotlib vs pyplot

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(超文本传输协议)是一种用于在网络上传输数据的应用层协议,它是互联网上数据通信的基础。简单来说,它定义了客户端(如浏览器)和服务器之间如何交换信息。

  1. 状态码

    服务器用状态码表示请求结果:
    • 500:服务器内部错误。

    • 404:资源未找到。

    • 200:成功。

  2. URL定位资源

    • 通过统一资源定位符(URL)指定资源地(如 https://example.com/page)。

工作流程示例:

  1. 浏览器输入 https://www.example.com

  2. 浏览器发送HTTP请求到Example的服务器。

  3. 服务器处理请求,返回HTML、CSS等文件。

  4. 浏览器解析文件,渲染成网页。

第二步、全局配置

# ===================== 第二步:全局配置(类比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_URLPAGE_NUMMIN_SLEEP等)属于Python 中的常量(约定俗成的),也可以称为配置项 / 参数项。下面详细拆解:

一、为什么用大写?—— 编程的命名规范(约定俗成,本质是变量

在 Python 中,并没有语法层面的 “常量”(不像 C/C++ 有const关键字,定义后无法修改),但开发者们形成了一个通用的命名约定

全部字母大写,单词之间用下划线分隔(蛇形命名法),表示这个变量是 **“常量”,即不建议在代码中修改其值 ** 的配置项 / 固定参数。

这么做的目的是:

  1. 视觉区分:大写的标识符在代码中非常醒目,一眼就能看出是 “配置参数”,而非业务逻辑中的临时变量(如pagedataheaders等小写 / 驼峰命名的变量)。
  2. 语义明确:告诉其他开发者(或未来的自己):“这个值是程序的配置项,是核心参数,不要随意修改”。
  3. 统一维护:把所有关键配置集中定义在代码开头,后续要调整时(比如想爬 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_URLPAGE_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 的变量,只是遵循 “全大写命名 = 常量(不建议修改)” 的约定。它们的定义规则和普通变量一模一样:

  1. 必须赋值才能使用:变量名 = 值(如MAX_SLEEP = 18)。
  2. 定义位置:通常放在代码开头(便于维护),也可以放在函数外(全局变量)或函数内(局部变量)。
  3. 如果未定义直接用,必然报错
    # 未定义的情况
    print(UNDEFINED_CONST)  # 抛出NameError: name 'UNDEFINED_CONST' is not defined
    

三、补充:Python 中 “真正不用定义就能用” 的情况

只有以下几种场景,标识符不用自己定义就能用,和这些大写配置项完全不同:

  1. Python 内置关键字:如iffordefimport等,是 Python 语法的一部分,天然存在(但不能作为变量名)。
  2. Python 内置函数 / 对象:如print()len()range()list()等,是 Python 自带的工具,可直接调用。
  3. 导入的模块 / 对象:如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可以解决这个问题,让负号正常显示。

三、使用注意事项

  1. 配置时机这两行代码需要放在导入 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()
    
  2. 字体不存在的问题:如果指定的字体(如 SimHei)在当前系统中不存在,仍然会出现中文乱码。

    • 解决方案:
      1. 查看系统中已有的中文字体(比如在 Windows 的 “控制面板→字体” 中查看,Mac 在 “字体册” 中查看)。
      2. 改用系统中存在的中文字体名称。
      3. 若系统无合适中文字体,可下载安装后再配置。
  3. 局部字体设置(替代方案):如果不想全局修改字体,也可以在单个图表的标签、标题中单独指定字体,示例:

    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如何理解?

  1. rc: 代表 “runtime configuration”(运行时配置)。

    • 这个命名传统源自早期的 Unix 系统和一些古老软件(如 bash 的 .bashrc 文件),其中 rc 通常被认为是 “run commands” 或 “run control” 的缩写。在 Matplotlib 的语境下,它引申为 “配置文件” 或 “可配置的设置”

    • rc 设置定义了 Matplotlib 在绘制图形时所有元素的默认样式和行为

  2. 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 请求,获取服务器返回的响应数据是爬虫获取网页内容的第一步。

各部分拆解:
  1. requests.get()requests库的核心方法,用于发送HTTP GET 请求(最常用的请求方式,对应 “获取数据” 的场景,比如浏览网页、查询信息)。除了getrequests还提供post(提交数据)、put(更新数据)、delete(删除数据)等方法,对应 REST API 的不同操作。
  2. url:必填参数,是要爬取的网页地址(比如豆瓣 Top250 的分页 URL:https://movie.douban.com/top250?start=0&filter=)。
  3. 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/"  # 模拟从豆瓣首页跳转
      }
      
    • 如果不传入headersrequests会使用默认的User-Agentpython-requests/xx.xx.xx),大部分网站会直接屏蔽这种请求。
  4. timeout=60:可选参数,设置请求的超时时间(单位:秒)。
    • 含义:如果 60 秒内服务器没有返回响应,就抛出requests.exceptions.Timeout异常,避免程序无限等待(比如网站卡顿、网络故障时)。
    • 建议:爬虫中必须设置超时时间,否则可能导致程序卡死
  5. 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 字符串” 到 “提取有效数据” 的关键桥梁。

各部分拆解:
  1. BeautifulSoup:Python 的第三方库(需安装:pip install beautifulsoup4),专门用于解析 HTML/XML 文档,能把杂乱的 HTML 代码(标签嵌套、未闭合、格式不规范)整理成清晰的树形结构。
  2. response.text:传入的解析原材料,即上一步获取的网页 HTML 字符串。
    • 注意:response.text的编码依赖于requests自动识别的编码,如果编码错误(比如中文乱码),需要先通过response.encoding = "utf-8"(或其他编码)手动设置,否则解析后的soup也会包含乱码。
  3. "html.parser":指定 BeautifulSoup 使用的解析器,这里是 Python内置的解析器(无需额外安装)。
    • 其他常用解析器(各有优劣):
      解析器 优点 缺点 安装方式
      html.parser 内置、无需额外安装 速度中等、容错性一般 Python 自带
      lxml 速度最快、容错性强(处理畸形 HTML) 需要额外安装 pip install lxml
      html5lib 兼容性最好(完全模拟浏览器解析) 速度最慢 pip install html5lib
    • 建议:新手用html.parser,处理大型 / 畸形 HTML 时用lxml
  4. soup:接收 BeautifulSoup 返回的解析对象,后续所有的数据提取操作(比如找电影名称、评分、简介)都基于这个对象。

总结

这两行代码构成了爬虫的基础工作流

  1. 请求阶段requests.get()发送请求,拿到 HTML 字符串(response.text);
  2. 解析阶段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 可读取的表格文件并保存的核心逻辑,同时包含数据截取、编码处理等实用优化,是爬虫数据持久化的典型操作。下面逐行拆解含义、作用和关键细节:

前置说明

代码中依赖两个关键前提:

  1. movie_data:是一个列表,其中每个元素是字典(对应一部电影的各项数据,如{"rank": "1", "name": "肖申克的救赎", "score": "9.7"}),这是 Pandas 创建 DataFrame 的常用数据格式。
  2. 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]
    

关键问题与解决方案

  1. Excel 打开 CSV 中文乱码?
    解决方案:使用encoding="utf-8-sig"(不要用utf-8gbkgbk只适用于简体中文,跨平台兼容性差)。
  2. 保存后出现多余的索引列?
    解决方案:设置index=False
  3. 保存路径不存在报错?
    解决方案:提前用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()的核心作用是:

  1. 指定程序入口:当脚本直接运行时,执行main()函数,触发整个爬虫 / 可视化流程;
  2. 支持模块复用:当脚本被导入时,不会自动执行逻辑,只暴露函数 / 类供其他代码使用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐