Boto3 para descargar todos los archivos de un S3 Bucket

83

Estoy usando boto3 para obtener archivos del depósito s3. Necesito una funcionalidad similar comoaws s3 sync

Mi código actual es

#!/usr/bin/python
import boto3
s3=boto3.client('s3')
list=s3.list_objects(Bucket='my_bucket_name')['Contents']
for key in list:
    s3.download_file('my_bucket_name', key['Key'], key['Key'])

Esto funciona bien, siempre que el depósito solo tenga archivos. Si hay una carpeta dentro del depósito, está arrojando un error

Traceback (most recent call last):
  File "./test", line 6, in <module>
    s3.download_file('my_bucket_name', key['Key'], key['Key'])
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/inject.py", line 58, in download_file
    extra_args=ExtraArgs, callback=Callback)
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/transfer.py", line 651, in download_file
    extra_args, callback)
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/transfer.py", line 666, in _download_file
    self._get_object(bucket, key, filename, extra_args, callback)
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/transfer.py", line 690, in _get_object
    extra_args, callback)
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/transfer.py", line 707, in _do_get_object
    with self._osutil.open(filename, 'wb') as f:
  File "/usr/local/lib/python2.7/dist-packages/boto3/s3/transfer.py", line 323, in open
    return open(filename, mode)
IOError: [Errno 2] No such file or directory: 'my_folder/.8Df54234'

¿Es esta una forma adecuada de descargar un bucket de s3 completo usando boto3. Cómo descargar carpetas.

Shan
fuente

Respuestas:

35

Cuando se trabaja con depósitos que tienen más de 1000 objetos, es necesario implementar una solución que utilice NextContinuationTokenconjuntos secuenciales de, como máximo, 1000 claves. Esta solución primero compila una lista de objetos, luego crea iterativamente los directorios especificados y descarga los objetos existentes.

import boto3
import os

s3_client = boto3.client('s3')

def download_dir(prefix, local, bucket, client=s3_client):
    """
    params:
    - prefix: pattern to match in s3
    - local: local path to folder in which to place files
    - bucket: s3 bucket with target contents
    - client: initialized s3 client object
    """
    keys = []
    dirs = []
    next_token = ''
    base_kwargs = {
        'Bucket':bucket,
        'Prefix':prefix,
    }
    while next_token is not None:
        kwargs = base_kwargs.copy()
        if next_token != '':
            kwargs.update({'ContinuationToken': next_token})
        results = client.list_objects_v2(**kwargs)
        contents = results.get('Contents')
        for i in contents:
            k = i.get('Key')
            if k[-1] != '/':
                keys.append(k)
            else:
                dirs.append(k)
        next_token = results.get('NextContinuationToken')
    for d in dirs:
        dest_pathname = os.path.join(local, d)
        if not os.path.exists(os.path.dirname(dest_pathname)):
            os.makedirs(os.path.dirname(dest_pathname))
    for k in keys:
        dest_pathname = os.path.join(local, k)
        if not os.path.exists(os.path.dirname(dest_pathname)):
            os.makedirs(os.path.dirname(dest_pathname))
        client.download_file(bucket, k, dest_pathname)
Grant Langseth
fuente
cambiando esto a una respuesta aceptada ya que maneja un caso de uso más amplio. Gracias Grant
Shan
mi código entra en un bucle infinito enwhile next_token is not None:
gpd
@gpd esto no debería suceder ya que el cliente boto3 devolverá una página sin el NextContinuationToken cuando haya llegado a la última página, saliendo de la instrucción while. Si pega la última respuesta que obtiene al usar la API boto3 (lo que sea que esté almacenado en la variable de respuesta), creo que será más claro lo que está sucediendo en su caso específico. Intente imprimir la variable 'resultados' solo para probar. Supongo que le ha dado un objeto de prefijo que no coincide con ningún contenido de su depósito. ¿Revisaste eso?
Grant Langseth
Tenga en cuenta que necesitaría cambios menores para que funcione con Digital Ocean. como se explica aquí
David D.
1
Al usar este código, obtengo este error: el objeto 'NoneType' no es iterable: TypeError
NJones
76

