Boto3 для завантаження всіх файлів із сегмента S3


85

Я використовую boto3 для отримання файлів з відра s3. Мені потрібна подібна функціональність, якaws s3 sync

Мій поточний код -

#!/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'])

Це працює нормально, якщо сегмент містить лише файли. Якщо папка присутня всередині відра, виникає помилка

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'

Це правильний спосіб завантажити повне відро s3 за допомогою boto3. Як завантажити папки.


Відповіді:


40

При роботі з сегментами, що мають понад 1000 об'єктів, необхідно реалізувати рішення, яке використовує NextContinuationTokenпослідовні набори, щонайбільше, 1000 ключів. Це рішення спочатку складає список об’єктів, потім ітеративно створює зазначені каталоги та завантажує існуючі об’єкти.

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)

змінивши цю на прийняту відповідь, оскільки вона обробляє ширший варіант використання. Подяка Гранту
Шань,

мій код переходить в нескінченний цикл приwhile next_token is not None:
GPD

@gpd цього не повинно статися, оскільки клієнт boto3 поверне сторінку без NextContinuationToken, коли вона дійде до останньої сторінки, виходячи з оператора while. Якщо ви вставите останню відповідь, отриману від використання API boto3 (все, що зберігається у змінній відповіді), тоді, я думаю, буде більш зрозуміло, що відбувається у вашому конкретному випадку. Спробуйте роздрукувати змінну 'результати' лише для тестування. Я припускаю, що ви дали об’єкт префікса, який не відповідає жодному вмісту вашого сегмента. Ви це перевірили?
Грант Лангсет

1
Зверніть увагу, що вам знадобляться незначні зміни, щоб це працювало з Digital Ocean. як пояснюється тут
Девід Д.

2
Використовуючи цей код , я отримую цю помилку: об'єкт «» NoneType НЕ ітерації: TypeError
NJones

76

Я маю ті самі потреби і створив наступну функцію, яка рекурсивно завантажує файли.

Каталоги створюються локально, лише якщо вони містять файли.

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)

Функція називається так:

def _start():
    client = boto3.client('s3')
    resource = boto3.resource('s3')
    download_dir(client, resource, 'clientconf/', '/tmp', bucket='my-bucket')

6
Я не думаю, що вам потрібно створювати ресурс і клієнта. Я вважаю, що клієнт завжди доступний на ресурсі. Ви можете просто використовувати resource.meta.client.
TheHerk

2
Я думаю, що це має бути "download_dir (client, resource, subdir.get ('Prefix'), local, bucket )"
rm999,

6
Я отримував, OSError: [Errno 21] Is a directoryтому я завершив виклик download_file, if not file.get('Key').endswith('/')щоб вирішити. Дякую @glefait та @Shan
user336828

5
Чи не існує еквівалента команди aws-cli aws s3 syncу бібліотеці boto3?
greperror

8
Що distтут?
Роб Роуз

49

Amazon S3 не має папок / каталогів. Це плоска файлова структура .

Для підтримання зовнішнього вигляду каталогів імена шляхів зберігаються як частина об'єктного ключа (ім'я файлу). Наприклад:

  • images/foo.jpg

У цьому випадку цілий Ключ є images/foo.jpg, а не просто foo.jpg.

Я підозрюю, що ваша проблема полягає у тому, що botoповертає викликаний файл my_folder/.8Df54234і намагається зберегти його в локальній файловій системі. Однак ваша локальна файлова система інтерпретує my_folder/частину як назву каталогу, і ця директорія не існує у вашій локальній файловій системі .

Ви можете або скоротити ім'я файлу, щоб зберегти лише .8Df54234частину, або вам доведеться створити необхідні каталоги перед написанням файлів. Зверніть увагу, що це можуть бути багаторівневі вкладені каталоги.

Більш простим способом буде використання інтерфейсу командного рядка AWS (CLI) , який зробить всю цю роботу за вас, наприклад:

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

Також є syncопція, яка копіюватиме лише нові та змінені файли.


1
@j Я це розумію. Але мені потрібно було створити папку, автоматично, як і aws s3 sync. Чи можливо це в boto3.
Шан

4
Вам доведеться включити створення каталогу як частину вашого коду Python. Якщо Ключ містить каталог (наприклад foo/bar.txt), ви несете відповідальність за створення каталогу ( foo) перед викликом s3.download_file. Це не автоматична можливість boto.
Джон Ротенштейн,

Тут вміст сегмента S3 динамічний, тому мені доводиться перевіряти s3.list_objects(Bucket='my_bucket_name')['Contents']та фільтрувати ключі папок та створювати їх.
Шан

2
Погравшись деякий час з Boto3, перелічена тут команда AWS CLI, безумовно, є найпростішим способом зробити це.
AdjunctProfessorFalcon

1
@Ben Будь ласка, почніть нове запитання, а не задавайте питання як коментар до старого (2015 р.) Питання.
Джон Ротенштейн

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)

3
Чисто і просто, будь-яка причина, чому б не використовувати це? Це набагато зрозуміліше за всі інші рішення. Здається, колекції роблять для вас багато речей у фоновому режимі.
Joost

