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/O,CPU是空闲的。这时,异步库
aiohttp
配合
asyncio
就该登场了。它可以同时发起和处理大量网络请求,极大地提升下载效率。不过,异步编程模型有一定学习门槛,代码结构也更复杂。
实操心得 :对于新手,强烈建议从
requests开始。先把同步的逻辑跑通,理解整个流程。当你的爬虫需要抓取的图片量很大,并且遇到了明显的性能瓶颈时,再考虑升级到aiohttp。99%的中小规模需求,requests完全够用。
2.2 HTML解析库:BeautifulSoup4 与 lxml
拿到HTML后,我们需要从中“大海捞针”,找出所有图片的标签(通常是
<img>
)。这就需要HTML解析库。
BeautifulSoup4
(简称bs4)是这方面的王者,它提供了“一锅乱炖”式的解析方式,能很好地处理各种不规范的HTML,API也非常直观,支持通过标签名、属性、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藏在
data-src
这类自定义属性里。因此,我们的提取逻辑需要更灵活。
下载图片本身,可以继续使用
requests.get()
,但需要以二进制模式(
stream=True
)读取响应内容,然后写入本地文件。这里的关键是处理文件路径和命名,避免重复和乱码。
2.4 辅助工具:正则表达式与路径处理
虽然
BeautifulSoup
能解决大部分解析问题,但偶尔遇到一些特别“淘气”的URL,或者需要从JavaScript代码块中提取信息时,正则表达式
re
模块就是一把瑞士军刀。比如,过滤出所有以
.jpg
或
.png
结尾的字符串。
本地文件保存离不开
os
和
pathlib
库。它们能帮你安全地创建文件夹、拼接路径、检查文件是否存在,是文件操作的基础。
工具链总结表:
| 环节 | 首选工具 | 备选/进阶工具 | 核心作用 |
|---|---|---|---|
| 网络请求 |
requests
|
aiohttp
(异步)
| 获取网页HTML内容 |
| HTML解析 |
BeautifulSoup4
+
lxml
|
parsel
(Scrapy风格)
|
从HTML中定位
<img>
标签
|
| 链接提取 |
BeautifulSoup
属性查找
|
正则表达式
re
| 从标签中获取真实的图片URL |
| 文件下载 |
requests.get()
(流模式)
|
aiohttp
+
aiofiles
(异步)
| 将网络图片以二进制形式保存到磁盘 |
| 路径管理 |
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, urlparse
-
os: 用于操作系统功能,如创建目录。 -
requests: 用于发送HTTP请求。 -
BeautifulSoup: 用于解析HTML。 -
urllib.parse.urljoin: 这是一个至关重要的函数。网页中的图片链接很多是相对路径(如/images/photo.jpg),我们需要将其与基础URL拼接成绝对路径。 -
urllib.parse.urlparse: 用于解析URL,从中提取信息,比如我们用它来生成合理的本地文件名。
第二步:定义核心爬取函数
def download_images(url, save_dir='downloaded_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, headers=headers, timeout=10)
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):
# 获取图片链接。优先考虑 data-src (懒加载),其次是 src
img_url = img_tag.get('data-src') or img_tag.get('src')
if not img_url:
# 如果既没有src也没有data-src,跳过这个标签
continue
# 将可能的相对URL转换为绝对URL
img_url = urljoin(url, img_url)
# 5. 下载并保存图片
try:
img_response = requests.get(img_url, headers=headers, stream=True, timeout=15)
img_response.raise_for_status()
# 从URL中提取一个合理的文件名
parsed_url = urlparse(img_url)
filename = os.path.basename(parsed_url.path)
# 如果文件名为空或太奇怪,使用索引作为文件名
if not filename or '.' not in filename:
filename = f'image_{i+1}.jpg'
else:
# 简单清理文件名,移除查询参数部分
filename = filename.split('?')[0]
# 完整的本地文件路径
filepath = os.path.join(save_dir, filename)
# 处理文件名冲突:如果文件已存在,在文件名后添加数字
counter = 1
original_filepath = filepath
while os.path.exists(filepath):
name, ext = os.path.splitext(original_filepath)
filepath = f"{name}_{counter}{ext}"
counter += 1
# 以二进制写入模式保存图片
with open(filepath, 'wb') as f:
for chunk in img_response.iter_content(chunk_size=8192):
f.write(chunk)
downloaded_count += 1
print(f"[成功] 已保存: {filename} (来自: {img_url[:50]}...)")
except Exception as e:
print(f"[失败] 下载失败: {img_url} - 错误: {e}")
continue
print(f"[总结] 尝试下载 {len(img_tags)} 张图片,成功 {downloaded_count} 张。")
第三步:调用函数
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(stream=True)配合iter_content()可以分块下载大文件,避免一次性将整个图片加载到内存,更安全高效。 - 文件名处理 :直接从URL路径的最后一部分取文件名是最常见的做法。但必须考虑文件名可能缺失、包含非法字符或重复的情况。我们的代码做了简单的冲突处理。
-
懒加载处理
:
img_tag.get('data-src') or img_tag.get('src')这行代码是关键。它优先获取data-src属性(常用于懒加载),如果不存在,再退而求其次获取src属性。
注意事项 :请务必遵守目标网站的
robots.txt协议,并尊重版权。这个基础爬虫仅用于学习和技术演示,在实际使用中,请控制请求频率,避免对目标服务器造成压力。对于商业网站或明确禁止爬虫的网站,请勿使用。
运行这个脚本,你就能看到它开始工作,并将图片保存到
downloaded_images
文件夹中。一个最基础的爬虫已经诞生了。但这仅仅是开始,一个健壮的、实用的爬虫还需要考虑更多问题。
4. 爬虫健壮性提升与高级特性
基础版本能跑通,但就像一辆能开动的原型车,要真正上路,还需要刹车、后视镜、防撞梁。下面我们来为爬虫添加这些“安全配置”和“高级功能”。
4.1 应对反爬虫机制
现代网站或多或少都有一些反爬虫措施。我们的爬虫需要一些“伪装”和“礼貌”。
-
请求头伪装 :除了
User-Agent,有时还需要添加Referer(表明你从哪个页面跳转过来)、Accept(声明接受的响应类型)等头信息,让自己更像一个真实的浏览器。headers = { 'User-Agent': '...', 'Referer': 'https://www.google.com/', # 示例,可设置为目标网站的域名 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.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, data=login_data) # 然后用session去get目标页面 response = session.get(target_url, headers=headers)
4.2 增强链接提取与过滤
基础版本只抓取了
<img>
标签。但图片还可能以其他形式存在:
-
CSS背景图
:有些图片通过
background-image: url(...)设置在CSS里。要抓取这些,需要先提取所有<style>标签或链接的CSS文件,然后用正则表达式匹配url(...)中的路径。这比较复杂,通常需要根据目标网站具体情况定制。 -
JavaScript动态加载
:越来越多的网站用JS动态渲染内容,初始HTML里没有图片标签。这时
requests+BeautifulSoup就无能为力了,因为它们只能看到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: # 这是一个简单示例,实际规则更复杂 continue
4.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 tqdm
from tqdm import tqdm
# 将遍历 img_tags 的循环改为
for i, img_tag in enumerate(tqdm(img_tags, desc="下载进度")):
# ... 下载逻辑 ...
同时,将打印信息写入日志文件,方便事后排查问题。
import logging
logging.basicConfig(level=logging.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(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('image_crawler.log', encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
class ImageCrawler:
def __init__(self, base_url, output_dir='./downloaded_images', max_workers=5, 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_ok=True)
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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.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, headers=self.headers, timeout=15)
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, headers=self.headers, stream=True, timeout=20)
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_workers=self.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(total=len(image_urls), desc="下载进度", unit="img") 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', type=int, default=5, help='并发下载线程数 (默认: 5)')
parser.add_argument('-d', '--delay', type=float, nargs=2, default=[1.0, 3.0],
metavar=('MIN', 'MAX'), help='请求延迟范围,单位秒 (默认: 1.0 3.0)')
args = parser.parse_args()
crawler = ImageCrawler(
base_url=args.url,
output_dir=args.output,
max_workers=args.workers,
delay_range=tuple(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 5
6. 常见问题排查与实战经验分享
即使有了健壮的脚本,在实际操作中还是会遇到各种稀奇古怪的问题。下面是我总结的一些典型“坑”及其解决方案。
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)也被当成了图片内容。 -
检查流式下载
:确保使用了
stream=True和resp.iter_content()。直接使用resp.content对于大文件可能引发内存问题,但通常不会导致文件损坏。 -
验证文件头
:真正的图片文件有特定的文件头(Magic Number)。例如,JPEG以
FF D8 FF开头,PNG以89 50 4E 47开头。可以写一个简单的函数在保存前校验。
-
检查响应状态码和内容类型
:在保存文件前,打印
6.3 爬取到的图片数量远少于浏览器所见
- 症状 :脚本只找到十几张图,但浏览器肉眼可见有上百张。
-
排查与解决
:
-
懒加载(Lazy Load)
:这是最主要的原因。现代网站大量使用此技术。解决方案就是像我们代码里做的那样,优先抓取
data-src,data-original等属性,而不是src。有时需要查看网页JS代码才能确定属性名。 -
JavaScript动态加载
:图片是通过JS(如Ajax)在页面加载后动态插入的。
BeautifulSoup解析的是初始HTML,看不到这些内容。 诊断方法 :在浏览器中右键点击“检查”,查看“元素”(Elements)面板,如果能在这里看到完整的<img>标签,说明是懒加载;如果这里也看不到,说明是JS动态加载。 解决方案 :使用Selenium、Playwright或Pyppeteer等无头浏览器工具。 -
图片在iframe或Shadow DOM中
:如果图片位于
<iframe>内,你需要先获取iframe的src,然后单独请求那个URL并解析。Shadow DOM则更复杂,通常也需要无头浏览器。
-
懒加载(Lazy Load)
:这是最主要的原因。现代网站大量使用此技术。解决方案就是像我们代码里做的那样,优先抓取
6.4 文件名乱码或包含非法字符
-
症状
:保存的文件名是一堆乱码,或者包含
\/:*?"<>|等Windows系统不允许的字符,导致保存失败。 -
解决方案
:在生成文件名时进行清洗。
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 filename
6.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张图片的测试模式跑一遍。观察日志,检查下载的图片是否正确,请求是否正常。确认无误后,再放开限制进行全量爬取。这个习惯帮我避免了很多次“跑了一晚上,结果全下错了”的悲剧。爬虫开发,三分在写,七分在调。耐心和细致的测试,是成功的关键。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_32466193/article/details/163708794