Tengo las mismas necesidades y creé la siguiente función que descarga los archivos de forma recursiva.

Los directorios se crean localmente solo si contienen archivos.

import boto3
import os

def download_dir(client, resource, dist, local='/tmp', bucket='your_bucket'):
    paginator = client.get_paginator('list_objects')
    for result in paginator.paginate(Bucket=bucket, Delimiter='/', Prefix=dist):
        if result.get('CommonPrefixes') is not None:
            for subdir in result.get('CommonPrefixes'):
                download_dir(client, resource, subdir.get('Prefix'), local, bucket)
        for file in result.get('Contents', []):
            dest_pathname = os.path.join(local, file.get('Key'))
            if not os.path.exists(os.path.dirname(dest_pathname)):
                os.makedirs(os.path.dirname(dest_pathname))
            resource.meta.client.download_file(bucket, file.get('Key'), dest_pathname)

La función se llama de esa manera:

def _start():
    client = boto3.client('s3')
    resource = boto3.resource('s3')
    download_dir(client, resource, 'clientconf/', '/tmp', bucket='my-bucket')
glefait
fuente
6
No creo que necesites crear un recurso y un cliente. Creo que un cliente siempre está disponible en el recurso. Puedes usar resource.meta.client.
TheHerk
2
Creo que debería ser "download_dir (client, resource, subdir.get ('Prefix'), local, bucket )"
rm999
6
Recibí un mensaje, OSError: [Errno 21] Is a directoryasí que envié la llamada a download_file if not file.get('Key').endswith('/')para resolverlo. Gracias @glefait y @Shan
user336828
5
¿No hay un equivalente del comando aws-cli aws s3 syncdisponible en la biblioteca boto3?
greperror
8
¿Qué hay distaquí?
Rob Rose
49

Amazon S3 no tiene carpetas / directorios. Es una estructura de archivo plana .

Para mantener la apariencia de los directorios, los nombres de las rutas se almacenan como parte de la clave del objeto (nombre de archivo). Por ejemplo:

  • images/foo.jpg

En este caso, la clave completa es images/foo.jpg, en lugar de solo foo.jpg.

Sospecho que su problema es que botoestá devolviendo un archivo llamado my_folder/.8Df54234y está intentando guardarlo en el sistema de archivos local. Sin embargo, su sistema de archivos local interpreta la my_folder/parte como un nombre de directorio y ese directorio no existe en su sistema de archivos local .

Puede truncar el nombre del archivo para guardar solo la .8Df54234parte, o tendría que crear los directorios necesarios antes de escribir archivos. Tenga en cuenta que podrían ser directorios anidados de varios niveles.

Una forma más sencilla sería utilizar la AWS Command-Line Interface (CLI) , que hará todo este trabajo por usted, por ejemplo:

aws s3 cp --recursive s3://my_bucket_name local_folder

También hay una syncopción que solo copiará archivos nuevos y modificados.

John Rotenstein
fuente
1
@j Lo entiendo. Pero necesitaba que se creara la carpeta, automáticamente como aws s3 sync. ¿Es posible en boto3.
Shan
4
Debería incluir la creación de un directorio como parte de su código Python. Si la clave contiene un directorio (por ejemplo foo/bar.txt), usted sería responsable de crear el directorio ( foo) antes de llamar s3.download_file. No es una capacidad automática de boto.
John Rotenstein
Aquí, el contenido del depósito S3 es dinámico, por lo que tengo que verificar s3.list_objects(Bucket='my_bucket_name')['Contents']y filtrar las claves de carpeta y crearlas.
Shan
2
Después de jugar con Boto3 por un tiempo, el comando AWS CLI que se enumera aquí es definitivamente la forma más fácil de hacer esto.
AdjunctProfessorFalcon
1
@Ben Inicie una nueva pregunta en lugar de hacer una pregunta como comentario sobre una pregunta anterior (2015).
John Rotenstein
43
import os
import boto3

#initiate s3 resource
s3 = boto3.resource('s3')

# select bucket
my_bucket = s3.Bucket('my_bucket_name')

# download file into current directory
for s3_object in my_bucket.objects.all():
    # Need to split s3_object.key into path and file name, else it will give error file not found.
    path, filename = os.path.split(s3_object.key)
    my_bucket.download_file(s3_object.key, filename)
