Сортируем файлы с помощью Python +7


AliExpress RU&CIS

Штош. Скорее всего, у многих в папке загрузок собиралась куча разных инсталляторов, архивов и прочих файлов. И вот наступает момент, когда из этой кучи экскрементов нужно найти какой-то файл. Вот я и подумал, почему бы не написать скрипт сортировщика по расширениям файлов на Python?

Пишем код

Для начала импортируем стандартный модуль os для работы с операционной системой. С помощью этого модуля мы будем создавать папки и перемещать файлы.

import os

Создадим переменную для пути папки или диска, в которой мы будем сортировать файлы. В моих любимых окошечках путь к файлу записывается через backslash, который в Python является специальным символом экранирования. Поэтому мы либо пишем 2 бэкслэша, либо добавляем перед строкой букву r, тем самым подавляя экранирование.

main_path = 'd:\\down'
# main_path = r'd:\down

Чтобы создать папку, используем метод os.mkdir()

os.mkdir(main_path + '\\aboba')

Создаем много папок

Напишем функцию для создания папок из списка названий. Для каждого названия проверяем существование папки с помощью метода os.path.exists().

# also creates folders from dictionary keys
def create_folders_from_list(folder_path, folder_names):
    for folder in folder_names:
        if not os.path.exists(f'{folder_path}\\{folder}'):
            os.mkdir(f'{folder_path}\\{folder}')

Теперь давайте создадим словарь extensions. Ключи - названия папок. Значения - расширения файлов для каждой отдельной папки.

# key names will be folder names!
extensions = {

    'video': ['mp4', 'mov', 'avi', 'mkv', 'wmv', '3gp', '3g2', 'mpg', 'mpeg', 'm4v', 
              'h264', 'flv', 'rm', 'swf', 'vob'],

    'data': ['sql', 'sqlite', 'sqlite3', 'csv', 'dat', 'db', 'log', 'mdb', 'sav', 
             'tar', 'xml'],

    'audio': ['mp3', 'wav', 'ogg', 'flac', 'aif', 'mid', 'midi', 'mpa', 'wma', 'wpl',
              'cda'],

    'image': ['jpg', 'png', 'bmp', 'ai', 'psd', 'ico', 'jpeg', 'ps', 'svg', 'tif', 
              'tiff'],

    'archive': ['zip', 'rar', '7z', 'z', 'gz', 'rpm', 'arj', 'pkg', 'deb'],

    'text': ['pdf', 'txt', 'doc', 'docx', 'rtf', 'tex', 'wpd', 'odt'],

    '3d': ['stl', 'obj', 'fbx', 'dae', '3ds', 'iges', 'step'],

    'presentation': ['pptx', 'ppt', 'pps', 'key', 'odp'],

    'spreadsheet': ['xlsx', 'xls', 'xlsm', 'ods'],

    'font': ['otf', 'ttf', 'fon', 'fnt'],

    'gif': ['gif'],

    'exe': ['exe'],

    'bat': ['bat'],

    'apk': ['apk']
}

Передаем в функцию create_folders_from_list() новоиспеченный словарь. Папки создадутся из названий ключей.

Получаем пути подпапок и файлов

Пишем функцию для получения путей подпапок. Для каждого объекта в методе os.scandir() проверяем, является ли он каталогом.

def get_subfolder_paths(folder_path) -> list:
    subfolder_paths = [f.path for f in os.scandir(folder_path) if f.is_dir()]

    return subfolder_paths

Можно создать функцию получения имен подпапок, которая нам не пригодится дальше. Почему бы и нет? На самом деле, как сниппет для дальнейших проектов она может быть полезна.

Нам нужно взять пути подпапок, разделить по бэкслэшам строку и взять последний элемент.

def get_subfolder_names(folder_path) -> list:
    subfolder_paths = get_subfolder_paths(folder_path)
    subfolder_names = [f.split('\\')[-1] for f in subfolder_paths]

    return subfolder_names

Теперь получим пути всех файлов в папке, скопируем функцию get_subfolder_paths() и добавим в условие генератора not.

def get_file_paths(folder_path) -> list:
    file_paths = [f.path for f in os.scandir(folder_path) if not f.is_dir()]

    return file_paths

Для полного счастья не хватает только функции получения имен файлов.

def get_file_names(folder_path) -> list:
    file_paths = [f.path for f in os.scandir(folder_path) if not f.is_dir()]
    file_names = [f.split('\\')[-1] for f in file_paths]

    return file_names

