使用 Python `sqlite3` 在 SQLite 中插入图片
目录
- 引言:图片存储策略与 BLOB 简介
- 1.1 为什么要在数据库中存储图片?(优点与缺点)
- 1.2 BLOB (Binary Large Object) 数据类型
- SQLite 中的 BLOB
- 2.1 BLOB 类型特性
- 2.2 Python
bytes对象与 SQLite BLOB 的映射
- 在 Python 中插入图片到 SQLite
- 3.1 准备数据库表结构
- 3.2 读取图片文件为二进制数据
- 3.3 执行
INSERT语句 - Python 代码实现:插入图片
- 在 Python 中从 SQLite 检索图片
- 4.1 执行
SELECT语句获取 BLOB 数据 - 4.2 将二进制数据写入图片文件
- Python 代码实现:检索图片
- 4.1 执行
- 最佳实践与注意事项
- 5.1 参数化查询 (防止 SQL 注入)
- 5.2 使用
with语句管理连接 - 5.3 事务管理 (
COMMIT与ROLLBACK) - 5.4 错误处理 (
try-except) - 5.5 性能、备份与可伸缩性考量 (重要)
- 何时适合存储 BLOB
- 何时建议存储文件路径
- 5.6 文件路径与二进制数据转换
- 综合代码示例
- 总结
1. 引言:图片存储策略与 BLOB 简介
在开发应用程序时,如何存储图片、视频或其他文件是一个常见的挑战。通常有两种主要策略:将文件存储在文件系统(例如服务器硬盘)中,并在数据库中只存储文件的路径;或者直接将文件的二进制数据存储在数据库中。
1.1 为什么要在数据库中存储图片?(优点与缺点)
优点:
- 数据完整性与事务性:图片数据与数据库中的其他结构化数据一起存储,可以利用数据库的事务特性确保数据一致性。例如,插入一条记录及其对应的图片可以在一个事务中完成,要么都成功,要么都失败。
- 管理简化:无需单独管理文件系统上的文件,所有数据都集中在数据库中,简化了备份、恢复和同步操作。
- 便携性:数据库文件(如 SQLite 的
.db文件)包含所有数据,便于移动和部署。 - 访问控制:可以利用数据库的权限管理机制来控制对图片的访问。
缺点:
- 数据库文件膨胀:图片文件通常较大,将大量图片存储在数据库中会导致数据库文件迅速增大,影响数据库的性能、备份和恢复时间。
- 性能影响:从数据库读取和写入大尺寸 BLOB 数据可能比直接从文件系统操作更慢,尤其是在处理大量并发请求时。
- 缓存效率低:文件系统通常有更好的操作系统级文件缓存优化。
- 工具限制:许多数据库管理工具对大尺寸 BLOB 的显示和导出不够友好。
结论:通常情况下,推荐将大尺寸图片存储在文件系统中,并在数据库中存储其路径。 只有在特定场景下(例如:小尺寸图标、缩略图、需要高度数据完整性且文件数量不多、或者应用程序设计要求单文件部署时),直接在数据库中存储图片才是一个可行的选择。
1.2 BLOB (Binary Large Object) 数据类型
BLOB 是一种数据类型,用于存储二进制数据,例如图片、音频、视频或任何其他文件。数据库管理系统(DBMS)不对 BLOB 数据的内容进行解释,只将其作为一串字节进行存储和检索。
2. SQLite 中的 BLOB
2.1 BLOB 类型特性
SQLite 支持 BLOB 数据类型。当你将数据声明为 BLOB 类型时,SQLite 会按原样存储字节,不对其进行任何编码转换。这意味着你可以存储任何二进制文件,而无需担心数据损坏或格式问题。
2.2 Python bytes 对象与 SQLite BLOB 的映射
在 Python 中,二进制数据通常表示为 bytes 对象(例如 b'...')。Python 的 sqlite3 模块能够无缝地将 Python bytes 对象映射到 SQLite 的 BLOB 类型,反之亦然。当从数据库中读取 BLOB 列时,sqlite3 会将其作为 bytes 对象返回。
3. 在 Python 中插入图片到 SQLite
3.1 准备数据库表结构
首先,我们需要一个包含 BLOB 类型的列的表来存储图片数据。
CREATE TABLE images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
data BLOB NOT NULL,
mimetype TEXT -- 可选,存储MIME类型便于检索后识别文件类型
);
3.2 读取图片文件为二进制数据
使用 Python 的内置 open() 函数以二进制读取模式 ('rb') 打开图片文件,然后使用 read() 方法读取其全部内容。
with open("path/to/your/image.png", 'rb') as f:
image_data = f.read() # image_data 将是 bytes 类型
3.3 执行 INSERT 语句
使用参数化查询将图片名称、二进制数据和可选的 MIME 类型插入到数据库中。
INSERT INTO images (name, data, mimetype) VALUES (?, ?, ?);
Python 代码实现:插入图片
import sqlite3
import os
import mimetypes # 用于猜测MIME类型
DB_FILE = "image_database.db"
# 用于测试的图片文件,请确保该文件存在或手动创建一个
TEST_IMAGE_PATH = "test_image.png"
def create_dummy_image(path=TEST_IMAGE_PATH):
"""创建一个简单的假图片文件用于测试"""
from PIL import Image # 需要安装 Pillow 库: pip install Pillow
if not os.path.exists(path):
try:
img = Image.new('RGB', (60, 30), color = 'red')
img.save(path)
print(f"Created dummy image: {path}")
except ImportError:
print("Pillow library not found. Please install it ('pip install Pillow') or provide your own test_image.png.")
# 如果没有 Pillow,可以创建一个非常小的文本文件作为二进制数据
with open(path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\xda\xed\xc1\x01\x01\x00\x00\x00\xc2\xa0\xf7OM\x00\x00\x00\x00IEND\xaeB`\x82')
print(f"Created minimal PNG dummy image fallback: {path}")
def init_db():
"""初始化数据库,创建 images 表。"""
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
data BLOB NOT NULL,
mimetype TEXT
);
''')
conn.commit()
print(f"Database '{DB_FILE}' initialized and 'images' table created.")
def insert_image(image_path, db_file=DB_FILE):
"""
将指定的图片文件插入到数据库中。
"""
if not os.path.exists(image_path):
print(f"Error: Image file not found at {image_path}")
return None
image_name = os.path.basename(image_path)
mime_type, _ = mimetypes.guess_type(image_path) # 猜测MIME类型
print(f"\nInserting '{image_name}' (MIME: {mime_type}) into database...")
image_id = None
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
with open(image_path, 'rb') as f:
image_data = f.read() # 读取二进制数据
sql = "INSERT INTO images (name, data, mimetype) VALUES (?, ?, ?);"
cursor.execute(sql, (image_name, image_data, mime_type))
conn.commit()
image_id = cursor.lastrowid
print(f"Image '{image_name}' inserted successfully with ID: {image_id}")
except sqlite3.Error as e:
print(f"Failed to insert image '{image_name}': {e}")
except IOError as e:
print(f"File I/O error when reading image '{image_name}': {e}")
return image_id
4. 在 Python 中从 SQLite 检索图片
4.1 执行 SELECT 语句获取 BLOB 数据
从数据库中选择 data 列以获取二进制图片数据。你通常会使用 WHERE 子句来根据 id 或 name 等标识符检索特定的图片。
SELECT data, name, mimetype FROM images WHERE id = ?;
4.2 将二进制数据写入图片文件
将检索到的 bytes 对象使用 open(file_path, 'wb') 以二进制写入模式保存到新的文件。
Python 代码实现:检索图片
def retrieve_image(image_id, output_dir="retrieved_images", db_file=DB_FILE):
"""
从数据库中检索指定 ID 的图片,并保存到文件。
"""
os.makedirs(output_dir, exist_ok=True) # 确保输出目录存在
print(f"\nRetrieving image with ID {image_id} from database...")
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
sql = "SELECT data, name, mimetype FROM images WHERE id = ?;"
cursor.execute(sql, (image_id,))
result = cursor.fetchone()
if result:
image_data, image_name, mime_type = result
# 如果没有 mimetype,尝试从原始文件名推断扩展名
file_extension = mimetypes.guess_extension(mime_type) if mime_type else os.path.splitext(image_name)[1]
if not file_extension:
file_extension = '.bin' # 默认二进制扩展名
output_path = os.path.join(output_dir, f"retrieved_{image_id}_{image_name}{file_extension}")
with open(output_path, 'wb') as f:
f.write(image_data)
print(f"Image '{image_name}' (ID: {image_id}) retrieved and saved to: {output_path}")
return output_path
else:
print(f"No image found with ID: {image_id}")
return None
except sqlite3.Error as e:
print(f"Failed to retrieve image with ID {image_id}: {e}")
except IOError as e:
print(f"File I/O error when writing image: {e}")
return None
def get_image_info(image_id, db_file=DB_FILE):
"""
获取指定 ID 图片的元数据(不包括图片数据本身)。
"""
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
sql = "SELECT id, name, mimetype FROM images WHERE id = ?;"
cursor.execute(sql, (image_id,))
result = cursor.fetchone()
if result:
print(f"Image Info: ID={result[0]}, Name={result[1]}, MIME Type={result[2]}")
return result
else:
print(f"No image found with ID: {image_id}")
return None
except sqlite3.Error as e:
print(f"Error fetching image info: {e}")
return None
5. 最佳实践与注意事项
5.1 参数化查询 (防止 SQL 注入)
这是最重要的安全实践。 永远不要直接将用户输入或变量值拼接到 SQL 字符串中。对于 BLOB 数据,参数化查询是唯一安全且正确的方式。sqlite3 模块会自动处理二进制数据的转义。
5.2 使用 with 语句管理连接
sqlite3.connect() 对象是一个上下文管理器。使用 with 语句可以确保数据库连接在操作完成后自动关闭,即使发生错误也能正确释放资源。
5.3 事务管理 (COMMIT 与 ROLLBACK)
插入和更新 BLOB 数据是事务性的。
conn.commit():显式提交事务,将更改永久保存到数据库。conn.rollback():如果操作过程中发生错误,可以调用此方法回滚事务,撤销所有未提交的更改,使数据库恢复到操作前的状态。这对于维护数据完整性至关重要。
5.4 错误处理 (try-except)
始终使用 try...except sqlite3.Error 来捕获可能发生的数据库错误,并进行适当的处理,而不是让程序崩溃。同时,对于文件操作,也要捕获 IOError。
5.5 性能、备份与可伸缩性考量 (重要)
何时适合存储 BLOB:
- 图片文件尺寸非常小(如图标、缩略图)。
- 应用程序要求单文件部署,不想依赖外部文件系统。
- 对事务完整性有极高要求,图片与结构化数据必须严格同步。
- 图片数量有限。
何时建议存储文件路径:
- 图片文件尺寸较大(几百 KB 到几 MB 或更大)。
- 图片数量巨大。
- 需要高性能的图片检索或服务器端直接提供图片服务(如 Web 服务器)。
- 数据库备份/恢复速度是一个关键指标。
在这种情况下,数据库中存储图片路径 (TEXT类型) 和元数据(如图片名、描述、MIME 类型、文件大小)是更好的选择。
5.6 文件路径与二进制数据转换
- 读取文件到
bytes:with open(path, 'rb') as f: data = f.read() bytes写入文件:with open(path, 'wb') as f: f.write(data)
6. 综合代码示例
import sqlite3
import os
import mimetypes
import shutil
from PIL import Image # 需要安装 Pillow: pip install Pillow
# --- 配置 ---
DB_FILE = "gallery.db"
TEST_IMAGE_FOLDER = "test_images"
RETRIEVED_IMAGE_FOLDER = "retrieved_gallery"
# --- 辅助函数:创建测试图片 ---
def create_sample_images(num_images=3):
"""
创建一些用于测试的假图片文件。
"""
os.makedirs(TEST_IMAGE_FOLDER, exist_ok=True)
print(f"Creating {num_images} sample images in '{TEST_IMAGE_FOLDER}'...")
sample_images_paths = []
for i in range(num_images):
width, height = (100 + i * 10), (50 + i * 10)
color = (255 - i * 50, 100 + i * 30, 50 + i * 70) # 不同的颜色
img_path = os.path.join(TEST_IMAGE_FOLDER, f"sample_image_{i+1}.png")
try:
img = Image.new('RGB', (width, height), color=color)
img.save(img_path)
sample_images_paths.append(img_path)
except ImportError:
print("Pillow library not found. Cannot create sample images. Please install it ('pip install Pillow') or provide your own test images.")
# Fallback for minimal png
with open(img_path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\xda\xed\xc1\x01\x01\x00\x00\x00\xc2\xa0\xf7OM\x00\x00\x00\x00IEND\xaeB`\x82')
sample_images_paths.append(img_path)
print(f"Sample images created: {sample_images_paths}")
return sample_images_paths
# --- 数据库操作函数 ---
def init_database():
"""初始化数据库并创建图片表。"""
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
data BLOB NOT NULL,
mimetype TEXT
);
''')
conn.commit()
print(f"Database '{DB_FILE}' initialized and 'images' table created.")
def insert_image_into_db(image_path, db_file=DB_FILE):
"""
将指定的图片文件插入到数据库中。
返回插入图片的 ID。
"""
if not os.path.exists(image_path):
print(f"Error: Image file not found at {image_path}")
return None
image_name = os.path.basename(image_path)
mime_type, _ = mimetypes.guess_type(image_path)
if mime_type is None: # 无法猜测MIME类型时提供一个默认值
mime_type = "application/octet-stream"
print(f"Attempting to insert '{image_name}' (MIME: {mime_type})...")
image_id = None
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
with open(image_path, 'rb') as f:
image_data = f.read()
sql = "INSERT INTO images (name, data, mimetype) VALUES (?, ?, ?);"
cursor.execute(sql, (image_name, image_data, mime_type))
conn.commit()
image_id = cursor.lastrowid
print(f"Successfully inserted '{image_name}' with ID: {image_id}")
except sqlite3.Error as e:
print(f"Database error during insertion of '{image_name}': {e}")
except IOError as e:
print(f"File I/O error when reading image '{image_name}': {e}")
return image_id
def retrieve_and_save_image_from_db(image_id, output_folder=RETRIEVED_IMAGE_FOLDER, db_file=DB_FILE):
"""
从数据库中检索指定 ID 的图片,并保存到文件。
返回保存的图片路径。
"""
os.makedirs(output_folder, exist_ok=True)
print(f"Attempting to retrieve image with ID {image_id}...")
saved_path = None
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
sql = "SELECT data, name, mimetype FROM images WHERE id = ?;"
cursor.execute(sql, (image_id,))
result = cursor.fetchone()
if result:
image_data, original_name, mime_type = result
# 从MIME类型或原始文件名推断文件扩展名
extension = mimetypes.guess_extension(mime_type)
if not extension and '.' in original_name:
extension = os.path.splitext(original_name)[1]
if not extension:
extension = '.bin' # Fallback to generic binary extension
# 构建输出文件名,避免重复
base_name = os.path.splitext(original_name)[0]
output_filename = f"retrieved_{base_name}_id{image_id}{extension}"
output_path = os.path.join(output_folder, output_filename)
with open(output_path, 'wb') as f:
f.write(image_data)
print(f"Successfully retrieved and saved image '{original_name}' (ID: {image_id}) to: {output_path}")
saved_path = output_path
else:
print(f"No image found with ID: {image_id}")
except sqlite3.Error as e:
print(f"Database error during retrieval of image ID {image_id}: {e}")
except IOError as e:
print(f"File I/O error when writing retrieved image: {e}")
return saved_path
def list_all_images_info(db_file=DB_FILE):
"""
列出数据库中所有图片的元数据。
"""
print("\n--- Listing All Images in Database ---")
try:
with sqlite3.connect(db_file) as conn:
cursor = conn.cursor()
cursor.execute("SELECT id, name, mimetype FROM images;")
images_info = cursor.fetchall()
if not images_info:
print("No images found in the database.")
return []
for img_id, name, mimetype in images_info:
print(f" ID: {img_id}, Name: '{name}', MIME Type: '{mimetype}'")
return images_info
except sqlite3.Error as e:
print(f"Error listing images: {e}")
return []
def cleanup():
"""清理生成的文件和数据库。"""
print("\n--- Cleaning up ---")
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
print(f"Deleted database file: {DB_FILE}")
if os.path.exists(TEST_IMAGE_FOLDER):
shutil.rmtree(TEST_IMAGE_FOLDER)
print(f"Deleted test image folder: {TEST_IMAGE_FOLDER}")
if os.path.exists(RETRIEVED_IMAGE_FOLDER):
shutil.rmtree(RETRIEVED_IMAGE_FOLDER)
print(f"Deleted retrieved image folder: {RETRIEVED_IMAGE_FOLDER}")
# --- 主执行逻辑 ---
def main():
cleanup() # 每次运行前清理
init_database()
sample_image_paths = create_sample_images(3)
inserted_ids = []
for path in sample_image_paths:
img_id = insert_image_into_db(path)
if img_id:
inserted_ids.append(img_id)
list_all_images_info()
if inserted_ids:
# 检索第一个插入的图片
print(f"\nAttempting to retrieve the first inserted image (ID: {inserted_ids[0]})")
retrieve_and_save_image_from_db(inserted_ids[0])
# 尝试检索一个不存在的图片
print("\nAttempting to retrieve a non-existent image (ID: 999)")
retrieve_and_save_image_from_db(999)
# 检索所有插入的图片
print("\nRetrieving all inserted images...")
for img_id in inserted_ids:
retrieve_and_save_image_from_db(img_id)
else:
print("\nNo images were inserted to retrieve.")
print("\n--- Program finished ---")
# cleanup() # 如果需要保留生成的文件以供检查,可以注释掉此行
if __name__ == "__main__":
main()
7. 总结
为您详尽解析了在 Python 中使用 sqlite3 模块将图片作为 BLOB 数据存储到 SQLite 数据库的方法。
核心要点回顾:
- BLOB 类型: SQLite 支持 BLOB 类型来存储任何二进制数据。
- Python
bytes: Python 的bytes对象与 SQLite 的 BLOB 类型无缝兼容。 - 文件操作: 使用
open(file, 'rb')读取二进制数据,使用open(file, 'wb')写入二进制数据。 - 安全实践: 始终使用参数化查询来插入 BLOB 数据,以防止 SQL 注入。
- 资源管理: 强烈建议使用
with语句管理数据库连接和文件句柄。 - 事务性:
INSERT和UPDATE操作需要conn.commit()来保存更改。 - 性能与策略: 对于大尺寸或大量图片,通常更推荐在文件系统中存储图片并只在数据库中存储其路径,而不是直接存储 BLOB。BLOB 存储更适合小尺寸、有限数量的图片或特定单文件应用场景。
通过遵循这些指南,您将能够安全、高效地在 Python 应用程序中处理 SQLite 数据库中的图像或其他二进制数据。
更多推荐

所有评论(0)