Tushar Niras
fuente
3
Limpio y simple, ¿alguna razón para no usar esto? Es mucho más comprensible que todas las demás soluciones. Las colecciones parecen hacer muchas cosas por ti en segundo plano.
Joost
3
Supongo que primero debe crear todas las subcarpetas para que esto funcione correctamente.
rpanai
2
Este código colocará todo en el directorio de salida de nivel superior independientemente de cuán profundamente anidado esté en S3. Y si varios archivos tienen el mismo nombre en diferentes directorios, pisotearán uno con otro. Creo que necesitas una línea más:, os.makedirs(path)y luego el destino de descarga debería ser object.key.
Scott Smith
13

Actualmente estoy logrando la tarea, usando lo siguiente

#!/usr/bin/python
import boto3
s3=boto3.client('s3')
list=s3.list_objects(Bucket='bucket')['Contents']
for s3_key in list:
    s3_object = s3_key['Key']
    if not s3_object.endswith("/"):
        s3.download_file('bucket', s3_object, s3_object)
    else:
        import os
        if not os.path.exists(s3_object):
            os.makedirs(s3_object)

Aunque hace el trabajo, no estoy seguro de que sea bueno hacerlo de esta manera. Lo dejo aquí para ayudar a otros usuarios y obtener más respuestas, con una mejor manera de lograrlo.

Shan
fuente
9

Más vale tarde que nunca :) La respuesta anterior con paginator es realmente buena. Sin embargo, es recursivo y puede terminar alcanzando los límites de recursividad de Python. Aquí hay un enfoque alternativo, con un par de comprobaciones adicionales.

import os
import errno
import boto3


def assert_dir_exists(path):
    """
    Checks if directory tree in path exists. If not it created them.
    :param path: the path to check if it exists
    """
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise


def download_dir(client, bucket, path, target):
    """
    Downloads recursively the given S3 path to the target directory.
    :param client: S3 client to use.
    :param bucket: the name of the bucket to download from
    :param path: The S3 directory to download.
    :param target: the local directory to download the files to.
    """

    # Handle missing / at end of prefix
    if not path.endswith('/'):
        path += '/'

    paginator = client.get_paginator('list_objects_v2')
    for result in paginator.paginate(Bucket=bucket, Prefix=path):
        # Download each file individually
        for key in result['Contents']:
            # Calculate relative path
            rel_path = key['Key'][len(path):]
            # Skip paths ending in /
            if not key['Key'].endswith('/'):
                local_file_path = os.path.join(target, rel_path)
                # Make sure directories exist
                local_file_dir = os.path.dirname(local_file_path)
                assert_dir_exists(local_file_dir)
                client.download_file(bucket, key['Key'], local_file_path)


client = boto3.client('s3')

download_dir(client, 'bucket-name', 'path/to/data', 'downloads')
ifoukarakis
fuente
1
Tengo KeyError: 'Contents'. ruta de entrada '/arch/R/storeincomelogs/, ruta completa /arch/R/storeincomelogs/201901/01/xxx.parquet.
Mithril
3

Tengo una solución para esto que ejecuta la AWS CLI en el mismo proceso.

Instalar awsclicomo python lib:

pip install awscli

Luego defina esta función:

from awscli.clidriver import create_clidriver

def aws_cli(*cmd):
    old_env = dict(os.environ)
    try:

        # Environment
        env = os.environ.copy()
        env['LC_CTYPE'] = u'en_US.UTF'
        os.environ.update(env)

        # Run awscli in the same process
        exit_code = create_clidriver().main(*cmd)

        # Deal with problems
        if exit_code > 0:
            raise RuntimeError('AWS CLI exited with code {}'.format(exit_code))
    finally:
        os.environ.clear()
        os.environ.update(old_env)

Ejecutar:

