如何防止Python大规模图像抓取过程中出现内存不足错误

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 图像抓取是一种常见的网络爬虫技术,用于从网页上下载图片并保存到本地文件夹中。然而,当需要抓取的图片数量很大时,可能会出现内存不足的错误,导致程序崩溃。本文介绍了如何使用Python进行大规模的图像抓取,并提供了一些优化内存使用的方法和技巧,以及如何计算和评估图片的质量指标。

亿牛云代理.jpg

摘要

图像抓取是一种常见的网络爬虫技术,用于从网页上下载图片并保存到本地文件夹中。然而,当需要抓取的图片数量很大时,可能会出现内存不足的错误,导致程序崩溃。本文介绍了如何使用Python进行大规模的图像抓取,并提供了一些优化内存使用的方法和技巧,以及如何计算和评估图片的质量指标。

正文

1. 导入必要的库和模块

为了实现图像抓取的功能,我们需要导入一些必要的库和模块,如pickle、logging、datetime等。其中,pickle模块用于序列化和反序列化对象,方便我们将处理结果保存到文件中;logging模块用于记录程序的运行日志,方便我们调试和监控程序的状态;datetime模块用于获取和处理日期和时间相关的信息,方便我们设置请求头部和日志格式等。

import pickle
import logging
from datetime import datetime
from dateutil.parser import parse as parse_date
from brisque import BRISQUE
import os
import cv2
import numpy as np
from PIL import Image
from io import BytesIO
import os
import requests
from skimage import color
from time import sleep
from random import choice
import concurrent.futures
from requests.exceptions import Timeout
from robots import RobotParser

from headers import HEADERS

MAX_RETRIES = 3         # Number of times the crawler should retry a URL
INITIAL_BACKOFF = 2     # Initial backoff delay in seconds
DEFAULT_SLEEP = 10      # Default sleep time in seconds after a 429 error

brisque = BRISQUE(url=False)

2. 设置日志记录器

为了方便记录程序的运行日志,我们需要设置一个日志记录器,用于将日志信息输出到文件中。我们可以使用logging模块提供的方法来创建一个名为“image-scraper”的日志记录器,并设置其日志级别为INFO。然后,我们可以创建一个文件处理器,用于将日志信息写入到指定的文件中,并设置其日志格式为包含日志级别、线程名、时间和消息内容等信息。最后,我们可以将文件处理器添加到日志记录器中,使其生效。

# --- SETUP LOGGER ---

filename = 'image-scraper.log'
filepath = os.path.dirname(os.path.abspath(__file__))

# create file path for log file
log_file = os.path.join(filepath, filename)

# create a FileHandler to log messages to the log file
handler = logging.FileHandler(log_file)
# set the log message formats
handler.setFormatter(
    logging.Formatter(
        '%(levelname)s %(threadName)s (%(asctime)s): %(message)s')
)
# create a logger with the given name and log level
logger = logging.getLogger('image-scraper')
# prevent logging from being send to the upper logger - that includes the console logging
logger.propagate = False
logger.setLevel(logging.INFO)
# add the FileHandler to the logger
logger.addHandler(handler)

3. 定义计算图片质量指标的函数

