摘要
圖像抓取是一種常見(jiàn)的網(wǎng)絡(luò)爬蟲(chóng)技術(shù)花嘶,用于從網(wǎng)頁(yè)上下載圖片并保存到本地文件夾中秀菱。然而,當(dāng)需要抓取的圖片數(shù)量很大時(shí)戚嗅,可能會(huì)出現(xiàn)內(nèi)存不足的錯(cuò)誤雨涛,導(dǎo)致程序崩潰枢舶。本文介紹了如何使用Python進(jìn)行大規(guī)模的圖像抓取,并提供了一些優(yōu)化內(nèi)存使用的方法和技巧替久,以及如何計(jì)算和評(píng)估圖片的質(zhì)量指標(biāo)凉泄。
正文
1. 導(dǎo)入必要的庫(kù)和模塊
為了實(shí)現(xiàn)圖像抓取的功能,我們需要導(dǎo)入一些必要的庫(kù)和模塊蚯根,如pickle后众、logging、datetime等颅拦。其中蒂誉,pickle模塊用于序列化和反序列化對(duì)象,方便我們將處理結(jié)果保存到文件中距帅;logging模塊用于記錄程序的運(yùn)行日志右锨,方便我們調(diào)試和監(jiān)控程序的狀態(tài);datetime模塊用于獲取和處理日期和時(shí)間相關(guān)的信息碌秸,方便我們?cè)O(shè)置請(qǐng)求頭部和日志格式等绍移。
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è)置一個(gè)日志記錄器讥电,用于將日志信息輸出到文件中蹂窖。我們可以使用logging模塊提供的方法來(lái)創(chuàng)建一個(gè)名為“image-scraper”的日志記錄器,并設(shè)置其日志級(jí)別為INFO恩敌。然后恼策,我們可以創(chuàng)建一個(gè)文件處理器,用于將日志信息寫(xiě)入到指定的文件中潮剪,并設(shè)置其日志格式為包含日志級(jí)別涣楷、線程名、時(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. 定義計(jì)算圖片質(zhì)量指標(biāo)的函數(shù)
為了評(píng)估圖片的質(zhì)量碳褒,我們需要計(jì)算一些圖片質(zhì)量指標(biāo),如亮度看疗、清晰度沙峻、對(duì)比度、色彩度等两芳。我們可以定義一個(gè)函數(shù)get_image_quality_metrics摔寨,接受一個(gè)包含圖片數(shù)據(jù)的響應(yīng)對(duì)象作為參數(shù),并返回一個(gè)包含各種質(zhì)量指標(biāo)的字典怖辆。在這個(gè)函數(shù)中是复,我們首先使用PIL庫(kù)和numpy庫(kù)將圖片數(shù)據(jù)轉(zhuǎn)換為數(shù)組形式删顶,并使用cv2庫(kù)和skimage庫(kù)對(duì)圖片進(jìn)行處理和計(jì)算。具體來(lái)說(shuō):
計(jì)算亮度:我們將圖片轉(zhuǎn)換為灰度圖淑廊,并計(jì)算其像素值的平均值逗余。
計(jì)算清晰度:我們使用拉普拉斯算子對(duì)灰度圖進(jìn)行邊緣檢測(cè),并計(jì)算其方差值季惩。
計(jì)算對(duì)比度:我們使用均方根對(duì)比度的公式录粱,計(jì)算灰度圖像素值與其平均值的差的平方的平均值的平方根。
計(jì)算噪聲:我們使用高斯濾波或中值絕對(duì)偏差(MAD)的方法画拾,計(jì)算圖片的方差值啥繁。
計(jì)算飽和度:我們將圖片轉(zhuǎn)換為HSV顏色空間,并計(jì)算其飽和度通道的平均值碾阁。
計(jì)算色彩度:我們將圖片轉(zhuǎn)換為L(zhǎng)AB顏色空間输虱,并計(jì)算其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ā)送請(qǐng)求的函數(shù)
為了從網(wǎng)頁(yè)上下載圖片,我們需要發(fā)送GET請(qǐng)求到圖片的URL蚕钦,并獲取響應(yīng)對(duì)象亭病。我們可以定義一個(gè)函數(shù)send_request,接受一個(gè)URL作為參數(shù)嘶居,并返回一個(gè)響應(yīng)對(duì)象罪帖。在這個(gè)函數(shù)中,我們需要處理一些可能出現(xiàn)的異常和錯(cuò)誤邮屁,如超時(shí)整袁、狀態(tài)碼不為200、429等佑吝。為了避免被網(wǎng)站屏蔽或限制坐昙,我們需要使用代理服務(wù)器和隨機(jī)選擇的請(qǐng)求頭部。具體來(lái)說(shuō):
我們使用requests庫(kù)提供的方法來(lái)創(chuàng)建一個(gè)代理服務(wù)器對(duì)象芋忿,使用億牛云提供的代理服務(wù)器信息炸客。
我們使用一個(gè)while循環(huán)來(lái)重試請(qǐng)求,設(shè)置一個(gè)最大重試次數(shù)和一個(gè)初始退避延遲時(shí)間戈钢。
我們從headers模塊中隨機(jī)選擇一個(gè)請(qǐng)求頭部痹仙,并將其添加到請(qǐng)求中。
我們使用try-except語(yǔ)句來(lái)捕獲可能出現(xiàn)的異常和錯(cuò)誤殉了,并根據(jù)不同的情況進(jìn)行處理:?
如果出現(xiàn)超時(shí)錯(cuò)誤开仰,我們記錄日志信息,并增加重試次數(shù)和退避延遲時(shí)間。
如果出現(xiàn)狀態(tài)碼不為200的錯(cuò)誤抖所,我們記錄日志信息梨州,并根據(jù)狀態(tài)碼進(jìn)行處理:?
如果狀態(tài)碼為429痕囱,表示請(qǐng)求過(guò)于頻繁田轧,我們需要等待一段時(shí)間后再重試,我們可以使用time模塊提供的sleep方法來(lái)暫停程序運(yùn)行鞍恢,并設(shè)置一個(gè)默認(rèn)的睡眠時(shí)間傻粘。
如果狀態(tài)碼為403或404,表示請(qǐng)求被拒絕或資源不存在帮掉,我們可以直接跳出
如果狀態(tài)碼為其他值弦悉,表示請(qǐng)求出現(xiàn)其他錯(cuò)誤,我們可以直接拋出異常蟆炊,并記錄日志信息稽莉。?
如果沒(méi)有出現(xiàn)異常或錯(cuò)誤涩搓,我們返回響應(yīng)對(duì)象污秆,并記錄日志信息。
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)
? ? # 億牛云 爬蟲(chóng)代理加強(qiáng)版
? ? proxyHost = "www.16yun.cn"
? ? proxyPort = "31111"
? ? # 代理驗(yàn)證信息
? ? 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)對(duì)象中提取圖片的數(shù)據(jù)昧甘,并計(jì)算其質(zhì)量指標(biāo)和BRISQUE分?jǐn)?shù)良拼,我們可以定義一個(gè)函數(shù)process_image,接受一個(gè)響應(yīng)對(duì)象和一個(gè)URL作為參數(shù)充边,并返回一個(gè)包含圖片信息的字典庸推。在這個(gè)函數(shù)中,我們需要使用“with”語(yǔ)句來(lái)管理文件和圖片對(duì)象的打開(kāi)和關(guān)閉浇冰,以及使用“del”語(yǔ)句來(lái)釋放不再需要的變量贬媒,從而優(yōu)化內(nèi)存使用。具體來(lái)說(shuō):
我們使用PIL庫(kù)提供的方法來(lái)打開(kāi)響應(yīng)對(duì)象中的圖片數(shù)據(jù)肘习,并將其轉(zhuǎn)換為RGBA格式际乘。
我們使用os模塊提供的方法來(lái)創(chuàng)建一個(gè)名為“images”的文件夾,用于存儲(chǔ)下載的圖片井厌。
我們使用datetime模塊提供的方法來(lái)獲取當(dāng)前的日期和時(shí)間蚓庭,并將其轉(zhuǎn)換為字符串格式,作為圖片的文件名仅仆。
我們使用“with”語(yǔ)句來(lái)打開(kāi)一個(gè)以日期和時(shí)間命名的文件器赞,并將圖片數(shù)據(jù)寫(xiě)入到文件中。
我們使用brisque模塊提供的方法來(lái)計(jì)算圖片的BRISQUE分?jǐn)?shù)墓拜,并將其添加到字典中港柜。
我們使用前面定義的get_image_quality_metrics函數(shù)來(lái)計(jì)算圖片的其他質(zhì)量指標(biāo),并將其添加到字典中。
我們使用“del”語(yǔ)句來(lái)刪除不再需要的變量夏醉,如響應(yīng)對(duì)象爽锥、圖片對(duì)象等。
我們返回包含圖片信息的字典畔柔。
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. 使用線程池來(lái)處理多個(gè)網(wǎng)站的圖片抓取任務(wù)
為了提高程序的效率和并發(fā)性氯夷,我們可以使用線程池來(lái)處理多個(gè)網(wǎng)站的圖片抓取任務(wù),并將處理結(jié)果保存到文件中靶擦。我們可以使用concurrent.futures模塊提供的方法來(lái)創(chuàng)建一個(gè)線程池對(duì)象腮考,并使用submit方法來(lái)提交每個(gè)網(wǎng)站的圖片抓取任務(wù)。具體來(lái)說(shuō):
我們創(chuàng)建一個(gè)名為“websites”的列表玄捕,用于存儲(chǔ)需要抓取圖片的網(wǎng)站的URL踩蔚。
我們創(chuàng)建一個(gè)名為“results”的列表,用于存儲(chǔ)每個(gè)網(wǎng)站的圖片抓取結(jié)果枚粘。
我們使用“with”語(yǔ)句來(lái)創(chuàng)建一個(gè)線程池對(duì)象馅闽,并設(shè)置其最大線程數(shù)為10。
我們遍歷每個(gè)網(wǎng)站的URL馍迄,并使用submit方法來(lái)提交一個(gè)圖片抓取任務(wù)福也,傳入send_request函數(shù)和URL作為參數(shù),并將返回的future對(duì)象添加到results列表中柬姚。
我們遍歷results列表中的每個(gè)future對(duì)象拟杉,并使用result方法來(lái)獲取其結(jié)果,即響應(yīng)對(duì)象量承。
我們判斷響應(yīng)對(duì)象是否為None搬设,如果不為None,表示請(qǐng)求成功撕捍,我們則使用process_image函數(shù)來(lái)處理響應(yīng)對(duì)象拿穴,并將返回的圖片信息字典添加到results列表中;如果為None忧风,表示請(qǐng)求失敗默色,我們則跳過(guò)該網(wǎng)站。
我們使用pickle模塊提供的方法來(lái)將results列表序列化并保存到一個(gè)名為“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)存使用的方法和技巧,以及如何計(jì)算和評(píng)估圖片的質(zhì)量指標(biāo)缘厢。我們使用requests庫(kù)來(lái)發(fā)送GET請(qǐng)求到圖片的URL吃度,并使用代理服務(wù)器和隨機(jī)選擇的請(qǐng)求頭部來(lái)避免被網(wǎng)站屏蔽或限制。我們使用PIL庫(kù)和cv2庫(kù)來(lái)處理圖片數(shù)據(jù)贴硫,并使用brisque模塊和自定義的函數(shù)來(lái)計(jì)算圖片的質(zhì)量指標(biāo)和BRISQUE分?jǐn)?shù)椿每。我們使用logging模塊來(lái)記錄程序的運(yùn)行日志伊者,并使用pickle模塊來(lái)將處理結(jié)果保存到文件中。我們使用“with”語(yǔ)句來(lái)管理文件和圖片對(duì)象的打開(kāi)和關(guān)閉间护,以及使用“del”語(yǔ)句來(lái)釋放不再需要的變量亦渗,從而優(yōu)化內(nèi)存使用。我們使用concurrent.futures模塊來(lái)創(chuàng)建一個(gè)線程池汁尺,并使用submit方法來(lái)提交每個(gè)網(wǎng)站的圖片抓取任務(wù)法精,從而提高程序的效率和并發(fā)性。通過(guò)這些方法和技巧均函,我們可以實(shí)現(xiàn)一個(gè)高效亿虽、穩(wěn)定菱涤、可擴(kuò)展的大規(guī)模圖像抓取程序苞也。