在本篇博客中,我们将探讨如何使用 Scrapy 框架下载图片,并详细解释两种下载图片的方式。
一、项目结构
首先,我们假设项目结构如下:
biantu_down_pic/
├── biantu_down_pic/
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders/
│ ├── __init__.py
│ └── down_pic.py
├── log_file.log
└── scrapy.cfg
二、settings.py 配置
在 settings.py
中,我们需要进行一些基本配置:
# Scrapy settings for biantu_down_pic project
BOT_NAME = 'biantu_down_pic'
SPIDER_MODULES = ['biantu_down_pic.spiders']
NEWSPIDER_MODULE = 'biantu_down_pic.spiders'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'WARNING'
LOG_FILE = './log_file.log'
USER_AGENTS_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0',
# 更多 User Agent 可添加
]
DOWNLOADER_MIDDLEWARES = {
'biantu_down_pic.middlewares.BiantuDownPicDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
'biantu_down_pic.pipelines.BiantuDownPicSavePipeline': 300, # 先下载图片再保存到数据库
'biantu_down_pic.pipelines.BiantuDownPicPipeline': 301,
'biantu_down_pic.pipelines.MysqlPipeline': 302,
}
# 图片地址配置
IMAGES_STORE = 'D:/ruoyi/pic.baidu.com/uploadPath'
# MySQL 配置
MYSQL_HOST = "localhost"
MYSQL_USER = "drawing-bed"
MYSQL_PASSWORD = "HBt5J7itWJEXe"
MYSQL_DBNAME = "drawing-bed"
MYSQL_PORT = 3306
三、爬虫文件(down_pic.py)
在 down_pic.py
文件中,我们定义了爬虫类 DownPicSpider
,用于从网页 https://pic.baidu.com/
下载图片。
代码示例
import imghdr
import json
import os
import re
from datetime import datetime
import scrapy
from biantu_down_pic.items import BiantuDownPicItem
from biantu_down_pic.settings import IMAGES_STORE
from biantu_down_pic.pipelines import sanitize_filename
class DownPicSpider(scrapy.Spider):
name = 'down_pic'
start_urls = ['https://pic.baidu.com/']
def __init__(self, token=None, map_json=None, *args, **kwargs):
super(DownPicSpider, self).__init__(*args, **kwargs)
self.token = token
self.map_json = json.loads(map_json) if map_json else {}
def parse(self, response, **kwargs):
# 从 map_json 获取必要的信息
key_id = self.map_json.get('id')
url = self.map_json.get('url')
title = self.map_json.get("title")
# 设置 cookies
cookies = {i.split('=')[0]: i.split('=')[1] for i in self.token.split('; ')}
# 发起请求到详情页
yield scrapy.Request(
url,
cookies=cookies,
callback=self.parse_detail,
meta={'key_id': key_id, 'url': url, 'cookies': cookies, 'title': title}
)
def parse_detail(self, response, **kwargs):
# 从 URL 中提取图片 ID
pic_id = response.url.split('/')[-1].split('.')[0]
cookies = response.meta['cookies']
# 构建请求源图片 URL 的地址
source_url = f'https://pic.baidu.com/e/extend/downpic.php?id={pic_id}'
# 获取缩略图 URL
thumbnail_url = response.xpath('//*[@id="img"]/img/@src').extract_first()
thumbnail_url = response.urljoin(thumbnail_url)
# 将缩略图 URL 添加到 meta 中
response.meta['thumbnail_url'] = thumbnail_url
# 发送请求获取源图片 URL
yield scrapy.Request(
source_url,
cookies=cookies,
callback=self.parse_source_url,
meta=response.meta
)
def parse_source_url(self, response, **kwargs):
# 解析响应中的图片下载链接
pic_data = response.json()
pic_url = response.urljoin(pic_data['pic'])
cookies = response.meta['cookies']
# 发送请求下载图片
yield scrapy.Request(
pic_url,
cookies=cookies,
callback=self.save_image,
meta=response.meta
)
def save_image(self, response, **kwargs):
# 动态生成文件保存路径
current_time = datetime.now()
date_path = current_time.strftime('%Y/%m/%d')
download_dir = os.path.join(IMAGES_STORE, 'pic', date_path).replace('\\', '/')
# 生成清理后的文件名
title = response.meta['title']
pic_name = sanitize_filename(title)
pic_name = self.clean_filename(pic_name)
# 根据响应头获取文件扩展名
content_type = response.headers.get('Content-Type', b'').decode('utf-8')
extension = self.get_extension(content_type, response.body)
if not pic_name.lower().endswith(extension):
pic_name += extension
file_path = os.path.join(download_dir, pic_name).replace('\\', '/')
# 确保下载目录存在
os.makedirs(download_dir, exist_ok=True)
# 保存图片
with open(file_path, 'wb') as f:
f.write(response.body)
# 构建 Item 并返回
item = BiantuDownPicItem()
item['id'] = response.meta['key_id']
item['title'] = response.meta['title']
item['title_min'] = response.meta['title'] + '_min.jpg'
item['url'] = response.meta['url']
item['min_url'] = response.meta['thumbnail_url']
item['download_path'] = download_dir.replace(IMAGES_STORE, '')
item['max_path'] = file_path.replace(IMAGES_STORE, '/profile')
yield item
@staticmethod
def clean_filename(filename):
"""清理文件名,去除无效字符"""
return re.sub(r'[<>:"/\\|?*]', '', filename)
@staticmethod
def get_extension(content_type, body):
"""根据内容类型或文件内容获取扩展名"""
if 'image/jpeg' in content_type:
return '.jpg'
elif 'image/png' in content_type:
return '.png'
elif 'image/jpg' in content_type:
return '.jpg'
else:
return '.' + imghdr.what(None, body)
四、管道文件(pipelines.py)
在 pipelines.py
文件中,我们定义了两个管道类,用于处理和保存下载的图片。
代码示例
import logging
import re
import scrapy
from scrapy.pipelines.images import ImagesPipeline
log = logging.getLogger(__name__)
class BiantuDownPicPipeline:
"""基础的 Item 处理管道,仅打印 Item"""
def process_item(self, item, spider):
print(item)
return item
def sanitize_filename(filename):
"""移除文件名中的非法字符,替换为 'X'"""
return re.sub(r'[<>:"/\\|?*]', 'X', filename)
class BiantuDownPicSavePipeline(ImagesPipeline):
"""自定义图片下载和保存管道"""
def get_media_requests(self, item, info):
"""发送请求去下载缩略图"""
min_url = item['min_url']
print('1. 发送请求去下载图片, min_url:', min_url)
return scrapy.Request(url=min_url)
def file_path(self, request, response=None, info=None, *, item=None):
"""定义图片的存储路径"""
print('2. 图片的存储路径')
filename = sanitize_filename(item['title_min'])
download_path = f"{item['download_path']}/{filename}"
return download_path
def item_completed(self, results, item, info):
"""更新 Item,添加缩略图的存储路径"""
print(f'3. 对 Item 进行更新, result: {results}')
if results:
ok, res = results[0]
if ok:
item['min_path'] = '/profile' + res["path"]
return item
class MysqlPipeline(object):
"""MySQL 数据库管道,暂时保留现状"""
def __init__(self, host, user, password, database, port):
self.host = host
self.user = user
self.password = password
self.database = database
self.port = port
@classmethod
def from_crawler(cls, crawler):
return cls(
host=crawler.settings.get("MYSQL_HOST"),
user=crawler.settings['MYSQL_USER'],
password=crawler.settings['MYSQL_PASSWORD'],
database=crawler.settings['MYSQL_DBNAME'],
port=crawler.settings['MYSQL_PORT']
)
def open_spider(self, spider):
"""在爬虫启动时创建数据库连接"""
self.conn = pymysql.connect(
host=self.host,
user=self.user,
password=self.password,
database=self.database,
charset='utf8',
port=self.port
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
"""处理 Item 并插入数据库"""
dt = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# SQL 语句暂时保留,待更新
# insert_sql = """
# INSERT INTO biz_pic (
# url, title, source_url, category_name, size, volume, create_by, create_time
# ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
# """
# try:
# self.cursor.execute(insert_sql, (
# item['url'],
# item['title'],
# item['source_url'],
# item['category_name'],
# item['size'],
# item['volume'],
# 'admin',
# dt
# ))
# self.conn.commit()
# except Exception as e:
# log.error(f"插入数据出错,{e}")
def close_spider(self, spider):
"""在爬虫关闭时关闭数据库连接"""
self.cursor.close()
self.conn.close()
log.warning("爬取数据结束===============================>end")
五、两种下载方式的区别和使用场景
在这篇文章中,我们介绍了使用 Scrapy 下载图片的两种不同方式:直接链接下载和通过下载链接下载。接下来,我们将详细介绍这两种方式的区别以及它们适用的场景。
1. 直接链接下载
直接链接下载是指图片的 URL 本身就指向图片文件,可以直接通过 URL 获取图片内容。
示例代码
在 pipelines.py
中的 BiantuDownPicSavePipeline
类中,我们使用直接链接下载图片:
class BiantuDownPicSavePipeline(ImagesPipeline):
"""自定义图片下载和保存管道"""
def get_media_requests(self, item, info):
"""发送请求去下载缩略图"""
min_url = item['min_url']
print('1. 发送请求去下载图片, min_url:', min_url)
return scrapy.Request(url=min_url)
def file_path(self, request, response=None, info=None, *, item=None):
"""定义图片的存储路径"""
print('2. 图片的存储路径')
filename = sanitize_filename(item['title_min'])
download_path = f"{item['download_path']}/{filename}"
return download_path
def item_completed(self, results, item, info):
"""更新 Item,添加缩略图的存储路径"""
print(f'3. 对 Item 进行更新, result: {results}')
if results:
ok, res = results[0]
if ok:
item['min_path'] = '/profile' + res["path"]
return item
使用场景
- 简单且常见:适用于绝大多数公开访问的图片文件。
- 性能较好:直接通过 URL 获取图片,无需额外的请求和处理。
2. 通过下载链接下载
通过下载链接下载是指图片的 URL 需要通过一个中间链接来获取实际的图片文件。这种情况通常用于需要验证或有时效性的下载链接。
示例代码
在 down_pic.py
中的 DownPicSpider
类中,我们使用通过下载链接下载图片:
class DownPicSpider(scrapy.Spider):
name = 'down_pic'
start_urls = ['https://pic.baidu.com/']
def parse_detail(self, response, **kwargs):
# 从 URL 中提取图片 ID
pic_id = response.url.split('/')[-1].split('.')[0]
cookies = response.meta['cookies']
# 构建请求源图片 URL 的地址
source_url = f'https://pic.baidu.com/e/extend/downpic.php?id={pic_id}'
# 获取缩略图 URL
thumbnail_url = response.xpath('//*[@id="img"]/img/@src').extract_first()
thumbnail_url = response.urljoin(thumbnail_url)
# 将缩略图 URL 添加到 meta 中
response.meta['thumbnail_url'] = thumbnail_url
# 发送请求获取源图片 URL
yield scrapy.Request(
source_url,
cookies=cookies,
callback=self.parse_source_url,
meta=response.meta
)
def parse_source_url(self, response, **kwargs):
# 解析响应中的图片下载链接
pic_data = response.json()
pic_url = response.urljoin(pic_data['pic'])
cookies = response.meta['cookies']
# 发送请求下载图片
yield scrapy.Request(
pic_url,
cookies=cookies,
callback=self.save_image,
meta=response.meta
)
使用场景
- 需要身份验证或时效性的下载链接:适用于需要通过特定请求获取下载链接的情况。
- 安全性要求较高:适用于下载需要权限控制的图片。
总结
- 直接链接下载:适用于绝大多数公开访问的图片文件,操作简单且性能较好。
- 通过下载链接下载:适用于需要通过验证或时效性链接获取图片的情况,安全性更高但操作稍复杂。