aws_cli('s3', 'sync', '/path/to/source', 's3://bucket/destination', '--delete')
mattalxndr
fuente
Usé la misma idea pero sin usar el synccomando, y más bien simplemente ejecutando el comando aws s3 cp s3://{bucket}/{folder} {local_folder} --recursive. Tiempos reducidos de minutos (casi 1h) a literalmente segundos
acaruci
Estoy usando este código pero tengo un problema donde se muestran todos los registros de depuración. Tengo esto declarado globalmente: logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.WARNING) logger = logging.getLogger()y solo quiero que los registros se generen desde la raíz. ¿Algunas ideas?
April Polubiec
1

Es una muy mala idea obtener todos los archivos de una sola vez, debería hacerlo por lotes.

Una implementación que utilizo para buscar una carpeta en particular (directorio) de S3 es,

def get_directory(directory_path, download_path, exclude_file_names):
    # prepare session
    session = Session(aws_access_key_id, aws_secret_access_key, region_name)

    # get instances for resource and bucket
    resource = session.resource('s3')
    bucket = resource.Bucket(bucket_name)

    for s3_key in self.client.list_objects(Bucket=self.bucket_name, Prefix=directory_path)['Contents']:
        s3_object = s3_key['Key']
        if s3_object not in exclude_file_names:
            bucket.download_file(file_path, download_path + str(s3_object.split('/')[-1])

y aún así, si desea obtener todo el depósito, úselo a través de CIL como @John Rotenstein mencionó a continuación,

aws s3 cp --recursive s3://bucket_name download_path
Ganatra
fuente
0
for objs in my_bucket.objects.all():
    print(objs.key)
    path='/tmp/'+os.sep.join(objs.key.split(os.sep)[:-1])
    try:
        if not os.path.exists(path):
            os.makedirs(path)
        my_bucket.download_file(objs.key, '/tmp/'+objs.key)
    except FileExistsError as fe:                          
        print(objs.key+' exists')

Este código descargará el contenido en el /tmp/directorio. Si lo desea, puede cambiar el directorio.

Rajesh Rajendran
fuente
0

Si desea llamar a un script de bash usando Python, aquí hay un método simple para cargar un archivo desde una carpeta en el depósito S3 a una carpeta local (en una máquina Linux):

import boto3
import subprocess
import os

###TOEDIT###
my_bucket_name = "your_my_bucket_name"
bucket_folder_name = "your_bucket_folder_name"
local_folder_path = "your_local_folder_path"
###TOEDIT###

# 1.Load thes list of files existing in the bucket folder
FILES_NAMES = []
s3 = boto3.resource('s3')
my_bucket = s3.Bucket('{}'.format(my_bucket_name))
for object_summary in my_bucket.objects.filter(Prefix="{}/".format(bucket_folder_name)):
#     print(object_summary.key)
    FILES_NAMES.append(object_summary.key)

# 2.List only new files that do not exist in local folder (to not copy everything!)
new_filenames = list(set(FILES_NAMES )-set(os.listdir(local_folder_path)))

# 3.Time to load files in your destination folder 
for new_filename in new_filenames:
    upload_S3files_CMD = """aws s3 cp s3://{}/{}/{} {}""".format(my_bucket_name,bucket_folder_name,new_filename ,local_folder_path)

    subprocess_call = subprocess.call([upload_S3files_CMD], shell=True)
    if subprocess_call != 0:
        print("ALERT: loading files not working correctly, please re-check new loaded files")
HazimoRa3d
fuente
0

Obtuve el requisito similar y obtuve ayuda al leer algunas de las soluciones anteriores y en otros sitios web, se me ocurrió el siguiente script, solo quería compartir si podría ayudar a alguien.

from boto3.session import Session
import os

def sync_s3_folder(access_key_id,secret_access_key,bucket_name,folder,destination_path):    
    session = Session(aws_access_key_id=access_key_id,aws_secret_access_key=secret_access_key)
    s3 = session.resource('s3')
    your_bucket = s3.Bucket(bucket_name)
    for s3_file in your_bucket.objects.all():
        if folder in s3_file.key:
            file=os.path.join(destination_path,s3_file.key.replace('/','\\'))
            if not os.path.exists(os.path.dirname(file)):
                os.makedirs(os.path.dirname(file))
            your_bucket.download_file(s3_file.key,file)
sync_s3_folder(access_key_id,secret_access_key,bucket_name,folder,destination_path)
Kranti
fuente
0

Reposicionando la respuesta de @glefait con una condición if al final para evitar el error 20 del sistema operativo. La primera clave que obtiene es el nombre de la carpeta, que no se puede escribir en la ruta de destino.

def download_dir(client, resource, dist, local='/tmp', bucket='your_bucket'):
    paginator = client.get_paginator('list_objects')
    for result in paginator.paginate(Bucket=bucket, Delimiter='/', Prefix=dist):
        if result.get('CommonPrefixes') is not None:
            for subdir in result.get('CommonPrefixes'):
                download_dir(client, resource, subdir.get('Prefix'), local, bucket)
        for file in result.get('Contents', []):
            print("Content: ",result)
            dest_pathname = os.path.join(local, file.get('Key'))
            print("Dest path: ",dest_pathname)
            if not os.path.exists(os.path.dirname(dest_pathname)):
                print("here last if")
                os.makedirs(os.path.dirname(dest_pathname))
            print("else file key: ", file.get('Key'))
            if not file.get('Key') == dist:
                print("Key not equal? ",file.get('Key'))
                resource.meta.client.download_file(bucket, file.get('Key'), dest_pathname)enter code here
vinay
fuente
0

Me he estado encontrando con este problema durante un tiempo y con todos los foros diferentes que he visitado no he visto un resumen completo de lo que funciona. Entonces, seguí adelante y tomé todas las piezas (agregué algunas cosas por mi cuenta) y ¡he creado un S3 Downloader completo de extremo a extremo!

Esto no solo descargará archivos automáticamente, sino que si los archivos S3 están en subdirectorios, los creará en el almacenamiento local. En el caso de mi aplicación, necesito establecer permisos y propietarios, así que también lo agregué (se puede comentar si no es necesario).

Esto ha sido probado y funciona en un entorno Docker (K8) pero he agregado las variables ambientales en el script en caso de que quiera probarlo / ejecutarlo localmente.

Espero que esto ayude a alguien en su búsqueda por encontrar la automatización de descargas de S3. También agradezco cualquier consejo, información, etc. sobre cómo se puede optimizar mejor si es necesario.

#!/usr/bin/python3
import gc
import logging
import os
import signal
import sys
import time
from datetime import datetime

import boto
from boto.exception import S3ResponseError
from pythonjsonlogger import jsonlogger

formatter = jsonlogger.JsonFormatter('%(message)%(levelname)%(name)%(asctime)%(filename)%(lineno)%(funcName)')

json_handler_out = logging.StreamHandler()
json_handler_out.setFormatter(formatter)

#Manual Testing Variables If Needed
#os.environ["DOWNLOAD_LOCATION_PATH"] = "some_path"
#os.environ["BUCKET_NAME"] = "some_bucket"
#os.environ["AWS_ACCESS_KEY"] = "some_access_key"
#os.environ["AWS_SECRET_KEY"] = "some_secret"
#os.environ["LOG_LEVEL_SELECTOR"] = "DEBUG, INFO, or ERROR"

#Setting Log Level Test
logger = logging.getLogger('json')
logger.addHandler(json_handler_out)
logger_levels = {
    'ERROR' : logging.ERROR,
    'INFO' : logging.INFO,
    'DEBUG' : logging.DEBUG
}
logger_level_selector = os.environ["LOG_LEVEL_SELECTOR"]
logger.setLevel(logger_level_selector)

#Getting Date/Time
now = datetime.now()
logger.info("Current date and time : ")
logger.info(now.strftime("%Y-%m-%d %H:%M:%S"))

#Establishing S3 Variables and Download Location
download_location_path = os.environ["DOWNLOAD_LOCATION_PATH"]
bucket_name = os.environ["BUCKET_NAME"]
aws_access_key_id = os.environ["AWS_ACCESS_KEY"]
aws_access_secret_key = os.environ["AWS_SECRET_KEY"]
logger.debug("Bucket: %s" % bucket_name)
logger.debug("Key: %s" % aws_access_key_id)
logger.debug("Secret: %s" % aws_access_secret_key)
logger.debug("Download location path: %s" % download_location_path)

#Creating Download Directory
if not os.path.exists(download_location_path):
    logger.info("Making download directory")
    os.makedirs(download_location_path)

#Signal Hooks are fun
class GracefulKiller:
    kill_now = False
    def __init__(self):
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)
    def exit_gracefully(self, signum, frame):
        self.kill_now = True