Сортируем файлы

Приступаем к функции сортировки. Получаем пути файлов в переменную file_paths. Создаем переменную ext_list со списком метода словаря extensions.items(). Обращение к списку по индексу возвращает нам пару ключ-значение в виде списка, первый элемент которого - это ключ или название папки в нашем проекте, а второй элемент - это значение, то есть расширения файлов для этой папки.

def sort_files(folder_path):
    file_paths = get_file_paths(folder_path)
    ext_list = list(extensions.items())

Теперь создадим цикл для каждого пути файла в списке. Вытащим отдельно расширение и имя файла.

for file_path in file_paths:
  extension = file_path.split('.')[-1]
  file_name = file_path.split('\\')[-1]

Создадим еще один цикл внутри. Для каждого ключа в словаре мы проверяем, есть ли расширение файла в списке расширений. Если есть, то перемещаем файл.

for dict_key_int in range(len(ext_list)):
  if extension in ext_list[dict_key_int][1]:
    print(f'Moving {file_name} in {ext_list[dict_key_int][0]} folder\n')
    os.rename(file_path, f'{main_path}\\{ext_list[dict_key_int][0]}\\{file_name}')

Сделать это можно при помощи изменения пути файла методом os.rename("Путь файла сейчас", "Будущий путь файла")

Готовая функция сортировки файлов:

def sort_files(folder_path):
    file_paths = get_file_paths(folder_path)
    ext_list = list(extensions.items())

    for file_path in file_paths:
        extension = file_path.split('.')[-1]
        file_name = file_path.split('\\')[-1]

        for dict_key_int in range(len(ext_list)):
            if extension in ext_list[dict_key_int][1]:
                print(f'Moving {file_name} in {ext_list[dict_key_int][0]} folder\n')
                os.rename(file_path, f'{main_path}\\{ext_list[dict_key_int][0]}\\{file_name}')

Удаляем пустые папки

Остался последний штрих - удаление пустых папок. Все просто. Создаем функцию. Получаем пути подпапок. Проверяем, какой список возвращает метод os.listdir("folder_path") для каждой подпапки. Если возвращается пустой список, значит удаляем папку с помощью os.rmdir("folder_path")

def remove_empty_folders(folder_path):
    subfolder_paths = get_subfolder_paths(folder_path)

    for p in subfolder_paths:
        if not os.listdir(p):
            print('Deleting empty folder:', p.split('\\')[-1], '\n')
            os.rmdir(p)

Полный код программы

import os

main_path = 'd:\\down'

# key names will be folder names!
extensions = {

    'video': ['mp4', 'mov', 'avi', 'mkv', 'wmv', '3gp', '3g2', 'mpg', 'mpeg', 'm4v', 'h264', 'flv',
              'rm', 'swf', 'vob'],

    'data': ['sql', 'sqlite', 'sqlite3', 'csv', 'dat', 'db', 'log', 'mdb', 'sav', 'tar', 'xml'],

    'audio': ['mp3', 'wav', 'ogg', 'flac', 'aif', 'mid', 'midi', 'mpa', 'wma', 'wpl', 'cda'],

    'image': ['jpg', 'png', 'bmp', 'ai', 'psd', 'ico', 'jpeg', 'ps', 'svg', 'tif', 'tiff'],

    'archive': ['zip', 'rar', '7z', 'z', 'gz', 'rpm', 'arj', 'pkg', 'deb'],

    'text': ['pdf', 'txt', 'doc', 'docx', 'rtf', 'tex', 'wpd', 'odt'],

    '3d': ['stl', 'obj', 'fbx', 'dae', '3ds', 'iges', 'step'],

    'presentation': ['pptx', 'ppt', 'pps', 'key', 'odp'],

    'spreadsheet': ['xlsx', 'xls', 'xlsm', 'ods'],

    'font': ['otf', 'ttf', 'fon', 'fnt'],

    'gif': ['gif'],

    'exe': ['exe'],

    'bat': ['bat'],

    'apk': ['apk']
}


# also creates folders from dictionary keys
def create_folders_from_list(folder_path, folder_names):
    for folder in folder_names:
        if not os.path.exists(f'{folder_path}\\{folder}'):
            os.mkdir(f'{folder_path}\\{folder}')


def get_subfolder_paths(folder_path) -> list:
    subfolder_paths = [f.path for f in os.scandir(folder_path) if f.is_dir()]

    return subfolder_paths


