Travail avec des données binaires

Le pilote mssql-python prend en charge binary, varbinary, et image les types de données pour Microsoft SQL.

Microsoft SQL stocke les données binaires dans ces types de colonnes :

Type Description Taille maximale
binary(n) Données binaires à longueur fixe. 8 000 octets
varbinary(n) Données binaires de longueur variable. 8 000 octets
varbinary(max) Données binaires volumineuses. 2 Go
image Anciennes données binaires volumineuses (déconseillées). 2 Go

Le pilote mssql-python retourne les données binaires sous forme d’objets Pythonbytes.

Quand stocker le binaire dans la base de données par rapport au système de fichiers :

  • Stockez dans la base de données lorsque les fichiers sont petits (moins de 1 Mo), que la cohérence transactionnelle avec d’autres données est importante, ou que vous devez sauvegarder les données et les fichiers ensemble.
  • Stockez dans le système de fichiers (ou Stockage Blob Azure) lorsque les fichiers sont volumineux (plus de 1 Mo), que vous avez besoin d’une livraison CDN, ou que vous devez servir les fichiers directement aux clients sans aller-retour dans la base de données.
  • Pour un compromis, la fonctionnalité FILESTREAM de Microsoft SQL stocke les données dans le système de fichiers avec une cohérence transactionnelle.

Insérer des données binaires

Passez des objets Python bytes en tant que paramètres ; le pilote les associe à des colonnes varbinary.

Insérer directement les octets

Créez un objet Python bytes et passez-le comme paramètre de requête :

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

# Create a table to hold binary documents
cursor.execute("""
    CREATE TABLE #Documents (
        ID INT IDENTITY PRIMARY KEY,
        Name NVARCHAR(255),
        Content VARBINARY(MAX),
        Size INT NULL,
        ContentHash VARBINARY(32) NULL
    )
""")

# Binary data as bytes
binary_data = b'\x00\x01\x02\x03\x04\x05'

cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "sample.bin", "content": binary_data}
)
conn.commit()

Insérer à partir du fichier

Lisez un fichier en mode binaire et insérez son contenu :

def insert_file(cursor, file_path: str, name: str):
    """Insert a file as binary data."""
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": content, "size": len(content)}
    )

insert_file(cursor, "image.png", "profile_picture.png")
conn.commit()

Insérer fichier image

Stockez les fichiers images avec des métadonnées en détectant les types MIME à partir des extensions de fichiers :

# Create a table to hold images
cursor.execute("""
    CREATE TABLE #Images (
        ID INT IDENTITY PRIMARY KEY,
        FileName NVARCHAR(255),
        FileSize INT NULL,
        ContentType NVARCHAR(100) NULL,
        Width INT NULL,
        Height INT NULL,
        ImageData VARBINARY(MAX),
        Description NVARCHAR(MAX) NULL
    )
""")