#Downloading from S3 Bucket
def download_s3_bucket():
    conn = boto.connect_s3(aws_access_key_id, aws_access_secret_key)
    logger.debug("Connection established: ")
    bucket = conn.get_bucket(bucket_name)
    logger.debug("Bucket: %s" % str(bucket))
    bucket_list = bucket.list()
#    logger.info("Number of items to download: {0}".format(len(bucket_list)))

    for s3_item in bucket_list:
        key_string = str(s3_item.key)
        logger.debug("S3 Bucket Item to download: %s" % key_string)
        s3_path = download_location_path + "/" + key_string
        logger.debug("Downloading to: %s" % s3_path)
        local_dir = os.path.dirname(s3_path)

        if not os.path.exists(local_dir):
            logger.info("Local directory doesn't exist, creating it... %s" % local_dir)
            os.makedirs(local_dir)
            logger.info("Updating local directory permissions to %s" % local_dir)
#Comment or Uncomment Permissions based on Local Usage
            os.chmod(local_dir, 0o775)
            os.chown(local_dir, 60001, 60001)
        logger.debug("Local directory for download: %s" % local_dir)
        try:
            logger.info("Downloading File: %s" % key_string)
            s3_item.get_contents_to_filename(s3_path)
            logger.info("Successfully downloaded File: %s" % s3_path)
            #Updating Permissions
            logger.info("Updating Permissions for %s" % str(s3_path))
