Beautiful Soup 4(BS4)核心使用笔记:HTML 解析与节点提取
Beautiful Soup 4(简称 BS4)是 Python 中用于解析 HTML/XML 文档的经典库,其核心作用是将复杂的 HTML 字符串转换为结构化的 “对象树”(DOM 树),方便开发者快速定位、提取节点数据(如文本、属性)。BS4 本身不具备解析能力,需依赖第三方解析器(如lxml、html5lib),本笔记围绕实操代码,从 “解析器选择”“基础流程”“节点选择”“查找方法”“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.name或Tag.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 核心工作流总结
- 准备 HTML:从字符串、本地文件或网络响应获取 HTML;
-
创建解析对象:
soup = BeautifulSoup(html, 'lxml')(推荐lxml解析器); -
选择节点:
- 简单结构:直接用
soup.标签名(获取第一个); - 复杂筛选:用
find()/find_all()(按标签 + 属性 + 文本); - 嵌套结构:用
soup.select()(CSS 选择器,灵活首选);
- 简单结构:直接用
-
提取数据:
- 文本:
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.***)风景分类页」批量下载图片,支持多页面下载、按页面创建文件夹分类保存,整体逻辑清晰,分为 “单图下载” 和 “页面批量处理” 两个核心模块。以下从 “功能概述→库作用→函数解析→主程序→注意事项” 逐步拆解。
一、代码整体功能概述
-
目标网站:推图网风景分类页(
https://www.tuiimg.***/fengjing/); -
下载范围:第 1~10 页(
list_1.html到list_10.html)的图片; - 核心流程:① 创建根文件夹 “图片集合”;② 对每一页,创建单独的子文件夹(如 “第 1 页”“第 2 页”);③ 解析页面,提取每张图片的 “详情页链接” 和 “图片名称”;④ 进入图片详情页,找到大图 URL,下载并保存到对应子文件夹;
-
最终效果:所有图片按页面分类存放在 “图片集合” 中,每张图片以其名称命名(后缀
.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)
五、关键技术点总结
-
相对链接补全:列表页解析的
pic_url是相对链接(如/fengjing/xxx.html),需拼接根域名https://www.tuiimg.***才能访问; -
路径处理:用
os.path.join而非硬写路径(如subdir_path = dir_path + "\\" + subdir_name),避免跨平台报错; -
HTML 解析技巧:
- 用
find_all('a', target="_parent")按属性筛选标签; - 用
select('a > img')按 “父子关系” 筛选标签(a 标签下的 img 标签);
- 用
-
二进制文件保存:图片是二进制数据,需用
response.content获取,以wb(二进制写入)模式保存,不能用text和w模式。
六、注意事项与优化建议
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~10,对每一页执行:① 提取页码,创建 “第 X 页” 子文件夹;② 访问该页列表页,解析 HTML;③ 提取所有图片的详情页链接和名称;④ 对每张图片,访问详情页提取大图 URL,下载保存到子文件夹;
- 所有页面处理完成,图片按页面分类保存在 “图片集合” 中。