def insert_image(cursor, image_path: str, description: str):
    """Insert an image into the database."""
    import os
    
    with open(image_path, "rb") as f:
        image_data = f.read()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, FileSize, ContentType, ImageData, Description)
        VALUES (%(filename)s, %(filesize)s, %(content_type)s, %(data)s, %(desc)s)
    """, {
        "filename": os.path.basename(image_path),
        "filesize": len(image_data),
        "content_type": get_content_type(image_path),
        "data": image_data,
        "desc": description
    })

def get_content_type(path: str) -> str:
    """Determine MIME type from file extension."""
    ext = path.lower().split(".")[-1]
    types = {
        "png": "image/png",
        "jpg": "image/jpeg",
        "jpeg": "image/jpeg",
        "gif": "image/gif",
        "pdf": "application/pdf",
    }
    return types.get(ext, "application/octet-stream")

Extraction de données binaires

Le pilote retourne les valeurs de colonnes varbinaire et binaire sous forme d’objets Pythonbytes.

Récupérer la colonne binaire

Interrogez les colonnes binaires et inspectez l’objet retourné bytes :

cursor.execute(
    "SELECT LargePhotoFileName, LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
    {"id": 70}
)
row = cursor.fetchone()

# LargePhoto is bytes
print(type(row.LargePhoto))  # <class 'bytes'>
print(len(row.LargePhoto))   # Number of bytes

Enregistrer dans le fichier

Récupérer des données binaires et les écrire sur le disque :

def save_photo(cursor, photo_id: int, output_path: str):
    """Retrieve binary data and save to file."""
    cursor.execute(
        "SELECT LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
        {"id": photo_id}
    )
    row = cursor.fetchone()
    
    if row and row.LargePhoto:
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        print(f"Saved {len(row.LargePhoto)} bytes to {output_path}")
    else:
        print("Photo not found or empty")

save_photo(cursor, 70, "output.gif")

Exporter les lignes binaires vers les fichiers

Exportez plusieurs lignes binaires vers des fichiers locaux :

import os

def export_photos(cursor, output_dir: str):
    """Export product photos to a directory."""
    os.makedirs(output_dir, exist_ok=True)
    
    cursor.execute("""
        SELECT ProductPhotoID, LargePhotoFileName, LargePhoto
        FROM Production.ProductPhoto
        WHERE ProductPhotoID > 1
    """)
    
    count = 0
    for row in cursor:
        output_path = os.path.join(output_dir, f"{row.ProductPhotoID}_{row.LargePhotoFileName}")
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        count += 1
    
    print(f"Exported {count} photos to {output_dir}")

export_photos(cursor, "./photos")

Insérer des valeurs binaires NULL

Passez None pour insérer un NULL SQL dans une colonne binaire. Puisque #Documents est une table temporaire, déclarez les types des paramètres en utilisant d’abord setinputsizes() afin que le pilote lie Content en tant que varbinary:

cursor.setinputsizes([(mssql_python.SQL_WVARCHAR, 255, 0), (mssql_python.SQL_VARBINARY, 0, 0)])
cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "empty", "content": None}
)

Note

Le pilote déduit normalement les types de paramètres via SQLDescribeParam, mais il ne peut pas résoudre les métadonnées de type pour une table ou une variablede table temporaire. L’insertion de None dans une colonne binary ou varbinary d’un objet temporaire sans setinputsizes() génère ProgrammingError: Implicit conversion from data type varchar to varbinary(max) is not allowed. Passez une entrée par paramètre, dans l’ordre, et utilisez une constante de type SQL comme mssql_python.SQL_VARBINARY pour la colonne binaire. Pour une table classique (permanente), le driver résout automatiquement le type et vous pouvez passer None directement.

Grandes données binaires

Pour les fichiers de plus de quelques mégaoctets, insérez le contenu complet dans une seule écriture varbinaire(max ) au lieu de lire le fichier en petits blocs.

Diffuser de gros fichiers

Pour les gros fichiers, traiter par blocs :

def insert_large_file(conn, table: str, file_path: str, chunk_size: int = 8192):
    """Insert large file in chunks using updatetext-style approach."""
    # Validate table name to prevent SQL injection
    import re
    if not re.match(r'^#{0,2}[A-Za-z_][A-Za-z0-9_.]*$', table):
        raise ValueError(f"Invalid table name: {table}")

    cursor = conn.cursor()
    
    # Get file size
    import os
    file_size = os.path.getsize(file_path)
    
    # Insert initial row with empty binary
    cursor.execute(f"""
        INSERT INTO {table} (Name, Content, Size)
        VALUES (%(name)s, 0x, %(size)s);
        SELECT SCOPE_IDENTITY();
    """, {"name": os.path.basename(file_path), "size": file_size})
    
    row_id = cursor.fetchval()
    
    # For modern Microsoft SQL, better to use varbinary(max) and single insert
    # This example shows streaming approach
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(f"""
        UPDATE {table} SET Content = %(content)s WHERE ID = %(id)s
    """, {"content": content, "id": row_id})
    
    conn.commit()
    return row_id

Utilisez une copie en vrac pour les données binaires

Utilisez cette bulkcopy() méthode pour insérer efficacement plusieurs fichiers binaires en une seule opération.

Important

bulkcopy() charge les données via une connexion séparée au serveur, donc la table de destination doit déjà exister et être engagée et visible sur d’autres sessions. Si vous créez la table dans le même script avec l’autocommit désactivé, appelez conn.commit() avant bulkcopy(). Les tables temporaires locales (#name) ne sont pas prises en charge car elles sont privées à la session qui les a créées. Une destination non validée ou inaccessible provoque l’échec de bulkcopy() en raison d’un dépassement du délai d’attente Failed to retrieve destination metadata.

Par défaut, bulkcopy() chaque valeur d’une ligne est assignée à une colonne de tableau par position ordinale, donc chaque colonne doit avoir une valeur dans l’ordre. Lorsque la table a une colonne d’identité ou que vous ne remplissez que certaines colonnes, passez column_mappings pour nommer explicitement les colonnes cibles. Sinon, les valeurs se déplacent sur les mauvaises colonnes et bulkcopy() échouent.

def bulk_insert_files(conn, table: str, file_paths: list[str]):
    """Bulk insert multiple binary files."""
    import os

    data = []
    for path in file_paths:
        with open(path, "rb") as f:
            content = f.read()
        data.append((os.path.basename(path), content, len(content)))

    cursor = conn.cursor()
    result = cursor.bulkcopy(table, data, column_mappings=["Name", "Content", "Size"])
    conn.commit()
    return result["rows_copied"]

Opérations de données binaires

Utilisez la fonction de HASHBYTES Microsoft SQL pour comparer le contenu binaire sans chercher les valeurs complètes.

Comparer les données binaires

Trouvez des lignes binaires partageant un contenu identique en calculant et en comparant les hachages de contenu dans Microsoft SQL Server :

# Find rows with identical binary content by hash
cursor.execute("""
    SELECT LargePhotoFileName, HASHBYTES('SHA2_256', LargePhoto) AS PhotoHash
    FROM Production.ProductPhoto
    WHERE ProductPhotoID > 1