为了评估图片的质量,我们需要计算一些图片质量指标,如亮度、清晰度、对比度、色彩度等。我们可以定义一个函数get_image_quality_metrics,接受一个包含图片数据的响应对象作为参数,并返回一个包含各种质量指标的字典。在这个函数中,我们首先使用PIL库和numpy库将图片数据转换为数组形式,并使用cv2库和skimage库对图片进行处理和计算。具体来说:

  • 计算亮度:我们将图片转换为灰度图,并计算其像素值的平均值。
  • 计算清晰度:我们使用拉普拉斯算子对灰度图进行边缘检测,并计算其方差值。
  • 计算对比度:我们使用均方根对比度的公式,计算灰度图像素值与其平均值的差的平方的平均值的平方根。
  • 计算噪声:我们使用高斯滤波或中值绝对偏差(MAD)的方法,计算图片的方差值。
  • 计算饱和度:我们将图片转换为HSV颜色空间,并计算其饱和度通道的平均值。
  • 计算色彩度:我们将图片转换为LAB颜色空间,并计算其a和b通道的平方和的平方根的平均值。
  • 获取图片的尺寸:我们获取图片的高度和宽度,并将其添加到字典中。

    def get_image_quality_metrics(response):
      """
      Calculate various image quality metrics for an image.
    
      Args:
          response (requests.Response): The response object containing the image data.
    
      Returns:
          dict: A dict of image quality metrics including brightness, sharpness, contrast, and colorfulness.
      """
      image_array = np.frombuffer(response.content, np.uint8)
      image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
    
      metrics = dict()
    
      # Calculate brightness
      gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
      metrics['brightness'] = np.mean(gray)
    
      # Calculate sharpness using variance of Laplacian
      metrics['sharpness'] = cv2.Laplacian(gray, cv2.CV_64F).var()
    
      # Calculate contrast using root mean squared contrast
      metrics['contrast'] = np.sqrt(np.mean((gray - np.mean(gray)) ** 2))
    
      # Calculate image noise using variance of Gaussian or median absolute deviation (MAD)
      metrics['noise'] = np.var(image)
    
      # Calculate saturation using average saturation of pixels or histogram analysis
      hsv = color.rgb2hsv(image)
      saturation = hsv[:, :, 1]
      metrics['saturation'] = np.mean(saturation)
    
      # Calculate colorfulness
      lab = color.rgb2lab(image)
      a, b = lab[:, :, 1], lab[:, :, 2]
      metrics['colorfulness'] = np.sqrt(np.mean(a ** 2 + b ** 2))
    
      # Get dimenstions of the image
      height, width, _ = image.shape
      metrics['height'] = height
      metrics['width'] = width
    
      return metrics
    

    4. 定义发送请求的函数

    为了从网页上下载图片,我们需要发送GET请求到图片的URL,并获取响应对象。我们可以定义一个函数send_request,接受一个URL作为参数,并返回一个响应对象。在这个函数中,我们需要处理一些可能出现的异常和错误,如超时、状态码不为200、429等。为了避免被网站屏蔽或限制,我们需要使用代理服务器和随机选择的请求头部。具体来说:

  • 我们使用requests库提供的方法来创建一个代理服务器对象,使用亿牛云提供的代理服务器信息。

  • 我们使用一个while循环来重试请求,设置一个最大重试次数和一个初始退避延迟时间。
  • 我们从headers模块中随机选择一个请求头部,并将其添加到请求中。
  • 我们使用try-except语句来捕获可能出现的异常和错误,并根据不同的情况进行处理:

    • 如果出现超时错误,我们记录日志信息,并增加重试次数和退避延迟时间。
    • 如果出现状态码不为200的错误,我们记录日志信息,并根据状态码进行处理:
      • 如果状态码为429,表示请求过于频繁,我们需要等待一段时间后再重试,我们可以使用time模块提供的sleep方法来暂停程序运行,并设置一个默认的睡眠时间。
      • 如果状态码为403或404,表示请求被拒绝或资源不存在,我们可以直接跳出
      • 如果状态码为其他值,表示请求出现其他错误,我们可以直接抛出异常,并记录日志信息。
    • 如果没有出现异常或错误,我们返回响应对象,并记录日志信息。

      def send_request(url: str) -> requests.Response:
      """
      Sends a GET request to the specified URL, checks whether the link is valid,
      and returns a response object.
      
      Args:
         url (str): The URL to send the GET request to
      """
      retry_count = 0
      backoff = INITIAL_BACKOFF
      header = choice(HEADERS)
      
      # 亿牛云 爬虫代理加强版
      proxyHost = "www.16yun.cn"
      proxyPort = "31111"
      
      # 代理验证信息
      proxyUser = "16YUN"
      proxyPass = "16IP"
      
      # create a proxy server object using the proxy information    
      proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
             
             
         "host": proxyHost,
         "port": proxyPort,
         "user": proxyUser,
         "pass": proxyPass,
      }
      proxies = {
             
             
         "http": proxyMeta,
         "https": proxyMeta,
      }
      
      while retry_count < MAX_RETRIES:
         try:
             # Send a GET request to the website and return the response object
             req = requests.get(url, headers=header, proxies=proxies, timeout=20)
             req.raise_for_status()
             logger.info(f"Successfully fetched {url}")
             return req
      
         except Timeout:
             # Handle timeout error: log the error and increase the retry count and backoff delay
             logger.error(f"Timeout error for {url}")
             retry_count += 1
             backoff *= 2
      
         except requests.exceptions.HTTPError as e:
             # Handle HTTP error: log the error and check the status code
             logger.error(f"HTTP error for {url}: {e}")
             status_code = e.response.status_code
      
             if status_code == 429:
                 # Handle 429 error: wait for some time and retry
                 logger.info(f"Waiting for {DEFAULT_SLEEP} seconds after 429 error")
                 sleep(DEFAULT_SLEEP)
                 retry_count += 1
      
             elif status_code == 403 or status_code == 404:
                 # Handle 403 or 404 error: break the loop and return None
                 logger.info(f"Skipping {url} due to {status_code} error")
                 break
      
             else:
                 # Handle other errors: raise the exception and log the error
                 logger.error(f"Other HTTP error for {url}: {e}")
                 raise e
      
      # Return None if the loop ends without returning a response object
      return None
      

      5. 定义处理图片的函数

      为了从响应对象中提取图片的数据,并计算其质量指标和BRISQUE分数,我们可以定义一个函数process_image,接受一个响应对象和一个URL作为参数,并返回一个包含图片信息的字典。在这个函数中,我们需要使用“with”语句来管理文件和图片对象的打开和关闭,以及使用“del”语句来释放不再需要的变量,从而优化内存使用。具体来说:

  • 我们使用PIL库提供的方法来打开响应对象中的图片数据,并将其转换为RGBA格式。

  • 我们使用os模块提供的方法来创建一个名为“images”的文件夹,用于存储下载的图片。
  • 我们使用datetime模块提供的方法来获取当前的日期和时间,并将其转换为字符串格式,作为图片的文件名。
  • 我们使用“with”语句来打开一个以日期和时间命名的文件,并将图片数据写入到文件中。
  • 我们使用brisque模块提供的方法来计算图片的BRISQUE分数,并将其添加到字典中。
  • 我们使用前面定义的get_image_quality_metrics函数来计算图片的其他质量指标,并将其添加到字典中。
  • 我们使用“del”语句来删除不再需要的变量,如响应对象、图片对象等。
  • 我们返回包含图片信息的字典。

    def process_image(response, url):
      """
      Process an image from a response object and calculate its quality metrics and BRISQUE score.
    
      Args:
          response (requests.Response): The response object containing the image data.
          url (str): The URL of the image.
    
      Returns:
          dict: A dict of image information including quality metrics and BRISQUE score.
      """
      # Open the image data from the response object and convert it to RGBA format
      image = Image.open(BytesIO(response.content)).convert('RGBA')
    
      # Create a folder named "images" to store the downloaded images
      os.makedirs('images', exist_ok=True)
    
      # Get the current date and time and convert it to a string format as the image file name
      date_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
    
      # Open a file with the date and time as the file name and write the image data to it
      with open(f'images/{date_time}.png', 'wb') as f:
          image.save(f, 'PNG')
    
      # Calculate the BRISQUE score of the image and add it to the dict
      image_info = dict()
      image_info['brisque'] = get_brisque_score(response)
    
      # Calculate the other quality metrics of the image and add them to the dict
      image_info.update(get_image_quality_metrics(response))
    
      # Delete the response object and the image object to free up memory
      del response
      del image
    
      # Return the dict of image information
      return image_info
    

    6. 使用线程池来处理多个网站的图片抓取任务

    为了提高程序的效率和并发性,我们可以使用线程池来处理多个网站的图片抓取任务,并将处理结果保存到文件中。我们可以使用concurrent.futures模块提供的方法来创建一个线程池对象,并使用submit方法来提交每个网站的图片抓取任务。具体来说:

  • 我们创建一个名为“websites”的列表,用于存储需要抓取图片的网站的URL。

  • 我们创建一个名为“results”的列表,用于存储每个网站的图片抓取结果。
  • 我们使用“with”语句来创建一个线程池对象,并设置其最大线程数为10。
  • 我们遍历每个网站的URL,并使用submit方法来提交一个图片抓取任务,传入send_request函数和URL作为参数,并将返回的future对象添加到results列表中。
  • 我们遍历results列表中的每个future对象,并使用result方法来获取其结果,即响应对象。
  • 我们判断响应对象是否为None,如果不为None,表示请求成功,我们则使用process_image函数来处理响应对象,并将返回的图片信息字典添加到results列表中;如果为None,表示请求失败,我们则跳过该网站。
  • 我们使用pickle模块提供的方法来将results列表序列化并保存到一个名为“results.pkl”的文件中。
    ```python

    Create a list of websites to scrape images from

    websites = [
    'https://unsplash.com/',
    'https://pixabay.com/',
    'https://www.pexels.com/',
    'https://www.freeimages.com/',
    'https://stocksnap.io/',
    ]

Create a list to store the results of each website

results = []

Create a thread pool with 10 threads and submit tasks for each website

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for website in websites:

    # Submit a task to send a request to the website and get a response object
    future = executor.submit(send_request, website)
    # Add the future object to the results list
    results.append(future)

Iterate over the results list and get the result of each future object

for future in results:

# Get the response object from the future object
response = future.result()
# Check if the response object is None or not
if response is not None:
    # Process the response object and get the image information dict
    image_info = process_image(response, website)
    # Add the image information dict to the results list
    results.append(image_info)
else:
    # Skip the website if the response object is None
    continue

Serialize and save the results list to a file using pickle module

with open('results.pkl', 'wb') as f:
pickle.dump(results, f)
```

