莱夢头像
关注

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/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 代码关键点解读与避坑指南

  1. User-Agent头 :这是模拟浏览器访问的关键。没有这个头,很多网站会拒绝请求或返回不同的内容。代码中使用的是一个常见的Chrome浏览器UA字符串。
  2. 异常处理 :网络请求和文件操作充满了不确定性。使用 try...except 包裹关键步骤,可以避免程序因单个图片下载失败而整体崩溃。
  3. 链接补全 : urljoin(base, url) 是处理相对路径的神器。无论 img_url 是 /img/1.jpg 还是 ./img/1.jpg ,它都能正确拼接成完整的URL。
  4. 流式下载 : requests.get(stream=True) 配合 iter_content() 可以分块下载大文件,避免一次性将整个图片加载到内存,更安全高效。
  5. 文件名处理 :直接从URL路径的最后一部分取文件名是最常见的做法。但必须考虑文件名可能缺失、包含非法字符或重复的情况。我们的代码做了简单的冲突处理。
  6. 懒加载处理 : img_tag.get('data-src') or img_tag.get('src') 这行代码是关键。它优先获取 data-src 属性(常用于懒加载),如果不存在,再退而求其次获取 src 属性。

注意事项 :请务必遵守目标网站的 robots.txt 协议,并尊重版权。这个基础爬虫仅用于学习和技术演示,在实际使用中,请控制请求频率,避免对目标服务器造成压力。对于商业网站或明确禁止爬虫的网站,请勿使用。

运行这个脚本,你就能看到它开始工作,并将图片保存到 downloaded_images 文件夹中。一个最基础的爬虫已经诞生了。但这仅仅是开始,一个健壮的、实用的爬虫还需要考虑更多问题。

4. 爬虫健壮性提升与高级特性

基础版本能跑通,但就像一辆能开动的原型车,要真正上路,还需要刹车、后视镜、防撞梁。下面我们来为爬虫添加这些“安全配置”和“高级功能”。

4.1 应对反爬虫机制

现代网站或多或少都有一些反爬虫措施。我们的爬虫需要一些“伪装”和“礼貌”。

  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',
    }
    
  2. 请求延迟 :在循环下载图片的请求之间,随机等待一小段时间。这是最基本的“礼貌”,可以避免因请求过快被服务器封禁IP。

    import time
    import random
    
    # 在下载每张图片的循环内,请求前或请求后添加
    time.sleep(random.uniform(0.5, 2.0)) # 随机等待0.5到2秒
    
  3. 处理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> 标签。但图片还可能以其他形式存在:

  1. CSS背景图 :有些图片通过 background-image: url(...) 设置在CSS里。要抓取这些,需要先提取所有 <style> 标签或链接的CSS文件,然后用正则表达式匹配 url(...) 中的路径。这比较复杂,通常需要根据目标网站具体情况定制。
  2. JavaScript动态加载 :越来越多的网站用JS动态渲染内容,初始HTML里没有图片标签。这时 requests + BeautifulSoup 就无能为力了,因为它们只能看到JS执行前的静态HTML。解决方案是使用 Selenium 或 Playwright 这类浏览器自动化工具,它们能控制一个真实的浏览器(如Chrome)加载页面、执行JS,等页面完全渲染后再获取HTML。这是一个更高级的话题,代码复杂度和资源消耗也更大。
  3. 链接过滤 :我们可能只想要特定尺寸、特定格式的图片。可以在提取链接后增加过滤逻辑。
    # 示例:只下载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 文件管理优化

  1. 按域名或日期分类保存 :下载的图片全部堆在一个文件夹里,很快就会混乱。我们可以创建更有条理的目录结构。

    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/
    
  2. 更智能的文件命名 :除了从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()