def get_file_paths(folder_path) -> list:
    file_paths = [f.path for f in os.scandir(folder_path) if not f.is_dir()]

    return file_paths


def sort_files(folder_path):
    file_paths = get_file_paths(folder_path)
    ext_list = list(extensions.items())

    for file_path in file_paths:
        extension = file_path.split('.')[-1]
        file_name = file_path.split('\\')[-1]

        for dict_key_int in range(len(ext_list)):
            if extension in ext_list[dict_key_int][1]:
                print(f'Moving {file_name} in {ext_list[dict_key_int][0]} folder\n')
                os.rename(file_path, f'{main_path}\\{ext_list[dict_key_int][0]}\\{file_name}')


def remove_empty_folders(folder_path):
    subfolder_paths = get_subfolder_paths(folder_path)

    for p in subfolder_paths:
        if not os.listdir(p):
            print('Deleting empty folder:', p.split('\\')[-1], '\n')
            os.rmdir(p)


if __name__ == "__main__":
    create_folders_from_list(main_path, extensions)
    sort_files(main_path)
    remove_empty_folders(main_path)

Настройка программы под свои нужды

Как вы уже могли понять, программа довольно гибкая, и вы можете настроить ее под себя. Для этого нужно всего-лишь изменить словарь extensions.

Приведу пример. Для каждого видео на свой YouTube канал я создаю каталог, в котором есть папки для футажей, картинок, звука, mkv файлов для последующего конвертирования в mp4 (premiere не любит mkv) и самого проекта.

Вот такой словарь.

main_folder = 'f:\\shtosh python\\new video'

# key names will be folder names!
extensions = {

    'img': ['jpg', 'png', 'bmp', 'gif', 'ico', 'jpeg'],

    'audio': ['mp3', 'wav'],

    'footage': ['mp4', 'mov', 'avi'],

    'mkv': ['mkv'],

    'prj': []
}

С помощью такого подхода я могу накидать любые файлы для монтажа в одну папку, затем запустить скрипт и легко импортировать все это дело в premiere pro.

Заключение

Штош. Код лежит на GitHub. Берите, изменяйте под себя, пользуйтесь на здоровье. Буду рад любому фидбеку.


UPD. Доработал программу, учитывая (не все) замечания в комментариях. Статью не переписывал, мне лень.




