BS4模块学习和爬虫批量图片下载案例

Beautiful Soup 4(BS4)核心使用笔记:HTML 解析与节点提取

Beautiful Soup 4(简称 BS4)是 Python 中用于解析 HTML/XML 文档的经典库,其核心作用是将复杂的 HTML 字符串转换为结构化的 “对象树”(DOM 树),方便开发者快速定位、提取节点数据(如文本、属性)。BS4 本身不具备解析能力,需依赖第三方解析器(如lxmlhtml5lib),本笔记围绕实操代码,从 “解析器选择”“基础流程”“节点选择”“查找方法”“CSS 选择器” 五大模块展开,补充原理、扩展用法与避坑指南。

一、前置知识:BS4 依赖的解析器对比

BS4 需搭配解析器才能工作,不同解析器的速度、容错性、安装要求不同,选择合适的解析器是高效使用 BS4 的第一步:

解析器 依赖安装(pip) 速度 容错性(处理不规范 HTML) 支持文档类型 推荐场景
lxml pip install lxml 最快 优秀(自动修复标签缺失) HTML/XML 绝大多数场景(推荐首选)
html5lib pip install html5lib 较慢 极强(模拟浏览器解析) HTML HTML 结构混乱(如多标签嵌套)
html.parser 无需安装(Python 自带) 中等 一般(仅修复简单问题) HTML 简单场景、不想装额外库

核心推荐:优先使用lxml解析器,兼顾速度与容错性,是工业界主流选择(本笔记所有代码均基于lxml)。

二、模块 1:BS4 基本使用流程(从 HTML 到对象树)

BS4 的核心工作流是 “加载 HTML→创建 BeautifulSoup 对象→格式化查看”,通过对象树可直接操作 HTML 节点。

1. 基础代码解析

from bs4 import BeautifulSoup

# 1. 定义待解析的HTML字符串(实际场景中可能从网页响应、本地文件读取)
html = """
<html>
  <head><title>示例页面</title></head>
  <body>
    <p class="intro">Hello, Beautiful Soup!</p>
    <a href="https://example.***">链接</a>
  </body>
</html>
"""

# 2. 创建BeautifulSoup对象(参数1:HTML字符串;参数2:解析器名称)
soup = BeautifulSoup(html, 'lxml')

# 3. 格式化输出HTML(prettify():按缩进整理标签,便于阅读)
print(soup.prettify())
输出结果(格式化后):
<html>
 <head>
  <title>
   示例页面
  </title>
 </head>
 <body>
  <p class="intro">
   Hello, Beautiful Soup!
  </p>
  <a href="https://example.***">
   链接
  </a>
 </body>
</html>

2. 核心概念:BeautifulSoup 对象与节点类型

创建的soup对象是整个 HTML 文档的 “根对象”,包含三种核心节点类型:

  • Tag 对象:对应 HTML 中的标签(如<title><p>),可通过soup.标签名直接访问;
  • NavigableString 对象:对应标签内的纯文本(如<title>内的 “示例页面”),通过Tag.string获取;
  • BeautifulSoup 对象:代表整个文档,可视为特殊的 “根 Tag”。

3. 扩展:从本地文件 / 网络响应加载 HTML

实际开发中,HTML 很少是硬编码的字符串,更多是从本地文件或网络请求获取:

# (1)从本地HTML文件加载(需指定编码,避免乱码)
with open('test.html', 'r', encoding='utf-8') as f:
    soup = BeautifulSoup(f.read(), 'lxml')

# (2)从网络响应加载(需先安装requests:pip install requests)
import requests
response = requests.get('https://example.***')  # 获取网页响应
response.encoding = 'utf-8'  # 设置编码(避免中文乱码)
soup = BeautifulSoup(response.text, 'lxml')  # 解析响应文本


示例:
import requests
from bs4 import BeautifulSoup

response = requests.get("https://www.baidu.***/cache/")
# 关键:用 apparent_encoding 自动推断编码(无需手动猜)  并不一定都是utf-8
response.encoding = response.apparent_encoding
soup_3 = BeautifulSoup(response.text, "lxml")
print(soup_3.title.text)  # 正确输出“百度一下,你就知道”
print(soup_3)