""")

hashes = {}
for row in cursor:
    hash_value = row.PhotoHash  # bytes
    if hash_value in hashes:
        print(f"Duplicate: {row.LargePhotoFileName} matches {hashes[hash_value]}")
    else:
        hashes[hash_value] = row.LargePhotoFileName

Calculer le hachage en Python

Calculez les hachages SHA-256 en Python et stockez-les aux côtés des données binaires :

import hashlib

def insert_with_hash(cursor, name: str, content: bytes):
    """Insert binary data with computed hash."""
    content_hash = hashlib.sha256(content).digest()
    
    cursor.execute("""
        INSERT INTO #Documents (Name, Content, ContentHash)
        VALUES (%(name)s, %(content)s, %(hash)s)
    """, {"name": name, "content": content, "hash": content_hash})

Encodage et décodage des données binaires

Convertir les données binaires vers et depuis des chaînes encodées en base64 :

import base64

# Store base64-encoded string
def insert_base64(cursor, name: str, base64_data: str):
    """Insert base64-encoded data as binary."""
    binary_data = base64.b64decode(base64_data)
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
        {"name": name, "content": binary_data}
    )

# Retrieve as base64
def get_as_base64(cursor, doc_id: int) -> str:
    """Retrieve binary data as base64 string."""
    cursor.execute("SELECT Content FROM #Documents WHERE ID = %(id)s", {"id": doc_id})
    row = cursor.fetchone()
    return base64.b64encode(row.Content).decode("utf-8")

Travail avec des formats binaires spécifiques

Ces exemples montrent comment valider et insérer des formats de fichiers courants avant de les stocker.

Documents PDF

Vérifiez les signatures des fichiers PDF avant de les insérer :

def insert_pdf(cursor, pdf_path: str, title: str):
    """Insert a PDF document."""
    with open(pdf_path, "rb") as f:
        pdf_data = f.read()
    
    # Verify it's a PDF (magic bytes)
    if not pdf_data.startswith(b'%PDF'):
        raise ValueError("Not a valid PDF file")
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(title)s, %(data)s)",
        {"title": title, "data": pdf_data}
    )

Images avec PIL/Pillow

Redimensionnez les images avec Pillow (PIL) avant de les stocker pour économiser de la place :

from PIL import Image
import io
import os

def insert_resized_image(cursor, image_path: str, max_size: tuple = (800, 600)):
    """Insert a resized image."""
    # Open and resize
    img = Image.open(image_path)
    img.thumbnail(max_size, Image.LANCZOS)
    
    # Convert to bytes
    buffer = io.BytesIO()
    img.save(buffer, format=img.format or "PNG")
    image_bytes = buffer.getvalue()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, Width, Height, ImageData)
        VALUES (%(name)s, %(width)s, %(height)s, %(data)s)
    """, {
        "name": os.path.basename(image_path),
        "width": img.width,
        "height": img.height,
        "data": image_bytes
    })