Комментарии (37):

  1. qark
    /#23140722

    Вместо os.path.* и прочих лучше использовать pathlib, а вместо range(len(list))enumerate(list).
    main_path стоит сделать параметром скрипта.

    • lesskop
      /#23141396

      Спасибо, хорошее замечание. Не использовал библиотеку pathilib, так как никаких сложных манипуляций с путями не производится, только проверка существования файла и парсинг имени файла и расширения.

  2. Ninil
    /#23141064

    В связи с потенциально большим количеством файлов в папке, лучше использовать генераторы

    • lesskop
      /#23141382

      О каких генераторах идет речь?

      • qark
        /#23141984

        Примерно о таких:


        (f.path for f in os.scandir(folder_path) if f.is_dir())

      • kuznitsin
        /#23142490

        То, что вы назвали генератором — таковым не является. Вы даже в аннотации к функции указали результатом тип list и именно его и возвращаете.

        def get_file_paths(folder_path) -> list:
            file_paths = [f.path for f in os.scandir(folder_path) if not f.is_dir()]
        
            return file_paths
        

        Если бы это действительно было генератором, выглядело бы оно примерно так.
        def get_file_paths(folder_path: str) -> Iterator[str]:
            for fn in os.scandir(folder_path):
                if not fn.is_dir():
                    yield fn.path
        


        Работать с путями куда проще и правильней при помощи стандартного os.path.join и os.path.split.

        • lesskop
          /#23142506

          Я имел ввиду генератор списка, очевидно. Не особо понимаю, как именно генератор помог бы мне, быстрее бы выполнилась программа? В чем достоинства?
          Насчет os.path.join и split согласен.

          • qark
            /#23142798

            Меньше памяти. Вместо огромного списка из 100500 строк получаешь небольшой итератор.

  3. unsignedchar
    /#23141406

    for file_path in file_paths:
    for p in subfolder_paths:

    Как заставить себя придумывать годные имена для переменных? Чтобы не однобуквенные, но немногословные, друг на друга непохожие, осмысленные, и в одном стиле? ;)

    • lesskop
      /#23141514

      Вы обратились явно не по адресу :)

  4. pansa
    /#23142084

    нужно найти какой-то файл. Вот я и подумал, почему бы не написать скрипт сортировщика по расширениям файлов на Python?

    Соррян. Так нужно файл найти или отсортировать? Вроде хотели одно, а делаем другое.
    У старика Кнута это прям разные главы были. Из того, что я помню, это более эффективный поиск по отсортированной последовательности, но у вас явно не про это.


    ЗЫ Вроде в виндоусе есть даже встроенный поиск. Даже я помню вечно тормозящая служба индексера была, да да! Что-то вы странное делаете.

    • lesskop
      /#23142466

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

  5. gdt
    /#23142242

    Спасибо, было интересно. А чем не устраивает встроенная в Explorer сортировка? Тоже имею кучу всего в Downloads и по работе периодически приходится что-то оттуда вытаскивать, но для моих целей этого более чем достаточно:
    built-in explorer sorting

    • lesskop
      /#23142480 / +1

      Решил организовать в едином стиле папки с большим количеством файлов + чуть-чуть покодить

  6. d583605
    /#23142702

    В качестве задачи со звёздочкой можно реализовать в исключительных случаях определение типа файла не по названию расширения, а по каким-нибудь мета-данным. Допустим .ts используется и для исходных файлов языка typescript, и для видеофайлов.

    • lesskop
      /#23142714

      Интересное предложение, реализовывать я его, конечно же, не буду :)
      P.S. Вы действительно имели дело с видеофайлами .ts? Я впервые от вас узнал, что такое существует.

      • d583605
        /#23143058

        Вы действительно имели дело с видеофайлами .ts?

        Когда я пропускаю формулу-1 по телевизору, то скачиваю запись через торрент. Там, от куда я скачиваю, обычно выкладывают их в формате .ts. Я был и сам удивлён в первый раз, когда только что скачанный файл мне предложили открыть vscodo'м.

  7. CodeNameHawk
    /#23142716

    Штош. Скорее всего, у многих в папке загрузок собиралась куча разных инсталляторов, архивов и прочих файлов.

    Посмотрел бы я как вы запустите инсталлятор, после вашей сортировки, исключение только если инсталлятор идет одним файлом.

    По тексту статьи она совсем об другой сортировке, сильно специфичной.

    • lesskop
      /#23142718

      Да, если инсталлятор идет не одним файлом, будет грустно. Можно закомментировать в словаре папку exe, как вариант решения проблемы.

      • unsignedchar
        /#23142800

        Во первых, если в папке Downloads завелись файлы setup.exe, setup(1).exe.., то самый разумный способ автоматической сортировки - Ctrl+A Del. Никто никогда не вспомнит, что это за инсталляторы. Нет смысла их куда то перекладывать.

        Во вторых, если было при скачивании лень нажать save as - значит, скачивалось что то одноразовое. Могу предложить отсортировать файлы по дате и удалить все что старше 1 месяца.

  8. CrocodileRed
    /#23142912

    Не забывайте про os.path.join()

  9. nero211
    /#23144038

    эм…

    @echo off
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\arch *.zip *.7z *.rar *.gz /MOV /IS /minage:1
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\doc *.doc* *.xls* *.pdf *.csv  /MOV /IS /minage:1
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\img *.psd  /MOV /IS /minage:1
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\soft *.exe *.msi *.cab *.iso *.mdf *.mds  /MOV /IS /minage:1
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\media *.mp3 *.wav *.mp4 *.avi /MOV /IS /minage:1
    ROBOCOPY C:\Users\mf\Downloads\ X:\dl\book *.fb2 /MOV /IS /minage:1
    


    а если так? не проще…

    • lesskop
      /#23144046

      Спасибо за хороший метод.
      Для конечного потребителя не проще. В скрипте нужно лишь поменять переменную пути каталога и настроить словарь extensions под свой метод сортировки.

      • unsignedchar
        /#23144146

        В cmd тоже можно использовать переменные ;) А строк получается значительно меньше. Получается, так проще.

        • lesskop
          /#23144160

          Еще раз, для конечного потребителя (обычного юзера, не для энтузиастов с хабра, знающих про ROBOCOPY в cmd) это не проще. Куда легче набрать в текстовом редакторе путь каталога, названия папок и расширения (а можно использовать типовые), чем вот это вот все.

          • nero211
            /#23144190

            Ваше решение более чем интересное, но, наверное, стоит начать с того. что обычные пользователи пользуются исключительно мышкой и не ходят на Хабр =)

            а папка %HOMEPATH%\Downloads обычно является просто свалкой, наравне с %TEMP% и %TMP%

            К вашему решению для обычного пользователя осталось добавить gui для настройки и инсталлятор с вэб установщиком интерпретатора.

            • lesskop
              /#23144206

              Они могут сходить на мой ютуб :)

              • unsignedchar
                /#23144618

                Это слишком длинный путь ;)

                Простой способ решения проблемы файлопомойки - сортировка средствами файл менеджера, перенос нужного, удаление ненужного. Написание кода тут совершенно лишнее. :Ь

                • lesskop
                  /#23144714

                  Хорошо, вы правы.
                  Извините, что потратил ваше время на чтение этой статьи и написание 4 комментариев

                  • unsignedchar
                    /#23144734

                    Да я понимаю, что решать разные задачи на питоне это прикольно ;) Но не всегда код на питоне это лучшее решение. И ваша задача - отличная иллюстрация для этого утверждения ;)

                    • lesskop
                      /#23144802

                      Вы никак не угомонитесь, все вас поняли, спасибо. Я зря писал программу.

  10. qvirin
    /#23145646

    А почему не используете os.walk?

    • lesskop
      /#23145674

      Решил в лоб, знал про os.listdir и os.scandir. Нужны были пути папок и файлов отдельно. Сделал.
      С os.walk не приходилось встречаться, хотя если так подумать, с ним очень просто реализовать многоуровневую сортировку.

  11. danilovmy
    /#23146818

    Привет. Буквально вчера перебирал file_cache на сервере, папка для временных файлов, которые необходимы пользователям. У нас уже было создано нечто подобное сортировке из статьи. За 6 лет функция обросла многими доработками. Потому расскажу, какие сложности встречались в этой, казалось бы, банальной задаче:

    Предпосылки — 100 гигабайт хранилище, 350 000 файлов, расширения png, gif, ico, jpeg, svg, webp, tiff, pdf, tmp, html, xml, xls, xslx, doc, docx, js, css. Видео ни разу не встречал, хоть и можно. И бесконечное количество файлов с .tmp, .temp или без расширений. Адобовские еще файлы какие то AI, PSD… и т.п.

    Сложности:
    1. Как уже в комментариях сказали — расширение файла не говорит ничего. Хуже того, если отсортировать картинки по расширению, не проверив, для последующей пакетной обработки, например, в PhotoShop или программно в том же PIL — все упадет. Хорошо так упадет.
    Для проверки переимовываем xxxxx.jpg в yyyy.png и открываем в photoshop.

    В итоге мы стали использовать Fleep автор Mykyta Paliienko. Эта утилита сообщает о типе файла по содержимому. Кстати пришлось доработать «fleeper» до рабочего состояния, в issue автору тоже сообщили.

    2. Файлы с именами, написанными через точку, и без расширений. Отдельная задача, поскольку формально расширение есть.

    Решилось так же флиппером.

    В коде git статьи extension получается через fp.split('.')[-1], это аналог os.path.splitext(fp) и оба не помогут в решении пункта 2, но os.path.splitext более «кошерный» в случае работы с файловой системой.

    3. Также в комментариях сказали про однострочные генераторы. Как в статье так и GIT с примером кода, используются листы или функции генераторов, даже там, где достаточно однострочного генератора, который может делать еще и полезную работу:

    # функция def sort_files()
    for fp, (file, extention) in (fp.path, os.path.splitext(fp.path) for fp in os.scandir(folder_path) if not fp.is_dir()):
    ....
    

    Если кого расстраивают длинные строки, например линтер black прям беснуется, то можно так же сделать генераторный pipline:
    # функция def sort_files()
    path_generator = (_file.path for _file in os.scandir(folder_path) if not _file.is_dir())
    extentions_generator = (_path, os.path.splitext(_path) for _path in path_generator)
    for path, (file, extention) in extentions_generator:
    ....
    


    4. Создание папок, и, потом, удаление пустых папок — выглядит вычурно. Если есть словарь хранящий { расширение: путь }, тогда проще получать путь из словаря и создавать путь, если словарь вернул пустое значение.

    5. Логирование. Сложная тема. Более 300 000 сообщений одномоментно в консоль бессмыслены и ломали «terminus»- консоль в sublime text.

    Решение — использовать стандартный логгер Python c выводом в консоль/файл управляя выводом через атрибуты запуска.

    Успехов автору в кодинге.

    • lesskop
      /#23147818

      Огромное спасибо за развернутый, конструктивный комментарий.