结论

本文介绍了如何使用Python进行大规模的图像抓取,并提供了一些优化内存使用的方法和技巧,以及如何计算和评估图片的质量指标。我们使用requests库来发送GET请求到图片的URL,并使用代理服务器和随机选择的请求头部来避免被网站屏蔽或限制。我们使用PIL库和cv2库来处理图片数据,并使用brisque模块和自定义的函数来计算图片的质量指标和BRISQUE分数。我们使用logging模块来记录程序的运行日志,并使用pickle模块来将处理结果保存到文件中。我们使用“with”语句来管理文件和图片对象的打开和关闭,以及使用“del”语句来释放不再需要的变量,从而优化内存使用。我们使用concurrent.futures模块来创建一个线程池,并使用submit方法来提交每个网站的图片抓取任务,从而提高程序的效率和并发性。通过这些方法和技巧,我们可以实现一个高效、稳定、可扩展的大规模图像抓取程序。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
14天前
|
机器学习/深度学习 人工智能 算法
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
宠物识别系统,本系统使用Python作为主要开发语言,基于TensorFlow搭建卷积神经网络算法,并收集了37种常见的猫狗宠物种类数据集【'阿比西尼亚猫(Abyssinian)', '孟加拉猫(Bengal)', '暹罗猫(Birman)', '孟买猫(Bombay)', '英国短毛猫(British Shorthair)', '埃及猫(Egyptian Mau)', '缅因猫(Maine Coon)', '波斯猫(Persian)', '布偶猫(Ragdoll)', '俄罗斯蓝猫(Russian Blue)', '暹罗猫(Siamese)', '斯芬克斯猫(Sphynx)', '美国斗牛犬
90 29
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
|
15天前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
28 3
|
21天前
|
算法 Java 程序员
Python内存管理机制深度剖析####
本文将深入浅出地探讨Python中的内存管理机制,特别是其核心组件——垃圾收集器(Garbage Collector, GC)的工作原理。不同于传统的摘要概述,我们将通过一个虚拟的故事线,跟随“内存块”小M的一生,从诞生、使用到最终被回收的过程,来揭示Python是如何处理对象生命周期,确保高效利用系统资源的。 ####
22 1
|
2月前
|
数据采集 JSON 数据处理
抓取和分析JSON数据:使用Python构建数据处理管道
在大数据时代,电商网站如亚马逊、京东等成为数据采集的重要来源。本文介绍如何使用Python结合代理IP、多线程等技术,高效、隐秘地抓取并处理电商网站的JSON数据。通过爬虫代理服务,模拟真实用户行为,提升抓取效率和稳定性。示例代码展示了如何抓取亚马逊商品信息并进行解析。
抓取和分析JSON数据:使用Python构建数据处理管道
|
1月前
|
机器学习/深度学习 TensorFlow 算法框架/工具
利用Python和TensorFlow构建简单神经网络进行图像分类
利用Python和TensorFlow构建简单神经网络进行图像分类
56 3
|
1月前
|
安全 开发者 Python
Python的内存管理pymalloc
Python的内存管理pymalloc
|
1月前
|
安全 开发者 Python
Python的内存管理pymalloc
Python的内存管理pymalloc
|
1月前
|
监控 Java API
Python是如何实现内存管理的
Python是如何实现内存管理的
|
2月前
|
数据采集 Python
python爬虫抓取91处理网
本人是个爬虫小萌新,看了网上教程学着做爬虫爬取91处理网www.91chuli.com,如果有什么问题请大佬们反馈,谢谢。
31 4
|
2月前
|
数据采集 Java Python
如何用Python同时抓取多个网页:深入ThreadPoolExecutor
在信息化时代,实时数据的获取对体育赛事爱好者、数据分析师和投注行业至关重要。本文介绍了如何使用Python的`ThreadPoolExecutor`结合代理IP和请求头设置,高效稳定地抓取五大足球联赛的实时比赛信息。通过多线程并发处理,解决了抓取效率低、请求限制等问题,提供了详细的代码示例和解析方法。
如何用Python同时抓取多个网页:深入ThreadPoolExecutor