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

亿牛云代理

摘要

图像抓取是一种常见的网络爬虫技术,用于从网页上下载图片并保存到本地文件夹中。然而,当需要抓取的图片数量很大时,可能会出现内存不足的错误,导致程序崩溃。本文介绍了如何使用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”的文件中。

# 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方法来提交每个网站的图片抓取任务,从而提高程序的效率和并发性。通过这些方法和技巧,我们可以实现一个高效、稳定、可扩展的大规模图像抓取程序。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,638评论 18 139
  • 设计和部署 API 以服务于机器学习模型。## Intuition[CLI 应用程序](https://franz...
    陶恒franz阅读 217评论 0 0
  • 安装Nginx:yum install -y nginxsystemctl start nginxsystemct...
    碧潭飘雪ikaros阅读 697评论 0 1
  • Awesome Ruby Toolbox Awesome A collection of awesome Ruby...
    debbbbie阅读 2,824评论 0 3
  • scrapy学习笔记(有示例版) 我的博客 scrapy学习笔记1.使用scrapy1.1创建工程1.2创建爬虫模...
    陈思煜阅读 12,670评论 4 46