Python图片采集脚本实战:从HTML解析到批量下载
之前在做动漫角色图片素材整理的时候想给一个小型图像分类模型准备训练集手动去图站一张张保存图片实在太低效而且容易漏图、重复命名。于是花了一个周末写了一套 Python 图片采集脚本把「搜索关键词 → 解析列表页 → 批量下载图片 → 本地分类存储」整条链路串了起来。这篇文章就把这套过程完整拆解出来从环境准备、核心原理到完整代码再到常见的报错排查和工程化建议新手可以照着搭有基础的可以直接抄作业改造成自己的素材采集工具。1. 项目背景与核心概念1.1 这个项目要解决什么问题「捕获一只纱雾酱」听起来像是一个娱乐向的项目名本质上做的事情是根据指定关键词或标签从公开网页中批量抓取某一类图片资源并按照规则保存到本地。这类需求在实际开发里非常常见准备图像分类、目标检测数据集的训练样本。搭建个人图片素材库按角色、画师、风格分类归档。做二次元图片站点的数据统计与分析。快速采集本地占位图、测试图用于前端开发或算法联调。如果手工操作几百张图片就要花费小半天而且容易出现文件名混乱、重复下载、漏图等问题。写一个脚本自动化既能保证图片目录结构统一又能重复执行后续想扩充数据量只需要改一下关键词或页码参数。1.2 涉及的核心技术点从技术角度看这个项目串联了以下几个关键环节环节技术点作用页面请求requests获取目标网页 HTML内容解析BeautifulSoup/ 正则从 HTML 中提取图片链接图片下载requests 二进制流把图片保存到本地文件管理os/hashlib创建目录、重命名、去重流程控制time/ 异常处理控制请求频率、处理失败任务数据管理SQLite / JSON记录已下载图片支持增量采集这里的难点并不是某个单一环节而是如何把各个环节稳定地串联起来尤其是在目标站点结构变化、请求频率限制、图片链接失效时如何让脚本具备一定的容错能力。1.3 学完你能掌握什么读完本文并动手实践后你将掌握使用requests模拟浏览器发起 HTTP 请求并处理响应状态。使用BeautifulSoup解析 HTML提取图片地址和分页信息。批量下载图片的完整流程包括目录管理、重命名和格式校验。常见爬取报错的排查思路例如 403、超时、空结果。一个可扩展的工程化结构后续可以加入多线程、断点续传、数据库去重。需要提前说明的是本文重点在“学习网页请求与数据提取的技术思路”并不是鼓励对任何站点进行大规模抓取。实际使用时请务必遵守目标网站的robots.txt规则、服务条款和版权要求只采集自己有权限使用的内容。2. 环境准备与依赖安装2.1 开发环境说明本文示例以常见的 Windows / macOS / Linux 环境为例核心依赖是 Python 第三方库不涉及特定系统功能。开发调试我使用 VS Code命令行工具使用终端即可。版本方面Python 建议使用 3.8 及以上版本requests、beautifulsoup4等库保持当前可用版本即可。如果你本地的 Python 版本较低部分语法比如f-string的增强写法可能需要调整。先检查 Python 环境python --version输出示例Python 3.10.122.2 安装第三方库本项目需要安装以下库requests负责 HTTP 请求和图片下载。beautifulsoup4解析 HTML 文档。lxmlBeautifulSoup 的解析器解析速度快推荐安装。Pillow用于校验图片文件是否完整避免下载到损坏文件。安装命令pip install requests beautifulsoup4 lxml Pillow如果网络较慢可以指定国内镜像源pip install requests beautifulsoup4 lxml Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 项目目录结构为了后续扩展和维护我建议把脚本按功能模块拆分而不是把几百行代码写在一个文件里。本文的最终目录结构如下capture_sagiri/ ├── main.py # 程序入口串联整个采集流程 ├── config.py # 配置项如请求头、目标地址、保存目录 ├── fetcher.py # 页面请求模块 ├── parser.py # HTML 解析模块 ├── downloader.py # 图片下载模块 ├── utils.py # 文件工具函数 ├── requirements.txt # 依赖清单 └── images/ # 图片保存目录运行后自动生成 └── sagiri/这种拆分方式的好处是职责清晰以后更换目标站点只需要改parser.py中的解析规则想加入数据库去重只需要在utils.py中增加一个类。3. 核心原理拆解3.1 HTTP 请求与响应爬虫的本质是模拟浏览器向服务器发起 HTTP 请求然后从响应内容中提取我们需要的数据。一个最简单的 GET 请求代码如下import requests 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 } resp requests.get(https://example.com, headersheaders, timeout10) print(resp.status_code) print(resp.text[:200])这里有两个关键点User-Agent很多服务器会识别请求来源如果没有 User-Agent容易直接被拒绝访问返回 403。timeout必须设置超时时间否则程序可能卡在某个请求上导致整个采集流程停滞。在批量采集时还要注意请求频率。频繁发起请求会给服务器带来压力也容易触发反爬机制。建议每次请求之间加入随机延迟。3.2 用 BeautifulSoup 解析 HTML拿到 HTML 文本后需要从中提取图片链接。BeautifulSoup 提供了非常友好的 DOM 查询接口。以一段典型的图片列表 HTML 为例div classimage-list div classitem a href/detail/1001.html img srchttps://example.com/images/sagiri_01.jpg alt纱雾_01 /a /div div classitem a href/detail/1002.html img srchttps://example.com/images/sagiri_02.jpg alt纱雾_02 /a /div /div解析代码from bs4 import BeautifulSoup soup BeautifulSoup(html_text, lxml) items soup.select(.image-list .item img) for img in items: src img.get(src) alt img.get(alt, unknown) print(alt, src)输出示例纱雾_01 https://example.com/images/sagiri_01.jpg 纱雾_02 https://example.com/images/sagiri_02.jpgselect方法接收 CSS 选择器和前端开发的选择器语法一致上手成本很低。get(src)是获取标签属性值的标准方式需要注意部分站点的图片懒加载机制真实地址可能不是在src属性而是在>import requests def download_image(url, save_path): resp requests.get(url, timeout15) if resp.status_code 200: with open(save_path, wb) as f: f.write(resp.content) return True return False这里有几个细节需要注意图片请求一般需要带上Referer头很多图站会对来源地址做校验没有 Referer 会返回 403。建议使用文件流方式下载大图resp.iter_content而不是一次性resp.content。保存前要确保目录存在否则open会报FileNotFoundError。3.4 请求频率控制与错误重试批量采集时稳定性比速度更重要。建议在每次请求后加入随机延迟避免看起来像机器人行为import time import random time.sleep(random.uniform(1, 3))同时对网络请求失败的情况要做重试。常见做法是设置最大重试次数超过次数后跳过该图片并记录日志。4. 完整实战编写图片采集脚本下面我们开始编写完整的项目代码。为了便于理解我以「按关键词搜索并批量采集某一角色图片」为例假设目标站点是一个公开的图片展示页列表页 URL 符合分页规律。4.1 创建项目结构首先创建项目目录mkdir capture_sagiri cd capture_sagiri然后创建虚拟环境推荐python -m venv venv激活虚拟环境Windowsvenv\Scripts\activatemacOS / Linuxsource venv/bin/activate创建requirements.txtrequests2.31.0 beautifulsoup44.12.2 lxml4.9.3 Pillow10.1.0如果你使用的是最新版本可以不锁定版本号直接写入库名。4.2 编写配置文件文件路径config.py# config.py # 目标站点基础配置请根据实际情况修改 BASE_URL https://example.com/search # 搜索页地址 KEYWORD sagiri # 搜索关键词 PAGES 5 # 要抓取的页数 SAVE_DIR images/sagiri # 本地保存目录 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://example.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, } REQUEST_DELAY (1, 3) # 请求延迟范围秒 MAX_RETRY 3 # 下载失败最大重试次数 TIMEOUT 15 # 请求超时时间配置项单独放到config.py里后续调整搜索关键词、页数和保存路径时不需要改动核心代码。4.3 编写页面请求模块文件路径fetcher.py# fetcher.py import requests import time import random from config import HEADERS, REQUEST_DELAY, TIMEOUT, MAX_RETRY def fetch_html(url, paramsNone): 请求页面并返回 HTML 文本。 失败时自动重试最多重试 MAX_RETRY 次。 for attempt in range(1, MAX_RETRY 1): try: resp requests.get( url, paramsparams, headersHEADERS, timeoutTIMEOUT ) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f[请求失败] {url}第 {attempt} 次重试错误{e}) time.sleep(2) print(f[请求失败] {url}已超过最大重试次数) return None def polite_delay(): 在两次请求之间添加随机延迟降低对目标站点的压力。 min_sec, max_sec REQUEST_DELAY time.sleep(random.uniform(min_sec, max_sec))这里使用resp.apparent_encoding自动识别页面编码可以避免一部分中文站点页面乱码问题。4.4 编写解析模块文件路径parser.py# parser.py from bs4 import BeautifulSoup from urllib.parse import urljoin from config import BASE_URL, KEYWORD def parse_image_urls(html_text, page_url): 解析列表页 HTML提取图片地址。 注意这里的选择器需要根据目标站点的实际结构调整。 soup BeautifulSoup(html_text, lxml) # 示例选择器适用于带 classimage-list item 结构的列表页 image_items soup.select(.image-list .item img) image_urls [] for img in image_items: # 部分站点使用># downloader.py import os import requests import hashlib from config import HEADERS, TIMEOUT, MAX_RETRY def download_image(url, save_path): 下载单张图片到指定路径。 返回 True 表示下载成功False 表示失败。 for attempt in range(1, MAX_RETRY 1): try: resp requests.get(url, headersHEADERS, timeoutTIMEOUT, streamTrue) if resp.status_code ! 200: print(f[状态异常] {resp.status_code} - {url}) continue # 使用流式写入避免大图占用过多内存 with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) return True except requests.RequestException as e: print(f[下载异常] {url}第 {attempt} 次重试错误{e}) return False def make_save_dir(dir_path): 确保保存目录存在。 os.makedirs(dir_path, exist_okTrue) def md5_hash(text): 计算字符串 MD5用于生成不重复的文件名。 return hashlib.md5(text.encode(utf-8)).hexdigest()这里用图片 URL 的 MD5 值作为文件名的一部分可以有效避免重复下载同一张图片。4.6 编写工具函数模块文件路径utils.py# utils.py import os from PIL import Image def verify_image(file_path): 校验图片文件是否完整可用。 使用 Pillow 打开图片如果文件损坏则抛出异常。 try: with Image.open(file_path) as img: img.verify() return True except Exception: return False def get_image_extension(url): 从 URL 中猜测图片扩展名。 优先从路径中提取默认返回 .jpg。 path url.split(?)[0].lower() if path.endswith(.png): return .png if path.endswith(.gif): return .gif if path.endswith(.webp): return .webp return .jpg def clean_file_name(name): 清理文件名中的非法字符。 invalid_chars [/, \\, :, *, ?, , , , |] for ch in invalid_chars: name name.replace(ch, _) return name.strip() or untitled4.7 编写主流程文件路径main.py# main.py import os from config import SAVE_DIR, PAGES from fetcher import fetch_html, polite_delay from parser import parse_image_urls, build_search_url from downloader import download_image, make_save_dir, md5_hash from utils import get_image_extension, verify_image def main(): print(f开始采集图片关键词配置共 {PAGES} 页) make_save_dir(SAVE_DIR) downloaded_count 0 skipped_count 0 for page in range(1, PAGES 1): print(f\n正在处理第 {page} 页...) # 1. 构造当前页 URL page_url build_search_url(page) print(f请求地址{page_url}) # 2. 请求页面 html_text fetch_html(page_url) if html_text is None: print(页面请求失败跳过当前页) continue # 3. 解析图片地址 image_urls parse_image_urls(html_text, page_url) print(f本页解析到 {len(image_urls)} 张图片) # 4. 逐张下载 for url in image_urls: extension get_image_extension(url) file_name md5_hash(url) extension save_path os.path.join(SAVE_DIR, file_name) # 如果文件已存在且校验通过则跳过 if os.path.exists(save_path) and verify_image(save_path): skipped_count 1 continue success download_image(url, save_path) if success: download_count len(os.listdir(SAVE_DIR)) print(f[成功] 保存至 {save_path}) downloaded_count 1 else: print(f[失败] 无法下载 {url}) # 5. 页间延迟避免请求过快 polite_delay() print(f\n采集完成新增下载{downloaded_count}跳过重复{skipped_count}) print(f图片保存目录{os.path.abspath(SAVE_DIR)}) if __name__ __main__: main()4.8 运行与验证在项目根目录执行python main.py预期输出示例开始采集图片关键词配置共 5 页 正在处理第 1 页... 请求地址https://example.com/search?qsagiripage1 本页解析到 18 张图片 [成功] 保存至 images/sagiri/3f9c2b1a0e8d4c6f7a2b3c4d5e6f7a8b.jpg [成功] 保存至 images/sagiri/8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d.png ... 跳过重复3 采集完成新增下载87跳过重复12 图片保存目录/your/path/capture_sagiri/images/sagiri运行结束后检查目录ls images/sagiri | head -20可以看到图片已经按 MD5 命名存放在本地。可以用 Pillow 写一个快速校验脚本确认所有图片都能正常打开python -c from utils import verify_image; import os; dimages/sagiri; bad[f for f in os.listdir(d) if not verify_image(os.path.join(d,f))]; print(损坏图片:, bad)4.9 结果说明这套流程的核心设计思路是「分页请求 → 提取链接 → 逐个下载 → 去重保存」。如果目标站点改版你只需要调整parser.py中的 CSS 选择器和build_search_url中的 URL 拼接规则主流程基本不需要改动。另外MD5 去重的逻辑保证了同一张图不会重复下载。即使你再次运行脚本已经存在的图片会被跳过实现简单的增量采集效果。5. 常见问题与排查思路在实际运行中你大概率会遇到下面这些问题。我把常见现象、原因和解决思路整理成了一张表方便快速查阅。问题现象常见原因解决思路请求返回 403 ForbiddenUser-Agent 缺失或不被识别服务器有反爬策略伪装完整浏览器请求头补充 Referer降低请求频率页面能访问但解析到 0 张图片CSS 选择器不匹配图片地址在>import sqlite3 class DownloadDB: def __init__(self, db_pathdownloads.db): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS image_record ( url TEXT PRIMARY KEY, file_path TEXT, keyword TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) def exists(self, url): row self.conn.execute( SELECT 1 FROM image_record WHERE url ?, (url,) ).fetchone() return row is not None def insert(self, url, file_path, keyword): self.conn.execute( INSERT OR IGNORE INTO image_record (url, file_path, keyword) VALUES (?, ?, ?), (url, file_path, keyword), ) self.conn.commit()数据库方案比起单纯的文件判断优势在于支持多关键词、多批次采集任务。即使图片被手动删除数据库里依然保留着来源记录。方便统计采集数量、来源分布等指标。6.2 多线程与异步改造requests是同步库下载图片时 CPU 基本处于等待状态。如果图片数量很大可以考虑使用ThreadPoolExecutor把下载过程改成多线程from concurrent.futures import ThreadPoolExecutor, as_completed def download_batch(image_urls, save_dir): with ThreadPoolExecutor(max_workers4) as executor: future_map { executor.submit(download_one_with_save, url, save_dir): url for url in image_urls } for future in as_completed(future_map): url future_map[future] try: result future.result() print(f[完成] {url} - {result}) except Exception as e: print(f[异常] {url} - {e})注意并发数不宜设置过大推荐 4 到 8 个线程即可避免对目标服务器造成过大压力。如果追求更高性能可以使用aiohttpasyncio做异步采集但这会增加代码复杂度建议等同步版本稳定后再改造。6.3 断点续传与失败重试批量采集最怕中途中断。为了保证任何情况下都能从断点继续需要做到两点每次下载成功后立即记录到数据库。重启脚本时自动跳过数据库中已存在的记录。这样即使采集到一半断网重启后也能直接继续。6.4 日志记录建议使用 Python 标准库logging替换print方便同时输出到控制台和文件import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(crawler.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(__name__)6.5 合规与安全注意事项这部分很重要希望你在动手前认真阅读遵守robots.txt。请求前先查看目标站点的robots.txt规则确认是否允许爬取。控制请求频率。每次请求之间至少间隔 1 到 3 秒既是对服务器的尊重也能降低被限制的风险。只采集有版权或授权使用的内容。图片素材可能涉及版权请勿将采集结果用于商业用途。不要采集包含个人隐私、敏感信息的内容。生产环境使用代理时确保代理来源合法合规。7. 总结与下一步本文从零完成了一个 Python 图片采集小工具核心包括使用requests请求页面并处理响应头、编码问题。使用BeautifulSoup解析列表页提取图片地址。使用文件流方式批量下载图片结合 MD5 实现去重。使用 Pillow 校验图片完整性提升数据质量。通过模块化结构拆分请求、解析、下载和工具函数方便后续扩展。如果你跟着文章敲了一遍代码现在应该已经拥有一个可以运行的基础版采集工具。下一步可以从这几个方向继续深入研究数据管理把文件记录迁移到 SQLite / MySQL支持更复杂的采集任务管理。性能优化学习asyncio和aiohttp实现异步采集提升大规模图片抓取效率。验证与清洗结合图像质量评估算法自动过滤模糊图、噪点图提高数据集质量。调度部署把脚本部署到服务器使用定时任务实现每日增量采集。最后再提醒一句技术本身是中立的关键在于使用场景。建议把脚本用于个人学习、数据集构建和授权范围内的素材整理不要对目标站点造成压力也不要侵犯他人版权。动手改一改代码把关键词换成你熟悉的内容运行一遍观察结果你对整个请求、解析、下载链路的理解会立刻不一样。如果这篇文章对你有帮助可以收藏备用后续我会继续输出爬虫工程化、图片数据处理和数据集构建相关的内容。

相关新闻