三、模块 2:直接节点选择(标签名访问,适合简单结构)

通过 “soup.标签名” 直接访问节点,适合 HTML 结构简单、标签唯一或仅需第一个匹配标签的场景。

1. 基础代码解析

from bs4 import BeautifulSoup

html = """
<html>
  <head><title>示例页面</title></head>
  <body>
    <p class="intro">Hello, Beautiful Soup!</p>
    <p class="intro">Hello, Python!</p>
    <a href="https://example.***1">百度</a>
    <a href="https://example.***2">京东</a>
  </body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')

# 1. 访问标签:默认返回【第一个匹配的标签】(Tag对象)
print("1. 访问第一个title标签:", soup.title)  
# 输出:<title>示例页面</title>(仅返回第一个title,因HTML中title通常唯一)

print("2. 访问第一个p标签:", soup.p)  
# 输出:<p class="intro">Hello, Beautiful Soup!</p>(返回第一个p,忽略第二个p)

print("3. 访问第一个a标签:", soup.a)  
# 输出:<a href="https://example.***1">百度</a>(返回第一个a)


# 2. 提取Tag对象的核心信息(名称、文本、属性)
# (1)获取标签名称(Tag.name)
print("\n4. title标签名称:", soup.title.name)  # 输出:title

# (2)获取标签文本(两种方式:string vs text)
print("5. title标签文本(string):", soup.title.string)  # 输出:示例页面(仅纯文本,无子标签时可用)
print("6. p标签文本(text):", soup.p.text)  # 输出:Hello, Beautiful Soup!(支持子标签文本合并)

# (3)获取标签属性(两种方式:[] vs get())
print("7. p标签的class属性([]):", soup.p['class'])  # 输出:['intro'](class是列表,因HTML允许多class)
print("8. a标签的href属性(get()):", soup.a.get('href'))  # 输出:https://example.***1(推荐,属性不存在时返回None)



扩展:
一、获取 “所有匹配的标签”:用 find_all('标签名')
find_all('标签名') 会返回所有匹配该标签的 Tag 对象列表(空列表表示无匹配),通过这个列表可以处理所有标签。
示例(基于你的 HTML 代码):
from bs4 import BeautifulSoup

html = """
<html>
  <head><title>示例页面</title></head>
  <body>
    <p class="intro">Hello, Beautiful Soup!</p>
    <p class="intro">Hello, Python!</p>  <!-- 第二个p标签 -->
    <a href="https://example.***1">百度</a>
    <a href="https://example.***2">京东</a>  <!-- 第二个a标签 -->
  </body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')

# 1. 获取所有p标签(返回列表)
all_p = soup.find_all('p')
print("所有p标签的列表:", all_p)
# 输出:
# [
#   <p class="intro">Hello, Beautiful Soup!</p>,
#   <p class="intro">Hello, Python!</p>
# ]

# 2. 获取所有a标签(返回列表)
all_a = soup.find_all('a')
print("\n所有a标签的列表:", all_a)
# 输出:
# [
#   <a href="https://example.***1">百度</a>,
#   <a href="https://example.***2">京东</a>
# ]
二、指定访问 “第 n 个标签”:通过列表索引(从 0 开始)
find_all() 返回的是列表,列表的索引从 0 开始(第 1 个元素索引为 0,第 2 个为 1,以此类推),通过 列表[n] 即可访问第 n+1 个标签。
示例:
# 1. 访问第2个p标签(索引为1,因为第1个p标签索引是0)
second_p = all_p[1]  # 等价于 soup.find_all('p')[1]
print("\n第2个p标签:", second_p)
# 输出:<p class="intro">Hello, Python!</p>

# 2. 访问第2个p标签的文本
print("第2个p标签的文本:", second_p.text)  # 输出:Hello, Python!


# 3. 访问第2个a标签(索引为1)
second_a = all_a[1]  # 等价于 soup.find_all('a')[1]
print("\n第2个a标签:", second_a)
# 输出:<a href="https://example.***2">京东</a>