3
Думаю, спочатку слід створити всі вкладені папки, щоб це працювало належним чином.
рпанай

2
Цей код помістить все у вихідний каталог верхнього рівня, незалежно від того, наскільки глибоко він вкладений у S3. І якщо декілька файлів мають однакові назви в різних каталогах, вони будуть тупотіти один одному. Я думаю, вам потрібен ще один рядок:, os.makedirs(path)і тоді призначення завантаження повинно бути object.key.
Скотт Сміт

13

Зараз я досягаю завдання, використовуючи наступне

#!/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)

Хоча, це робить роботу, я не впевнений, що це добре робити таким чином. Я залишаю це тут, щоб допомогти іншим користувачам та отримати подальші відповіді, з кращим способом досягнення цього


9

Краще пізно, ніж ніколи :) Попередня відповідь з пагінатором справді хороша. Однак це рекурсивно, і ви можете в кінцевому підсумку досягти меж рекурсії Python. Ось альтернативний підхід, з парою додаткових перевірок.

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')

1
Є KeyError: 'Contents'. вхідний шлях '/arch/R/storeincomelogs/, повний шлях /arch/R/storeincomelogs/201901/01/xxx.parquet.
Mithril

3

У мене є обхідний шлях для цього, який запускає AWS CLI в тому ж процесі.

Встановити awscliяк python lib:

pip install awscli

Потім визначте цю функцію:

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)

Для виконання:

aws_cli('s3', 'sync', '/path/to/source', 's3://bucket/destination', '--delete')

Я використовував ту саму ідею, але не використовуючи syncкоманду, а скоріше просто виконуючи команду aws s3 cp s3://{bucket}/{folder} {local_folder} --recursive. Час зменшився з хвилин (майже 1 год) буквально до секунд
acaruci

Я використовую цей код, але маю проблему, коли відображаються всі журнали налагодження. Я оголосив це глобально: logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.WARNING) logger = logging.getLogger()і хочу лише, щоб журнали виводилися з кореня. Будь-які ідеї?
Квітень Полубець

1

Дуже погана ідея отримувати всі файли за один раз, вам краще віддавати їх партіями.

Однією реалізацією, яку я використовую для отримання певної папки (каталогу) з S3, є,

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])

і все-таки, якщо ви хочете отримати весь сегмент, використовуйте його через CIL як @ John Rotenstein, згаданий нижче,

aws s3 cp --recursive s3://bucket_name download_path

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')

Цей код завантажить вміст у /tmp/каталог. Якщо ви хочете, ви можете змінити каталог.


0

Якщо ви хочете викликати скрипт bash за допомогою python, ось простий спосіб завантажити файл із папки у сегменті S3 у локальну папку (на машині 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")

0

Я отримав подібну вимогу і отримав допомогу, прочитавши декілька вищезазначених рішень та на інших веб-сайтах, я придумав нижче наведений сценарій, просто хотів поділитися, чи може це комусь допомогти.

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)

0

Повторне розміщення відповіді @glefait із умовою if в кінці, щоб уникнути помилки os 20. Першим ключем, який він отримує, є саме ім'я папки, яке неможливо записати в шлях призначення.

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

0

Я давно стикався з цією проблемою, і на всіх різних форумах, які я пройшов, я не бачив повного наскрізного фрагмента того, що працює. Отже, я пішов уперед і взяв усі шматки (додати деякі речі самостійно) і створив повний наскрізний завантажувач S3!

Це не тільки автоматично завантажує файли, але якщо файли S3 знаходяться в підкаталогах, вони створюватимуть їх на локальному сховищі. У екземплярі моєї програми мені потрібно встановити дозволи та власників, тому я також додав це (можна закоментувати, якщо це не потрібно).

Це перевірено і працює в середовищі Docker (K8), але я додав змінні середовища в сценарій на випадок, якщо ви хочете перевірити / запустити його локально.

Сподіваюся, це допоможе комусь у пошуках автоматизації завантаження S3. Я також вітаю будь-які поради, інформацію тощо про те, як це можна краще оптимізувати у разі потреби.

#!/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()

0

З документів AWS S3 (Як використовувати папки у сегменті S3?):

У Amazon S3 сегменти та об’єкти є основними ресурсами, а об’єкти зберігаються у сегментах. Amazon S3 має плоску структуру замість ієрархії, як ви бачили б у файловій системі. Однак для організаційної простоти консоль Amazon S3 підтримує концепцію папки як засіб групування об'єктів. Amazon S3 робить це, використовуючи спільний префікс імен для об’єктів (тобто об’єкти мають імена, які починаються із загального рядка). Назви об'єктів також називаються іменами ключів.

Наприклад, ви можете створити на консолі папку з фотографіями та зберегти в ній об'єкт з ім'ям myphoto.jpg. Потім об’єкт зберігається з назвою ключа photos / myphoto.jpg, де photos / є префіксом.

Щоб завантажити всі файли з "mybucket" у поточний каталог, дотримуючись емульованої структури каталогів сегмента (створення папок із сегмента, якщо вони ще не існують локально):

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)

Було б краще, якщо б ви могли включити якесь пояснення свого коду.
Джохан

1
@johan, дякую за відгук! Я додав відповідне пояснення
Дарія
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.