#Comment or Uncomment Permissions based on Local Usage
            os.chmod(s3_path, 0o664)
            os.chown(s3_path, 60001, 60001)
        except (OSError, S3ResponseError) as e:
            logger.error("Fatal error in s3_item.get_contents_to_filename", exc_info=True)
            # logger.error("Exception in file download from S3: {}".format(e))
            continue
        logger.info("Deleting %s from S3 Bucket" % str(s3_item.key))
        s3_item.delete()

def main():
    killer = GracefulKiller()
    while not killer.kill_now:
        logger.info("Checking for new files on S3 to download...")
        download_s3_bucket()
        logger.info("Done checking for new files, will check in 120s...")
        gc.collect()
        sys.stdout.flush()
        time.sleep(120)
if __name__ == '__main__':
    main()
Camarada 35
fuente
0

Desde AWS S3 Docs (¿Cómo uso carpetas en un bucket de S3?):

En Amazon S3, los depósitos y los objetos son los recursos principales y los objetos se almacenan en depósitos. Amazon S3 tiene una estructura plana en lugar de una jerarquía como la que vería en un sistema de archivos. Sin embargo, en aras de la simplicidad organizativa, la consola de Amazon S3 admite el concepto de carpeta como medio para agrupar objetos. Amazon S3 hace esto mediante el uso de un prefijo de nombre compartido para los objetos (es decir, los objetos tienen nombres que comienzan con una cadena común). Los nombres de objeto también se conocen como nombres de clave.

Por ejemplo, puede crear una carpeta en la consola llamada fotos y almacenar un objeto llamado myphoto.jpg en ella. A continuación, el objeto se almacena con el nombre de clave photos / myphoto.jpg, donde photos / es el prefijo.

Para descargar todos los archivos de "mybucket" en el directorio actual respetando la estructura de directorio emulado del depósito (creando las carpetas del depósito si aún no existen localmente):

import boto3
import os

bucket_name = "mybucket"
s3 = boto3.client("s3")
objects = s3.list_objects(Bucket = bucket_name)["Contents"]
for s3_object in objects:
    s3_key = s3_object["Key"]
    path, filename = os.path.split(s3_key)
    if len(path) != 0 and not os.path.exists(path):
        os.makedirs(path)
    if not s3_key.endswith("/"):
        download_to = path + '/' + filename if path else filename
        s3.download_file(bucket_name, s3_key, download_to)
Daria
fuente
Sería mejor si pudieras incluir alguna explicación de tu código.
johan
1
@johan, ¡gracias por los comentarios! Agregué una explicación relevante
Daria