# 4. 访问第2个a标签的href属性
print("第2个a标签的href:", second_a.get('href'))  # 输出:https://example.***2
三、遍历所有标签:用 for 循环处理
若要批量处理所有标签(如提取每个标签的文本或属性),直接遍历 find_all() 返回的列表即可。
示例:
# 遍历所有p标签,打印文本
print("\n所有p标签的文本:")
for p in all_p:
    print(p.text)
# 输出:
# Hello, Beautiful Soup!
# Hello, Python!


# 遍历所有a标签,打印链接文本和href
print("\n所有a标签的信息:")
for a in all_a:
    print(f"文本:{a.text},链接:{a.get('href')}")
# 输出:
# 文本:百度,链接:https://example.***1
# 文本:京东,链接:https://example.***2
四、关键注意事项
索引越界问题:若列表长度为n(即有n个标签),则有效索引范围是 0 ~ n-1。若访问 all_p[2] 但实际只有 2 个 p 标签(索引 0 和 1),会报错 IndexError。解决:先判断列表长度,再访问索引:


if len(all_p) >= 2:  # 确保至少有2个p标签
    print(all_p[1].text)
else:
    print("没有足够的p标签")
与直接访问标签的区别:
soup.p 等价于 soup.find('p'),仅返回第一个p 标签;
soup.find_all('p') 返回所有p 标签的列表,需通过索引访问指定位置。
灵活性:find_all() 支持结合属性筛选(如 soup.find_all('p', class_='intro')),先筛选出符合条件的所有标签,再通过索引访问指定的,例如:


# 筛选class为intro的所有p标签,再访问第2个
filtered_p = soup.find_all('p', class_='intro')
if len(filtered_p) >= 2:
    print("第2个class为intro的p标签:", filtered_p[1].text)  # 输出:Hello, Python!
总结
获取全部标签:用 soup.find_all('标签名'),得到列表;
访问第 n 个标签:用列表索引 soup.find_all('标签名')[n-1](n 从 1 开始);
批量处理:用 for 循环遍历列表。

2. 关键扩展与避坑指南