def get_image_as_pil(cursor, image_id: int) -> Image.Image:
    """Retrieve image as PIL Image object."""
    cursor.execute("SELECT ImageData FROM #Images WHERE ID = %(id)s", {"id": image_id})
    row = cursor.fetchone()
    return Image.open(io.BytesIO(row.ImageData))

Données compressées

Réduisez le stockage en compressant les données binaires avec gzip avant insertion :

import gzip

def insert_compressed(cursor, name: str, data: bytes):
    """Insert data with gzip compression."""
    compressed = gzip.compress(data)
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": compressed, "size": len(data)}
    )

def get_decompressed(cursor, data_id: int) -> bytes:
    """Retrieve and decompress data."""
    cursor.execute(
        "SELECT Content FROM #Documents WHERE ID = %(id)s",
        {"id": data_id}
    )
    row = cursor.fetchone()
    return gzip.decompress(row.Content)

Bonnes pratiques

Appliquez ces directives pour choisir le bon type de colonne et la bonne stratégie de stockage pour les données binaires.

Utilisez des types de colonnes appropriés

Choisissez le type de colonne en fonction des caractéristiques de vos données :

Pour différents cas d’usage binaires, sélectionnez le type de données Microsoft SQL approprié :

-- For small fixed-size binary (for example, hashes, UUIDs)
binary(32)      -- SHA-256 hash

-- For variable-size binary up to 8KB (for example, thumbnails, small icons)
varbinary(8000)

-- For large binary data (for example, documents, images)
varbinary(max)  -- Up to 2GB

Considérez FILESTREAM pour les fichiers volumineux

Pour les gros fichiers (plus de 1 Mo), considérez la fonctionnalité FILESTREAM SQL de Microsoft, qui stocke les données dans le système de fichiers. FILESTREAM nécessite une configuration côté serveur avant utilisation :

# FILESTREAM-enabled databases store large binaries more efficiently
# Access is still through normal queries but storage is file-based
large_binary_data = b"\x25\x50\x44\x46" + b"\x00" * 100  # sample data

cursor.execute("""
    CREATE TABLE ##FileStreamDemo (Name NVARCHAR(100), Document VARBINARY(MAX))
""")
cursor.execute("""
    INSERT INTO ##FileStreamDemo (Name, Document)
    VALUES (%(name)s, %(content)s)
""", {"name": "large_doc.pdf", "content": large_binary_data})

cursor.execute("SELECT Name, DATALENGTH(Document) AS DocSize FROM ##FileStreamDemo")
row = cursor.fetchone()
print(f"{row.Name}: {row.DocSize} bytes")

Valider les données binaires

Validez les types de fichiers en vérifiant les octets magiques (signatures de fichiers) avant de stocker des données binaires :

def insert_safe_image(cursor, name: str, data: bytes):
    """Insert image with validation."""
    # Check file signatures (magic bytes)
    signatures = {
        b'\x89PNG': 'image/png',
        b'\xff\xd8\xff': 'image/jpeg',
        b'GIF87a': 'image/gif',
        b'GIF89a': 'image/gif',
    }
    
    content_type = None
    for sig, mime in signatures.items():
        if data.startswith(sig):
            content_type = mime
            break
    
    if content_type is None:
        raise ValueError("Unknown or unsupported image format")
    
    cursor.execute("""
        INSERT INTO #Images (FileName, ContentType, ImageData)
        VALUES (%(name)s, %(type)s, %(data)s)
    """, {"name": name, "type": content_type, "data": data})