这个脚本的亮点在于:

  1. 面向对象封装 :将功能封装在 ImageCrawler 类中,结构清晰,易于维护和扩展。
  2. 命令行接口 :使用 argparse 库,可以通过命令行参数灵活配置URL、输出目录、并发数等。
  3. 并发下载 :使用 ThreadPoolExecutor 实现多线程下载,显著提升批量下载速度。
  4. 智能去重 :通过计算图片内容的MD5哈希值,避免下载完全相同的图片。
  5. 健壮的文件命名 :结合索引和哈希值,生成唯一且有序的文件名。
  6. 完善的日志 :同时输出到控制台和文件,便于调试和追溯。

使用方法:

# 基本用法
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。
  • 排查与解决 :
    1. 检查User-Agent :确保你的请求头中包含一个常见浏览器的User-Agent。可以尝试更换不同的UA字符串。
    2. 添加Referer :有些网站会检查请求来源(Referer),将其设置为目标网站的域名或一个搜索引擎的URL。
    3. 使用Session :对于需要维持状态的网站,务必使用 requests.Session() 。
    4. 模拟更完整的请求头 :复制浏览器开发者工具中Network标签下的一个真实请求的所有Headers,直接用作你的请求头字典。
    5. 终极方案:Selenium :如果网站反爬极其严格(如验证码、复杂JS加密),考虑使用Selenium模拟真人操作。但这会牺牲速度。

6.2 下载的图片文件损坏或无法打开

  • 症状 :文件大小异常(如只有几KB),或用图片查看器打开时报错。
  • 排查与解决 :
    1. 检查响应状态码和内容类型 :在保存文件前,打印 resp.status_code 和 resp.headers.get('content-type') 。确保状态码是200,且内容类型是图片(如 image/jpeg )。有时服务器返回的错误页面(如404 HTML)也被当成了图片内容。
    2. 检查流式下载 :确保使用了 stream=True 和 resp.iter_content() 。直接使用 resp.content 对于大文件可能引发内存问题,但通常不会导致文件损坏。
    3. 验证文件头 :真正的图片文件有特定的文件头(Magic Number)。例如,JPEG以 FF D8 FF 开头,PNG以 89 50 4E 47 开头。可以写一个简单的函数在保存前校验。

6.3 爬取到的图片数量远少于浏览器所见

  • 症状 :脚本只找到十几张图,但浏览器肉眼可见有上百张。
  • 排查与解决 :
    1. 懒加载(Lazy Load) :这是最主要的原因。现代网站大量使用此技术。解决方案就是像我们代码里做的那样,优先抓取 data-src , data-original 等属性,而不是 src 。有时需要查看网页JS代码才能确定属性名。
    2. JavaScript动态加载 :图片是通过JS(如Ajax)在页面加载后动态插入的。 BeautifulSoup 解析的是初始HTML,看不到这些内容。 诊断方法 :在浏览器中右键点击“检查”,查看“元素”(Elements)面板,如果能在这里看到完整的 <img> 标签,说明是懒加载;如果这里也看不到,说明是JS动态加载。 解决方案 :使用 Selenium 、 Playwright 或 Pyppeteer 等无头浏览器工具。
    3. 图片在iframe或Shadow DOM中 :如果图片位于 <iframe> 内,你需要先获取iframe的 src ,然后单独请求那个URL并解析。Shadow DOM则更复杂,通常也需要无头浏览器。

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 性能瓶颈与优化

  • 症状 :下载几百张图片速度极慢。
  • 优化方向 :
    1. 增加并发 :如我们脚本中所做,使用线程池( ThreadPoolExecutor )。注意,线程数不是越多越好,一般设置为5-15个为宜,过多可能导致本地网络拥堵或被目标服务器封禁。
    2. 异步IO :对于IO密集型任务,异步是终极方案。将核心下载逻辑用 aiohttp 和 aiofiles 重写,可以同时处理成千上万个请求,性能提升巨大,但代码复杂度也最高。
    3. 减少延迟 :在遵守网站规则的前提下,可以适当缩短 delay_range 。对于反爬不严的网站,可以设置为 (0.1, 0.5) 。
    4. 连接复用 :使用 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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--