(1)string vs text 的核心区别
方法 适用场景 特点 示例(若<p><span>Hello</span> Soup</p>
Tag.string 标签内无嵌套子标签(纯文本) 仅返回纯文本,有子标签时返回None 返回None(因 p 内有 span 子标签)
Tag.text 标签内有 / 无嵌套子标签 合并所有子标签的文本,返回字符串 返回Hello Soup(合并 span 和 p 的文本)
结论:优先用Tag.text,兼容性更强;仅当确认标签内无嵌套时用Tag.string
(2)属性访问的避坑点
  • []访问属性时,若属性不存在(如soup.p['id']),会直接报错KeyError
  • Tag.get('属性名')访问时,属性不存在会返回None(推荐),也可指定默认值:

    python

    print(soup.p.get('id', '无id属性'))  # 输出:无id属性(id不存在时返回默认值)
    
  • class属性是特殊的:HTML 中class可多个值(如<p class="intro red">),BS4 会将其解析为列表(如['intro', 'red']),而非字符串。
(3)标签不存在时的处理

若访问的标签在 HTML 中不存在(如soup.h1),会返回None,此时若直接调用Tag.nameTag.text,会报错AttributeError。需先判断是否存在:

h1_tag = soup.h1
if h1_tag:  # 先判断标签是否存在
    print(h1_tag.text)
else:
    print("HTML中无h1标签")  # 输出:HTML中无h1标签

四、模块 3:查找节点(find()find_all(),适合复杂筛选)

当需要按 “标签名 + 属性 + 文本” 组合筛选节点,或需获取所有匹配节点时,用find()find_all()(BS4 最核心的查找方法)。

1. 方法定义与参数说明

方法 返回值 核心参数(常用)
find() 第一个匹配的 Tag 对象(无则 None) name:标签名(如'p');attrs:属性字典(如{'class': 'book'});text:文本内容
find_all() 所有匹配的 Tag 对象列表(无则空列表) find()多一个limit参数:限制返回数量(如limit=2仅返回前 2 个)

2. 基础代码解析(基于用户代码)

from bs4 import BeautifulSoup

html = """
<div>
  <p class="book">Python 编程</p>
  <p class="book">Java 编程</p>
  <p class="movie">星际穿越</p>
  <a href="link1.html">链接1</a>
  <a href="link2.html">链接2</a>
  <a href="link3.html">链接3</a>
</div>
"""
soup = BeautifulSoup(html, 'lxml')

# 1. 按标签名查找:find_all('标签名') → 返回所有该标签的列表
all_p = soup.find_all('p')
print("1. 所有p标签:", all_p)
# 输出:[<p class="book">Python 编程</p>, <p class="book">Java 编程</p>, <p class="movie">星际穿越</p>]

all_a = soup.find_all('a', limit=2)  # 限制返回前2个a标签
print("2. 前2个a标签:", all_a)
# 输出:[<a href="link1.html">链接1</a>, <a href="link2.html">链接2</a>]


# 2. 按“标签名+属性”查找:筛选特定属性的标签
# 方式1:用class_参数(因class是Python关键字,加下划线区分)
book_p = soup.find_all('p', class_='book')
print("\n3. class为book的p标签:", book_p)
# 输出:[<p class="book">Python 编程</p>, <p class="book">Java 编程</p>]

# 方式2:用attrs参数(传属性字典,支持多属性筛选)
book_p2 = soup.find_all('p', attrs={'class': 'book'})  # 与方式1等价
print("4. attrs筛选class为book的p标签:", book_p2)

# 多属性筛选:查找href为link1.html的a标签
link1_a = soup.find('a', attrs={'href': 'link1.html', 'text': '链接1'})
print("5. href=link1.html且文本=链接1的a标签:", link1_a)
# 输出:<a href="link1.html">链接1</a>


# 3. 按文本内容查找:string参数(支持精确匹配或模糊匹配),从前的text已被废弃,但是功能一样
# 精确匹配:文本完全等于“Python 编程”的p标签
python_p = soup.find('p', string='Python 编程')
print("\n6. 文本=Python 编程的p标签:", python_p)  # 输出:<p class="book">Python 编程</p>

# 模糊匹配:文本包含“编程”的p标签(需结合正则表达式)
import re
program_p = soup.find_all('p', string=re.***pile('编程'))
print("7. 文本包含编程的p标签:", program_p)
# 输出:[<p class="book">Python 编程</p>, <p class="book">Java 编程</p>]

3. 避坑指南:find()find_all()的核心差异

场景 find() find_all()
返回值类型 单个 Tag 对象(或 None) 列表(空列表或 Tag 对象列表)
无匹配结果时 返回 None,直接调用属性会报错 返回空列表,遍历不会报错
适用场景 仅需第一个匹配节点 需所有匹配节点或限制数量(limit)

示例:无匹配结果的处理

# find()无匹配:返回None
no_tag_find = soup.find('h1')
print(no_tag_find)  # 输出:None
# if no_tag_find.text:  # 直接调用会报错AttributeError

# find_all()无匹配:返回空列表
no_tag_findall = soup.find_all('h1')
print(no_tag_findall)  # 输出:[]
for tag in no_tag_findall:  # 遍历空列表不会报错
    print(tag.text)

五、模块 4:CSS 选择器(select(),灵活强大)

CSS 选择器是前端开发中定位元素的标准方式,BS4 通过soup.select()支持 CSS 选择器语法,适合复杂嵌套结构的节点筛选(灵活性远超find()系列)。

1. 常用 CSS 选择器语法回顾

选择器类型 语法示例 作用
标签选择器 'li' 选择所有<li>标签
类选择器 '.item' 选择所有class="item"的标签
ID 选择器 '#menu' 选择id="menu"的标签(ID 唯一)
父子选择器 'ul > li' 选择ul的直接子标签li(仅一代)
祖孙选择器 'div li' 选择div下所有后代li(任意代)
属性选择器 'a[href="login.html"]' 选择href="login.html"<a>标签

2. 基础代码解析

from bs4 import BeautifulSoup

html = """
<div class="container">
  <ul id="menu">
    <li class="item">首页</li>
    <li class="item">产品</li>
    <li class="item">关于我们</li>
  </ul>
  <ul id="menu2">
    <li class="item">首页</li>
    <li class="item">产品</li>
    <li class="item">关于我们</li>
  </ul>
  <ol id="menu3">
    <li class="item">首页</li>
    <li class="item">产品</li>
    <li class="item">关于我们</li>
  </ol>
  <a href="login.html" class="link item">登录</a>
</div>
"""
soup = BeautifulSoup(html, 'lxml')

# 1. 标签选择器:选择所有指定标签
all_li = soup.select('li')
print("1. 所有li标签(数量):", len(all_li))  # 输出:9(3个ul+1个ol共9个li)


# 2. 类选择器(.类名):选择所有class包含该类的标签
all_item = soup.select('.item')
print("\n2. 所有class=item的标签(数量):", len(all_item))  # 输出:10(9个li+1个a)


# 3. ID选择器(#ID名):选择id为指定值的标签(ID唯一)
menu1 = soup.select('#menu')
print("\n3. id=menu的标签:", menu1)  # 输出:[<ul id="menu">...</ul>](仅1个)


# 4. 父子选择器(>):仅选择直接子标签
ul_direct_li = soup.select('ul > li')
print("\n4. ul的直接子li标签(数量):", len(ul_direct_li))  # 输出:6(仅ul下的li,排除ol下的3个li)


# 5. 祖孙选择器(空格):选择所有后代标签
div_all_li = soup.select('div li')
print("\n5. div下所有后代li标签(数量):", len(div_all_li))  # 输出:9(ul+ol下的所有li)


# 6. 多条件组合选择:同时满足多个选择器
container_link = soup.select('.container > .link')
print("\n6. container的直接子link标签:", container_link)  # 输出:[<a href="login.html" class="link item">登录</a>]


# 7. 扩展:属性选择器(按属性筛选)
login_a = soup.select('a[href="login.html"]')
print("\n7. href=login.html的a标签:", login_a)  # 输出:[<a href="login.html" class="link item">登录</a>]

# 模糊属性匹配(包含指定字符)
link_a = soup.select('a[href*="link"]')  # href包含"link"的a标签(本例无,输出空列表)
print("8. href包含link的a标签:", link_a)  # 输出:[]

3. 扩展:select()结果的处理

soup.select()返回的是Tag 对象列表,处理方式与find_all()一致:

# 遍历select()结果,提取文本和属性
for li in soup.select('ul > li'):
    print("li文本:", li.text, "| li的class:", li.get('class'))
# 输出示例:
# li文本: 首页 | li的class: ['item']
# li文本: 产品 | li的class: ['item']

六、BS4 核心工作流总结

  1. 准备 HTML:从字符串、本地文件或网络响应获取 HTML;
  2. 创建解析对象soup = BeautifulSoup(html, 'lxml')(推荐lxml解析器);
  3. 选择节点
    • 简单结构:直接用soup.标签名(获取第一个);
    • 复杂筛选:用find()/find_all()(按标签 + 属性 + 文本);
    • 嵌套结构:用soup.select()(CSS 选择器,灵活首选);
  4. 提取数据
    • 文本:Tag.text(优先)或Tag.string(纯文本场景);
    • 属性:Tag.get('属性名')(推荐,避免 KeyError)。

爬虫批量图片下载案例

图片网址:https://www.tuiimg.***/fengjing/list_1.html

完整代码

import os.path
import re
import requests
from bs4 import BeautifulSoup

def download_pic(pic_name, pic_url, subdir_path):
    # 创建文件名称
    pic_file = os.path.join(subdir_path, f'{pic_name}.jpg')

    # 打开链接 寻找大图片的链接
    response = requests.get(pic_url)
    html = response.text
    soup = BeautifulSoup(html,'lxml')
    # 大图片的网络地址
    img_url = soup.select(f'img[alt="{pic_name}"]')[0]['src']

    response = requests.get(img_url)
    with open(pic_file, "wb") as file:
        file.write(response.content)

def save_pics_by_page(page, dir_path):
    print(f"正在下载{page}页....")
    # 创建目录的过程
    subdir_name = f"第{re.findall(r'list_(\d+)', page)[0]}页"
    subdir_path = os.path.join(dir_path,subdir_name)
    if not os.path.exists(subdir_path):
        os.mkdir(subdir_path)

    # 获取数据的过程
    url = f"https://www.tuiimg.***/fengjing/{page}"
    response = requests.get(url)
    html = response.text

    # 筛选需要的数据
    soup = BeautifulSoup(html, 'lxml')
    pic_urls = [a['href'] for a in soup.find_all('a', target="_parent")[::2]]
    pic_imags = soup.select('a > img')
    pic_imags.pop(0)
    pic_names = [img['alt'] for img in pic_imags]

    # 开启循环 下载每一页中的10个图片
    for pic_name, pic_url in zip(pic_names, pic_urls):
        download_pic(pic_name, pic_url, subdir_path)
        print(f"{pic_name}图片下载完成!")

if __name__ == "__main__":
    dir_path = "图片集合"
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)
    for i in range(1,11):
        save_pics_by_page(f"list_{i}.html", dir_path)

