Python图片爬虫实战:从Requests到异步下载的完整实现

📅 2026/8/13 12:36:25
Python图片爬虫实战:从Requests到异步下载的完整实现
1. 项目概述从手动右键到自动化归档干过内容运营、素材收集或者数据分析的朋友十有八九都遇到过这个场景看到一个网页里面几十上百张高清图片正是你急需的素材库。然后呢一张张右键、另存为、重命名……重复劳动枯燥不说还容易出错漏存。几年前我刚入行做新媒体时就经常为了找配图这么干效率低到令人发指。后来接触了Python第一个想到的自动化任务就是搞定这个痛点——写个爬虫让它替我批量抓取网页上的图片并保存到本地。这个项目说白了就是用程序模拟浏览器访问网页自动识别页面中的所有图片链接然后一张张下载下来并按你设定的规则整理好。它解决的远不止是“省时间”的问题。比如在做竞品分析时你需要批量抓取对手官网的产品图在做设计灵感收集时需要从特定网站归档一批风格统一的图片甚至在做简单的数据标注或图像识别项目前期也需要一个可靠的图片采集工具。手动操作在几十张的规模下尚可忍受一旦目标成百上千自动化就成了唯一可行的路径。实现这个目标核心就是Python。它丰富的第三方库让HTTP请求、HTML解析、文件操作这些步骤变得异常简单。即使你是个刚学完基础语法的新手跟着清晰的步骤走一两个小时也能搭出一个可用的爬虫。接下来我会结合我这些年踩过的坑和优化经验把一个完整的、健壮的图片爬虫从思路到代码再到部署和问题排查给你彻底讲明白。你会发现自动化处理信息才是打开数字世界效率之门的正确方式。2. 核心工具链选型与原理剖析工欲善其事必先利其器。选对工具项目就成功了一半。一个图片爬虫的工作流程可以抽象为四个核心环节获取网页、解析内容、提取链接、下载保存。每个环节都有若干成熟的Python库可供选择我们的选型需要兼顾易用性、性能和生态。2.1 网络请求库Requests vs. aiohttp首先是把网页的原始HTML代码“拿回来”。这里最经典的选择是requests库。它提供了极其人性化的API几乎成了Python发送HTTP请求的事实标准。它的优点是同步、阻塞式代码写起来是线性的非常易于理解和调试。对于初学者或者目标页面不多的场景requests是首选。import requests response requests.get(https://example.com) html_content response.text但是当需要批量抓取成百上千个页面或者一个页面里有大量图片时同步请求的缺点就暴露了你必须等一张图片下载完才能开始下一张大部分时间都在等待网络I/OCPU是空闲的。这时异步库aiohttp配合asyncio就该登场了。它可以同时发起和处理大量网络请求极大地提升下载效率。不过异步编程模型有一定学习门槛代码结构也更复杂。实操心得对于新手强烈建议从requests开始。先把同步的逻辑跑通理解整个流程。当你的爬虫需要抓取的图片量很大并且遇到了明显的性能瓶颈时再考虑升级到aiohttp。99%的中小规模需求requests完全够用。2.2 HTML解析库BeautifulSoup4 与 lxml拿到HTML后我们需要从中“大海捞针”找出所有图片的标签通常是img。这就需要HTML解析库。BeautifulSoup4简称bs4是这方面的王者它提供了“一锅乱炖”式的解析方式能很好地处理各种不规范的HTMLAPI也非常直观支持通过标签名、属性、CSS选择器等多种方式查找元素。from bs4 import BeautifulSoup soup BeautifulSoup(html_content, html.parser) img_tags soup.find_all(img)注意上面代码中的html.parser这是Python内置的解析器速度慢但无需安装。在实际生产中我们通常会指定lxml作为解析器需要先pip install lxml。lxml是一个用C语言编写的库解析速度极快容错能力也更强。BeautifulSoup只是一个“包装器”它依赖底层的解析器如lxml,html5lib来干活。所以最佳实践是安装lxml然后这样创建对象soup BeautifulSoup(html_content, lxml) # 速度更快更健壮2.3 图片链接提取与下载找到img标签后图片的URL通常存储在src属性里。但事情没那么简单。有些网站使用懒加载技术初始的src可能是一个占位图真正的图片URL藏在>环节首选工具备选/进阶工具核心作用网络请求requestsaiohttp(异步)获取网页HTML内容HTML解析BeautifulSoup4lxmlparsel(Scrapy风格)从HTML中定位img标签链接提取BeautifulSoup属性查找正则表达式re从标签中获取真实的图片URL文件下载requests.get()(流模式)aiohttpaiofiles(异步)将网络图片以二进制形式保存到磁盘路径管理os,pathlib-创建目录、生成文件名、管理文件这套组合拳构成了我们爬虫项目的技术骨架。接下来我们就用它们来搭建一个五脏俱全的爬虫。3. 基础爬虫搭建从零到一的完整实现让我们暂时忘掉那些复杂的概念先动手实现一个最基础、但绝对可用的版本。这个版本的目标很明确给定一个网页URL爬取该页面上所有图片保存到本地一个文件夹里。3.1 环境准备与依赖安装首先确保你的Python环境已经就绪建议使用Python 3.7及以上版本。然后通过pip安装我们需要的核心库。打开你的终端或命令行执行以下命令pip install requests beautifulsoup4 lxml这三行命令会安装网络请求、HTML解析以及高效的解析器引擎。安装过程通常很快如果遇到速度慢的问题可以考虑使用国内的镜像源例如加上-i https://pypi.tuna.tsinghua.edu.cn/simple。3.2 核心代码逐行解析下面我们创建一个名为simple_image_crawler.py的Python文件并开始编写代码。第一步导入必要的模块import os import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparseos: 用于操作系统功能如创建目录。requests: 用于发送HTTP请求。BeautifulSoup: 用于解析HTML。urllib.parse.urljoin: 这是一个至关重要的函数。网页中的图片链接很多是相对路径如/images/photo.jpg我们需要将其与基础URL拼接成绝对路径。urllib.parse.urlparse: 用于解析URL从中提取信息比如我们用它来生成合理的本地文件名。第二步定义核心爬取函数def download_images(url, save_dirdownloaded_images): 从指定URL下载所有图片到本地目录 :param url: 目标网页的URL :param save_dir: 图片保存的本地目录名 # 1. 创建保存目录 if not os.path.exists(save_dir): os.makedirs(save_dir) print(f[信息] 创建保存目录: {save_dir}) else: print(f[信息] 目录已存在: {save_dir}) # 2. 发送HTTP请求获取网页内容 try: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出异常 response.encoding response.apparent_encoding # 自动识别编码 html_content response.text except requests.exceptions.RequestException as e: print(f[错误] 请求网页失败: {e}) return # 3. 解析HTML找到所有img标签 soup BeautifulSoup(html_content, lxml) img_tags soup.find_all(img) print(f[信息] 在页面上找到 {len(img_tags)} 个图片标签) # 4. 遍历每个img标签下载图片 downloaded_count 0 for i, img_tag in enumerate(img_tags): # 获取图片链接。优先考虑>if __name__ __main__: target_url https://example.com # 替换成你想爬取的网站 download_images(target_url)3.3 代码关键点解读与避坑指南User-Agent头这是模拟浏览器访问的关键。没有这个头很多网站会拒绝请求或返回不同的内容。代码中使用的是一个常见的Chrome浏览器UA字符串。异常处理网络请求和文件操作充满了不确定性。使用try...except包裹关键步骤可以避免程序因单个图片下载失败而整体崩溃。链接补全urljoin(base, url)是处理相对路径的神器。无论img_url是/img/1.jpg还是./img/1.jpg它都能正确拼接成完整的URL。流式下载requests.get(streamTrue)配合iter_content()可以分块下载大文件避免一次性将整个图片加载到内存更安全高效。文件名处理直接从URL路径的最后一部分取文件名是最常见的做法。但必须考虑文件名可能缺失、包含非法字符或重复的情况。我们的代码做了简单的冲突处理。懒加载处理img_tag.get(data-src) or img_tag.get(src)这行代码是关键。它优先获取>headers { User-Agent: ..., Referer: https://www.google.com/, # 示例可设置为目标网站的域名 Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }请求延迟在循环下载图片的请求之间随机等待一小段时间。这是最基本的“礼貌”可以避免因请求过快被服务器封禁IP。import time import random # 在下载每张图片的循环内请求前或请求后添加 time.sleep(random.uniform(0.5, 2.0)) # 随机等待0.5到2秒处理Cookie与Session有些网站需要登录后才能看到图片。这时可以使用requests.Session()对象它会自动管理Cookie模拟一次完整的浏览器会话。session requests.Session() # 可以先POST请求登录如果需要 # login_data {username: ..., password: ...} # session.post(login_url, datalogin_data) # 然后用session去get目标页面 response session.get(target_url, headersheaders)4.2 增强链接提取与过滤基础版本只抓取了img标签。但图片还可能以其他形式存在CSS背景图有些图片通过background-image: url(...)设置在CSS里。要抓取这些需要先提取所有style标签或链接的CSS文件然后用正则表达式匹配url(...)中的路径。这比较复杂通常需要根据目标网站具体情况定制。JavaScript动态加载越来越多的网站用JS动态渲染内容初始HTML里没有图片标签。这时requestsBeautifulSoup就无能为力了因为它们只能看到JS执行前的静态HTML。解决方案是使用Selenium或Playwright这类浏览器自动化工具它们能控制一个真实的浏览器如Chrome加载页面、执行JS等页面完全渲染后再获取HTML。这是一个更高级的话题代码复杂度和资源消耗也更大。链接过滤我们可能只想要特定尺寸、特定格式的图片。可以在提取链接后增加过滤逻辑。# 示例只下载jpg和png格式且链接中包含‘large’关键词的图片 allowed_extensions (.jpg, .jpeg, .png, .webp) if not img_url.lower().endswith(allowed_extensions): continue if large not in img_url: # 这是一个简单示例实际规则更复杂 continue4.3 文件管理优化按域名或日期分类保存下载的图片全部堆在一个文件夹里很快就会混乱。我们可以创建更有条理的目录结构。import datetime # 在创建保存目录时使用更结构化的路径 domain urlparse(url).netloc.replace(www., ) # 获取域名去掉www date_str datetime.datetime.now().strftime(%Y%m%d) save_dir os.path.join(downloads, domain, date_str) # 最终路径如 downloads/example.com/20231027/更智能的文件命名除了从URL提取我们还可以尝试从图片标签的alt属性图片描述文本生成文件名或者使用MD5哈希值作为唯一文件名。import hashlib # 使用图片内容的MD5作为文件名绝对唯一 img_data img_response.content file_hash hashlib.md5(img_data).hexdigest() # 获取文件扩展名 content_type img_response.headers.get(content-type, ) if jpeg in content_type: ext .jpg elif png in content_type: ext .png else: # 从URL猜测或使用默认 ext .jpg filename f{file_hash}{ext}4.4 加入进度显示与日志记录对于批量下载一个直观的进度条能极大提升体验。可以使用tqdm库。pip install tqdmfrom tqdm import tqdm # 将遍历 img_tags 的循环改为 for i, img_tag in enumerate(tqdm(img_tags, desc下载进度)): # ... 下载逻辑 ...同时将打印信息写入日志文件方便事后排查问题。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(crawler.log), logging.StreamHandler()]) # 之后用 logging.info(...) 代替 print(...)经过这些增强你的爬虫已经从一个“玩具”升级为一个相对可靠的工具了。它更礼貌、更健壮、也更易用。5. 实战构建一个可配置的通用爬虫脚本现在我们把前面提到的所有最佳实践整合起来写一个更通用、更强大的爬虫脚本。这个脚本支持通过命令行参数配置并具备基本的错误恢复能力。创建一个新文件advanced_image_crawler.py。#!/usr/bin/env python3 高级图片爬虫脚本 支持命令行参数具备反爬、分类保存、日志记录等功能。 import os import sys import time import random import logging import argparse import hashlib from urllib.parse import urljoin, urlparse from concurrent.futures import ThreadPoolExecutor, as_completed import requests from bs4 import BeautifulSoup from tqdm import tqdm # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(image_crawler.log, encodingutf-8), logging.StreamHandler(sys.stdout) ] ) logger logging.getLogger(__name__) class ImageCrawler: def __init__(self, base_url, output_dir./downloaded_images, max_workers5, delay_range(1, 3)): 初始化爬虫 :param base_url: 目标网页URL :param output_dir: 输出根目录 :param max_workers: 并发下载线程数 :param delay_range: 请求延迟范围秒 self.base_url base_url self.output_dir output_dir self.max_workers max_workers self.delay_range delay_range # 创建按域名和日期分类的目录 self.domain urlparse(base_url).netloc.replace(www., ) self.date_str time.strftime(%Y%m%d) self.save_dir os.path.join(output_dir, self.domain, self.date_str) os.makedirs(self.save_dir, exist_okTrue) logger.info(f图片将保存至: {self.save_dir}) # 会话和请求头 self.session requests.Session() self.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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: base_url, } # 记录已下载的图片哈希避免重复下载同一页面内 self.downloaded_hashes set() def _random_delay(self): 随机延迟模拟人工操作 time.sleep(random.uniform(*self.delay_range)) def fetch_page(self): 获取目标页面HTML try: logger.info(f正在获取页面: {self.base_url}) self._random_delay() resp self.session.get(self.base_url, headersself.headers, timeout15) resp.raise_for_status() # 尝试自动检测编码 resp.encoding resp.apparent_encoding if resp.apparent_encoding else utf-8 return resp.text except requests.exceptions.RequestException as e: logger.error(f获取页面失败: {e}) return None def extract_image_urls(self, html): 从HTML中提取所有图片URL if not html: return [] soup BeautifulSoup(html, lxml) img_tags soup.find_all(img) urls [] for img in img_tags: # 多种属性尝试 for attr in [data-src, data-original, src, data-lazy-src]: img_url img.get(attr) if img_url and not img_url.startswith(data:image): # 排除base64内嵌图片 # 转换为绝对URL absolute_url urljoin(self.base_url, img_url) urls.append(absolute_url) break # 找到一个有效属性就跳出循环 # 去重 unique_urls list(dict.fromkeys(urls)) logger.info(f共提取到 {len(unique_urls)} 个唯一图片链接) return unique_urls def download_single_image(self, img_url, index): 下载单张图片 try: self._random_delay() resp self.session.get(img_url, headersself.headers, streamTrue, timeout20) resp.raise_for_status() # 根据Content-Type确定扩展名 content_type resp.headers.get(content-type, ).lower() if jpeg in content_type or jpg in content_type: ext .jpg elif png in content_type: ext .png elif gif in content_type: ext .gif elif webp in content_type: ext .webp else: # 从URL猜测或使用默认 parsed urlparse(img_url) path_ext os.path.splitext(parsed.path)[1] ext path_ext if path_ext and len(path_ext) 6 else .jpg # 生成唯一文件名索引_内容MD5 img_data resp.content file_hash hashlib.md5(img_data).hexdigest()[:8] # 取前8位足够唯一且短 # 检查是否重复基于哈希 if file_hash in self.downloaded_hashes: logger.warning(f跳过重复图片: {img_url}) return False filename f{index:04d}_{file_hash}{ext} filepath os.path.join(self.save_dir, filename) with open(filepath, wb) as f: f.write(img_data) self.downloaded_hashes.add(file_hash) logger.debug(f已保存: {filename}) return True except Exception as e: logger.error(f下载失败 [{img_url[:50]}...]: {e}) return False def run(self): 主运行流程 html self.fetch_page() if not html: logger.error(无法获取页面内容程序退出。) return image_urls self.extract_image_urls(html) if not image_urls: logger.warning(未在页面中发现图片链接。) return logger.info(f开始下载 {len(image_urls)} 张图片使用 {self.max_workers} 个线程...) success_count 0 # 使用线程池并发下载 with ThreadPoolExecutor(max_workersself.max_workers) as executor: # 创建任务字典 {future: (url, index)} future_to_url { executor.submit(self.download_single_image, url, idx): (url, idx) for idx, url in enumerate(image_urls, 1) } # 使用tqdm创建进度条 with tqdm(totallen(image_urls), desc下载进度, unitimg) as pbar: for future in as_completed(future_to_url): url, idx future_to_url[future] try: if future.result(): # 如果下载成功返回True success_count 1 except Exception as e: logger.error(f任务执行异常 [{url[:50]}...]: {e}) finally: pbar.update(1) # 更新进度条 logger.info(f任务完成成功下载 {success_count}/{len(image_urls)} 张图片。) logger.info(f图片保存位置: {os.path.abspath(self.save_dir)}) def main(): parser argparse.ArgumentParser(description高级图片爬虫) parser.add_argument(url, help目标网页的URL) parser.add_argument(-o, --output, default./downloaded_images, help输出目录 (默认: ./downloaded_images)) parser.add_argument(-w, --workers, typeint, default5, help并发下载线程数 (默认: 5)) parser.add_argument(-d, --delay, typefloat, nargs2, default[1.0, 3.0], metavar(MIN, MAX), help请求延迟范围单位秒 (默认: 1.0 3.0)) args parser.parse_args() crawler ImageCrawler( base_urlargs.url, output_dirargs.output, max_workersargs.workers, delay_rangetuple(args.delay) ) crawler.run() if __name__ __main__: main()这个脚本的亮点在于面向对象封装将功能封装在ImageCrawler类中结构清晰易于维护和扩展。命令行接口使用argparse库可以通过命令行参数灵活配置URL、输出目录、并发数等。并发下载使用ThreadPoolExecutor实现多线程下载显著提升批量下载速度。智能去重通过计算图片内容的MD5哈希值避免下载完全相同的图片。健壮的文件命名结合索引和哈希值生成唯一且有序的文件名。完善的日志同时输出到控制台和文件便于调试和追溯。使用方法# 基本用法 python advanced_image_crawler.py https://example.com # 指定输出目录和并发数 python advanced_image_crawler.py https://example.com -o ./my_pics -w 10 # 指定更长的请求延迟更礼貌 python advanced_image_crawler.py https://example.com -d 2 56. 常见问题排查与实战经验分享即使有了健壮的脚本在实际操作中还是会遇到各种稀奇古怪的问题。下面是我总结的一些典型“坑”及其解决方案。6.1 请求被拒绝或返回403错误这是最常见的反爬虫响应。症状requests.get()抛出异常或返回的状态码是403 Forbidden。排查与解决检查User-Agent确保你的请求头中包含一个常见浏览器的User-Agent。可以尝试更换不同的UA字符串。添加Referer有些网站会检查请求来源Referer将其设置为目标网站的域名或一个搜索引擎的URL。使用Session对于需要维持状态的网站务必使用requests.Session()。模拟更完整的请求头复制浏览器开发者工具中Network标签下的一个真实请求的所有Headers直接用作你的请求头字典。终极方案Selenium如果网站反爬极其严格如验证码、复杂JS加密考虑使用Selenium模拟真人操作。但这会牺牲速度。6.2 下载的图片文件损坏或无法打开症状文件大小异常如只有几KB或用图片查看器打开时报错。排查与解决检查响应状态码和内容类型在保存文件前打印resp.status_code和resp.headers.get(content-type)。确保状态码是200且内容类型是图片如image/jpeg。有时服务器返回的错误页面如404 HTML也被当成了图片内容。检查流式下载确保使用了streamTrue和resp.iter_content()。直接使用resp.content对于大文件可能引发内存问题但通常不会导致文件损坏。验证文件头真正的图片文件有特定的文件头Magic Number。例如JPEG以FF D8 FF开头PNG以89 50 4E 47开头。可以写一个简单的函数在保存前校验。6.3 爬取到的图片数量远少于浏览器所见症状脚本只找到十几张图但浏览器肉眼可见有上百张。排查与解决懒加载Lazy Load这是最主要的原因。现代网站大量使用此技术。解决方案就是像我们代码里做的那样优先抓取>import re def sanitize_filename(filename): # 移除非法字符 filename re.sub(r[\/:*?|], _, filename) # 限制长度可选 if len(filename) 255: name, ext os.path.splitext(filename) filename name[:250-len(ext)] ext return filename6.5 性能瓶颈与优化症状下载几百张图片速度极慢。优化方向增加并发如我们脚本中所做使用线程池ThreadPoolExecutor。注意线程数不是越多越好一般设置为5-15个为宜过多可能导致本地网络拥堵或被目标服务器封禁。异步IO对于IO密集型任务异步是终极方案。将核心下载逻辑用aiohttp和aiofiles重写可以同时处理成千上万个请求性能提升巨大但代码复杂度也最高。减少延迟在遵守网站规则的前提下可以适当缩短delay_range。对于反爬不严的网站可以设置为(0.1, 0.5)。连接复用使用requests.Session()本身就复用了TCP连接比每次创建新连接高效。6.6 法律与道德风险规避这是最重要的一点技术必须在合规的框架内使用。尊重robots.txt在爬取前访问https://目标网站/robots.txt查看是否允许爬虫访问你想要的路径。Python有robotparser模块可以解析此文件。控制请求速率务必在请求间添加延迟避免对目标服务器造成拒绝服务攻击DoS的压力。我们的_random_delay函数就是干这个的。遵守版权明确你爬取的图片的版权归属。个人学习、研究使用通常属于合理使用范畴但未经授权用于商业目的、大量分发或公开传播可能构成侵权。识别并遵守网站条款许多网站的“服务条款”Terms of Service中明确禁止爬虫。在使用自动化工具前最好查阅一下。最后分享一个我自己的习惯在运行任何爬虫尤其是新写的、针对陌生网站的爬虫时我会先用一个很小的、只抓取前2-3张图片的测试模式跑一遍。观察日志检查下载的图片是否正确请求是否正常。确认无误后再放开限制进行全量爬取。这个习惯帮我避免了很多次“跑了一晚上结果全下错了”的悲剧。爬虫开发三分在写七分在调。耐心和细致的测试是成功的关键。