
摘要
圖像抓取是一種常見的網(wǎng)絡(luò)爬蟲技術(shù),用于從網(wǎng)頁上下載圖片并保存到本地文件夾中。然而,當(dāng)需要抓取的圖片數(shù)量很大時,可能會出現(xiàn)內(nèi)存不足的錯誤,導(dǎo)致程序崩潰。本文介紹了如何使用Python進(jìn)行大規(guī)模的圖像抓取,并提供了一些優(yōu)化內(nèi)存使用的方法和技巧,以及如何計算和評估圖片的質(zhì)量指標(biāo)。
正文
1. 導(dǎo)入必要的庫和模塊
為了實現(xiàn)圖像抓取的功能,我們需要導(dǎo)入一些必要的庫和模塊,如pickle、logging、datetime等。其中,pickle模塊用于序列化和反序列化對象,方便我們將處理結(jié)果保存到文件中;logging模塊用于記錄程序的運(yùn)行日志,方便我們調(diào)試和監(jiān)控程序的狀態(tài);datetime模塊用于獲取和處理日期和時間相關(guān)的信息,方便我們設(shè)置請求頭部和日志格式等。
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. 設(shè)置日志記錄器
為了方便記錄程序的運(yùn)行日志,我們需要設(shè)置一個日志記錄器,用于將日志信息輸出到文件中。我們可以使用logging模塊提供的方法來創(chuàng)建一個名為“image-scraper”的日志記錄器,并設(shè)置其日志級別為INFO。然后,我們可以創(chuàng)建一個文件處理器,用于將日志信息寫入到指定的文件中,并設(shè)置其日志格式為包含日志級別、線程名、時間和消息內(nèi)容等信息。最后,我們可以將文件處理器添加到日志記錄器中,使其生效。
# --- 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. 定義計算圖片質(zhì)量指標(biāo)的函數(shù)
為了評估圖片的質(zhì)量,我們需要計算一些圖片質(zhì)量指標(biāo),如亮度、清晰度、對比度、色彩度等。我們可以定義一個函數(shù)get_image_quality_metrics,接受一個包含圖片數(shù)據(jù)的響應(yīng)對象作為參數(shù),并返回一個包含各種質(zhì)量指標(biāo)的字典。在這個函數(shù)中,我們首先使用PIL庫和numpy庫將圖片數(shù)據(jù)轉(zhuǎn)換為數(shù)組形式,并使用cv2庫和skimage庫對圖片進(jìn)行處理和計算。具體來說:
計算亮度:我們將圖片轉(zhuǎn)換為灰度圖,并計算其像素值的平均值。
計算清晰度:我們使用拉普拉斯算子對灰度圖進(jìn)行邊緣檢測,并計算其方差值。
計算對比度:我們使用均方根對比度的公式,計算灰度圖像素值與其平均值的差的平方的平均值的平方根。
計算噪聲:我們使用高斯濾波或中值絕對偏差(MAD)的方法,計算圖片的方差值。
計算飽和度:我們將圖片轉(zhuǎn)換為HSV顏色空間,并計算其飽和度通道的平均值。
計算色彩度:我們將圖片轉(zhuǎn)換為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. 定義發(fā)送請求的函數(shù)
為了從網(wǎng)頁上下載圖片,我們需要發(fā)送GET請求到圖片的URL,并獲取響應(yīng)對象。我們可以定義一個函數(shù)send_request,接受一個URL作為參數(shù),并返回一個響應(yīng)對象。在這個函數(shù)中,我們需要處理一些可能出現(xiàn)的異常和錯誤,如超時、狀態(tài)碼不為200、429等。為了避免被網(wǎng)站屏蔽或限制,我們需要使用代理服務(wù)器和隨機(jī)選擇的請求頭部。具體來說:
我們使用requests庫提供的方法來創(chuàng)建一個代理服務(wù)器對象,使用億牛云提供的代理服務(wù)器信息。
我們使用一個while循環(huán)來重試請求,設(shè)置一個最大重試次數(shù)和一個初始退避延遲時間。
我們從headers模塊中隨機(jī)選擇一個請求頭部,并將其添加到請求中。
我們使用try-except語句來捕獲可能出現(xiàn)的異常和錯誤,并根據(jù)不同的情況進(jìn)行處理:?
如果出現(xiàn)超時錯誤,我們記錄日志信息,并增加重試次數(shù)和退避延遲時間。
如果出現(xiàn)狀態(tài)碼不為200的錯誤,我們記錄日志信息,并根據(jù)狀態(tài)碼進(jìn)行處理:?
如果狀態(tài)碼為429,表示請求過于頻繁,我們需要等待一段時間后再重試,我們可以使用time模塊提供的sleep方法來暫停程序運(yùn)行,并設(shè)置一個默認(rèn)的睡眠時間。
如果狀態(tài)碼為403或404,表示請求被拒絕或資源不存在,我們可以直接跳出
如果狀態(tài)碼為其他值,表示請求出現(xiàn)其他錯誤,我們可以直接拋出異常,并記錄日志信息。?
如果沒有出現(xiàn)異常或錯誤,我們返回響應(yīng)對象,并記錄日志信息。
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)
? ? # 億牛云 爬蟲代理加強(qiáng)版
? ? 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. 定義處理圖片的函數(shù)
為了從響應(yīng)對象中提取圖片的數(shù)據(jù),并計算其質(zhì)量指標(biāo)和BRISQUE分?jǐn)?shù),我們可以定義一個函數(shù)process_image,接受一個響應(yīng)對象和一個URL作為參數(shù),并返回一個包含圖片信息的字典。在這個函數(shù)中,我們需要使用“with”語句來管理文件和圖片對象的打開和關(guān)閉,以及使用“del”語句來釋放不再需要的變量,從而優(yōu)化內(nèi)存使用。具體來說:
我們使用PIL庫提供的方法來打開響應(yīng)對象中的圖片數(shù)據(jù),并將其轉(zhuǎn)換為RGBA格式。
我們使用os模塊提供的方法來創(chuàng)建一個名為“images”的文件夾,用于存儲下載的圖片。
我們使用datetime模塊提供的方法來獲取當(dāng)前的日期和時間,并將其轉(zhuǎn)換為字符串格式,作為圖片的文件名。
我們使用“with”語句來打開一個以日期和時間命名的文件,并將圖片數(shù)據(jù)寫入到文件中。
我們使用brisque模塊提供的方法來計算圖片的BRISQUE分?jǐn)?shù),并將其添加到字典中。
我們使用前面定義的get_image_quality_metrics函數(shù)來計算圖片的其他質(zhì)量指標(biāo),并將其添加到字典中。
我們使用“del”語句來刪除不再需要的變量,如響應(yīng)對象、圖片對象等。
我們返回包含圖片信息的字典。
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. 使用線程池來處理多個網(wǎng)站的圖片抓取任務(wù)
為了提高程序的效率和并發(fā)性,我們可以使用線程池來處理多個網(wǎng)站的圖片抓取任務(wù),并將處理結(jié)果保存到文件中。我們可以使用concurrent.futures模塊提供的方法來創(chuàng)建一個線程池對象,并使用submit方法來提交每個網(wǎng)站的圖片抓取任務(wù)。具體來說:
我們創(chuàng)建一個名為“websites”的列表,用于存儲需要抓取圖片的網(wǎng)站的URL。
我們創(chuàng)建一個名為“results”的列表,用于存儲每個網(wǎng)站的圖片抓取結(jié)果。
我們使用“with”語句來創(chuàng)建一個線程池對象,并設(shè)置其最大線程數(shù)為10。
我們遍歷每個網(wǎng)站的URL,并使用submit方法來提交一個圖片抓取任務(wù),傳入send_request函數(shù)和URL作為參數(shù),并將返回的future對象添加到results列表中。
我們遍歷results列表中的每個future對象,并使用result方法來獲取其結(jié)果,即響應(yīng)對象。
我們判斷響應(yīng)對象是否為None,如果不為None,表示請求成功,我們則使用process_image函數(shù)來處理響應(yīng)對象,并將返回的圖片信息字典添加到results列表中;如果為None,表示請求失敗,我們則跳過該網(wǎng)站。
我們使用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)
結(jié)論
本文介紹了如何使用Python進(jìn)行大規(guī)模的圖像抓取,并提供了一些優(yōu)化內(nèi)存使用的方法和技巧,以及如何計算和評估圖片的質(zhì)量指標(biāo)。我們使用requests庫來發(fā)送GET請求到圖片的URL,并使用代理服務(wù)器和隨機(jī)選擇的請求頭部來避免被網(wǎng)站屏蔽或限制。我們使用PIL庫和cv2庫來處理圖片數(shù)據(jù),并使用brisque模塊和自定義的函數(shù)來計算圖片的質(zhì)量指標(biāo)和BRISQUE分?jǐn)?shù)。我們使用logging模塊來記錄程序的運(yùn)行日志,并使用pickle模塊來將處理結(jié)果保存到文件中。我們使用“with”語句來管理文件和圖片對象的打開和關(guān)閉,以及使用“del”語句來釋放不再需要的變量,從而優(yōu)化內(nèi)存使用。我們使用concurrent.futures模塊來創(chuàng)建一個線程池,并使用submit方法來提交每個網(wǎng)站的圖片抓取任務(wù),從而提高程序的效率和并發(fā)性。通過這些方法和技巧,我們可以實現(xiàn)一個高效、穩(wěn)定、可擴(kuò)展的大規(guī)模圖像抓取程序。