这段代码是一个定向爬虫脚本,核心功能是从「推图网(tuiimg.***)风景分类页」批量下载图片,支持多页面下载、按页面创建文件夹分类保存,整体逻辑清晰,分为 “单图下载” 和 “页面批量处理” 两个核心模块。以下从 “功能概述→库作用→函数解析→主程序→注意事项” 逐步拆解。

一、代码整体功能概述

  1. 目标网站:推图网风景分类页(https://www.tuiimg.***/fengjing/);
  2. 下载范围:第 1~10 页(list_1.html 到 list_10.html)的图片;
  3. 核心流程:① 创建根文件夹 “图片集合”;② 对每一页,创建单独的子文件夹(如 “第 1 页”“第 2 页”);③ 解析页面,提取每张图片的 “详情页链接” 和 “图片名称”;④ 进入图片详情页,找到大图 URL,下载并保存到对应子文件夹;
  4. 最终效果:所有图片按页面分类存放在 “图片集合” 中,每张图片以其名称命名(后缀.jpg)。

二、导入库及作用

代码开头导入 4 个库,分别对应 “路径处理、正则提取、网络请求、HTML 解析” 四大核心需求:

导入语句 库 / 模块作用
import os.path 处理文件路径(如拼接路径、判断文件夹是否存在),避免跨平台路径问题(Windows/Linux);
import re 用正则表达式提取页面编号(如从list_1.html中提取1,用于命名子文件夹);
import requests 发送 HTTP 请求,获取网页 HTML 源码和图片二进制数据;
from bs4 import BeautifulSoup 解析 HTML 源码,提取需要的标签(如图片链接、图片名称);

三、核心函数解析

代码包含两个自定义函数:download_pic()(单张图片下载)和 save_pics_by_page()(单页面图片批量处理),两者是 “调用与被调用” 的关系(save_pics_by_page 调用 download_pic 下载单图)。

1. 单图下载函数:download_pic(pic_name, pic_url, subdir_path)

(1)函数作用

根据 “图片名称”“图片详情页链接”“目标保存文件夹路径”,从详情页中提取大图 URL,下载并保存图片到指定文件夹。

(2)参数说明
参数名 类型 作用描述
pic_name 字符串 图片的名称(从列表页解析的img标签alt属性获取,用于图片文件命名);
pic_url 字符串 图片详情页的相对链接(如/fengjing/202405/xxx.html,需结合根域名访问);
subdir_path 字符串 图片要保存的子文件夹路径(如 “图片集合 / 第 1 页”);
(3)内部逻辑步骤(逐行解析)
def download_pic(pic_name, pic_url, subdir_path):
    # 步骤1:拼接图片保存路径(子文件夹路径 + 图片名 + .jpg后缀)
    # os.path.join:跨平台路径拼接(Windows用\,Linux用/,自动适配)
    pic_file = os.path.join(subdir_path, f'{pic_name}.jpg')

    # 步骤2:发送请求,获取图片详情页的HTML
    # pic_url是相对链接,需补全根域名(https://www.tuiimg.***)
    response = requests.get(f'https://www.tuiimg.***{pic_url}')
    html = response.text  # 提取详情页HTML文本
    soup = BeautifulSoup(html, 'lxml')  # 解析HTML

    # 步骤3:从详情页中提取“大图的URL”
    # 逻辑:通过img标签的alt属性匹配(alt值=图片名称),找到对应的img标签,再取src属性(大图URL)
    # [0]:取匹配到的第一个img标签(通常详情页只有一个大图)
    img_url = soup.select(f'img[alt="{pic_name}"]')[0]['src']

    # 步骤4:下载大图并保存到本地
    # 1. 发送请求获取大图二进制数据(图片是二进制文件,用response.content获取)
    response_img = requests.get(img_url)
    # 2. 以“二进制写入模式(wb)”打开文件,写入图片数据
    with open(pic_file, "wb") as file:
        file.write(response_img.content)

2. 页面批量处理函数:save_pics_by_page(page, dir_path)

(1)函数作用

处理 “单一页面” 的所有图片:创建该页面的子文件夹、解析页面提取所有图片的 “详情页链接” 和 “名称”、调用download_pic批量下载。

(2)参数说明
参数名 类型 作用描述
page 字符串 页面文件名(如list_1.html,对应第 1 页);
dir_path 字符串 根文件夹路径(即 “图片集合”,子文件夹会创建在这个路径下);
(3)内部逻辑步骤(逐行解析)
def save_pics_by_page(page, dir_path):
    # 步骤1:打印下载状态(方便查看进度)
    print(f"正在下载{page}页....")

    # 步骤2:创建当前页面的子文件夹(如“第1页”“第2页”)
    # ① 用正则提取页码:从page(如list_1.html)中提取数字1
    # re.findall(r'list_(\d+)', page):匹配“list_”后接的数字,返回列表(如['1'])
    page_num = re.findall(r'list_(\d+)', page)[0]
    # ② 拼接子文件夹名称(如“第1页”)和路径(如“图片集合/第1页”)
    subdir_name = f"第{page_num}页"
    subdir_path = os.path.join(dir_path, subdir_name)
    # ③ 若子文件夹不存在,则创建(避免重复创建报错)
    if not os.path.exists(subdir_path):
        os.mkdir(subdir_path)

    # 步骤3:发送请求,获取当前列表页的HTML
    # 拼接列表页完整URL(如https://www.tuiimg.***/fengjing/list_1.html)
    url = f"https://www.tuiimg.***/fengjing/{page}"
    response = requests.get(url)
    html = response.text  # 提取列表页HTML文本

    # 步骤4:解析列表页,提取“图片详情页链接”和“图片名称”
    soup = BeautifulSoup(html, 'lxml')

    # ① 提取图片详情页链接(pic_urls)
    # 逻辑:找到所有target="_parent"的a标签(页面中图片的详情页链接对应的a标签属性)
    # [::2]:切片,每隔1个取1个(因页面结构中a标签成对出现,需过滤无效链接)
    pic_urls = [a['href'] for a in soup.find_all('a', target="_parent")[::2]]

    # ② 提取图片名称(pic_names)
    # 逻辑:找到所有“a标签下的img标签”(列表页的缩略图img)
    pic_imags = soup.select('a > img')
    # pop(0):删除第一个img标签(通常是页面顶部的logo或无效缩略图,需过滤)
    pic_imags.pop(0)
    # 从img标签的alt属性中提取图片名称(alt属性通常存储图片描述/名称)
    pic_names = [img['alt'] for img in pic_imags]

    # 步骤5:循环下载当前页面的所有图片
    # zip(pic_names, pic_urls):将图片名称和对应详情页链接配对(一一对应)
    for pic_name, pic_url in zip(pic_names, pic_urls):
        download_pic(pic_name, pic_url, subdir_path)  # 调用单图下载函数
        print(f"{pic_name}图片下载完成!")  # 打印单图下载状态

四、主程序入口逻辑(if __name__ == "__main__":

这部分是代码的 “启动入口”,负责初始化根文件夹、循环处理第 1~10 页的图片:

if __name__ == "__main__":
    # 步骤1:定义根文件夹名称(所有页面的子文件夹都放在这里)
    dir_path = "图片集合"
    # 步骤2:若根文件夹不存在,则创建
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)
    # 步骤3:循环处理第1~10页(range(1,11)生成1到10的整数)
    for i in range(1, 11):
        # 拼接页面文件名(如i=1时,page为list_1.html),调用页面处理函数
        save_pics_by_page(f"list_{i}.html", dir_path)

五、关键技术点总结

  1. 相对链接补全:列表页解析的pic_url是相对链接(如/fengjing/xxx.html),需拼接根域名https://www.tuiimg.***才能访问;
  2. 路径处理:用os.path.join而非硬写路径(如subdir_path = dir_path + "\\" + subdir_name),避免跨平台报错;
  3. HTML 解析技巧
    • find_all('a', target="_parent")按属性筛选标签;
    • select('a > img')按 “父子关系” 筛选标签(a 标签下的 img 标签);
  4. 二进制文件保存:图片是二进制数据,需用response.content获取,以wb(二进制写入)模式保存,不能用textw模式。

六、注意事项与优化建议

1. 潜在问题(代码未处理的场景)
  • 反爬拦截:未设置请求头(如User-Agent),频繁请求可能被网站识别为爬虫,导致请求失败(返回 403 禁止访问);
  • 异常情况:无任何异常处理(如请求超时、链接不存在、图片名称含特殊字符导致保存失败),会直接报错中断程序;
  • 图片格式固定:强制用.jpg后缀命名,但实际图片可能是.png等格式,可能导致图片无法打开;
  • 页面结构依赖:解析逻辑(如[::2]pop(0))完全依赖当前页面 HTML 结构,若网站更新页面布局,代码会失效。
2. 优化建议(让代码更健壮)
(1)添加请求头,模拟浏览器访问

requests.get()中添加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"
}
# 所有requests.get()都添加headers:
response = requests.get(url, headers=headers)
response = requests.get(f'https://www.tuiimg.***{pic_url}', headers=headers)
response_img = requests.get(img_url, headers=headers)
(2)添加异常处理(try-except)

避免单个图片下载失败导致整个程序中断:

def download_pic(pic_name, pic_url, subdir_path):
    try:
        pic_file = os.path.join(subdir_path, f'{pic_name}.jpg')
        # 过滤图片名称中的特殊字符(避免保存失败)
        pic_file = pic_file.replace('/', '').replace('\\', '').replace(':', '')
        response = requests.get(f'https://www.tuiimg.***{pic_url}', headers=headers, timeout=10)
        soup = BeautifulSoup(response.text, 'lxml')
        img_url = soup.select(f'img[alt="{pic_name}"]')[0]['src']
        response_img = requests.get(img_url, headers=headers, timeout=10)
        with open(pic_file, "wb") as file:
            file.write(response_img.content)
        print(f"{pic_name}图片下载完成!")
    except Exception as e:
        print(f"{pic_name}图片下载失败,错误原因:{str(e)}")
(3)动态获取图片格式(而非固定.jpg)

img_url中提取后缀,动态命名:

# 替换download_pic中的pic_file拼接逻辑
import os
# 从img_url中提取文件后缀(如.jpg、.png)
img_ext = os.path.splitext(img_url)[1]  # os.path.splitext("xxx.jpg")返回("xxx", ".jpg")
# 若后缀为空或过长,默认用.jpg
if not img_ext or len(img_ext) > 5:
    img_ext = ".jpg"
pic_file = os.path.join(subdir_path, f'{pic_name}{img_ext}')

七、代码执行流程总结

  1. 启动程序,创建根文件夹 “图片集合”;
  2. 循环 1~10,对每一页执行:① 提取页码,创建 “第 X 页” 子文件夹;② 访问该页列表页,解析 HTML;③ 提取所有图片的详情页链接和名称;④ 对每张图片,访问详情页提取大图 URL,下载保存到子文件夹;
  3. 所有页面处理完成,图片按页面分类保存在 “图片集合” 中。
转载请说明出处内容投诉
CSS教程网 » BS4模块学习和爬虫批量图片下载案例

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买