Привет, Хабр!

На днях возникла типовая задача — помочь знакомой превратить гору фотографий в упорядоченную иерархию. Всё бы ничего, но гор фотографий не одна, а две — на Mac и на ноуте под Win10. В поисках решения, наткнулся на несколько сценариев для linux, а вот чего-то такого кроссплатформенного найти не удалось. Будем писать сами — прошу под кат.

Итак, нам нужно автоматизировать что-то, в том числе в Windows. Говорим автоматизация, подразумеваем Powershell — и к моему огромному счастью, Microsoft не так давно сделала его кроссплатформенным. Им и будем пользоваться.

Поскольку у фотографий есть обязательный общий признак, дата создания, иерархию будем строить на его основе в следующем порядке: корень архива > год > месяц > день. По ходу действия:

  1. Создаём корневой каталог,
  2. Получаем список файлов,
  3. Для каждого файла определяем дату создания из EXIF и
    • Дополняем структуру каталогов, если необходимо,
    • Перетаскиваем файл.

Звучит несложно, поехали.

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

[CmdletBinding()]
$curDir=$MyInvocation.MyCommand.Path | Split-Path -Parent

Проверяем наличие exiftool.

if (!(ExifTool)) {
    Write-Host "Install exiftool first!" -ForegroundColor Red -BackgroundColor Black
    break
}

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

Создаём корень нашей будущей структуры:

$ExtList = "arw,jpg,jpeg,NEF"
$FileList = Get-ChildItem $curDir -Recurse | `
    Where-Object {(($ExtList.ToUpper() -match $_.Name.Split('.')[-1]) `
        -or ($ExtList.ToLower() -match $_.Name.Split('.')[-1])) `
        -and ($_.FullName -notmatch "Ordered")
    }

# Define & сreate archive root folder
$ArchiveRoot = Join-Path -Path $curDir -ChildPath "Ordered"
if (!(Test-Path $ArchiveRoot)) {
    New-Item -ItemType Directory -Path $ArchiveRoot
}

Так как не все файлы джпеги могут иметь нужную информацию в EXIF (например, после пересохранения из соцсетей), создаём для них отдельный каталог. Их судьбу пользователь будет решать отдельно:

$ChaosRoot = Join-Path -Path $curDir -ChildPath "Chaos"
if (!(Test-Path $ChaosRoot)) {
    New-Item -ItemType Directory -Path $ChaosRoot
}

Для каждого файла из тех, которые разбираем, делаем следующее.

Определяем дату создания с помощью exiftool и разбиваем её на три составляющих: год, месяц, день. Так как exiftool поддерживает экспорт в csv, а powershell умеет с csv работать:

foreach ($File in $FileList) {
    $FileDateString = $null
    $FileDate = $null
    try {
        $FileDateString = ConvertFrom-Csv (ExifTool.exe $File.FullName -CSV) | `
            Select-Object -ExpandProperty CreateDate -ErrorAction Stop    
    }
    catch {
        Move-Item -Path $File.FullName -Destination $ChaosRoot -Force
        continue
    }
    
    $FileDate = $FileDateString.Split(" ")[0].Split(":")

Если нужный атрибут получить удалось, тогда разбиваем его на отдельные значения года, дня и месяца и идём далее, а если нет — нет, неопознанный файл сразу летит в хаос.

Итак, дата получена. Создаём каталог, ну и перетаскиваем файл:

$YearPath = Join-Path $ArchiveRoot $FileDate[0]
$MonthPath = Join-Path $YearPath $FileDate[1]
$FileDest = Join-Path $MonthPath $FileDate[2]
if (!(Test-Path $FileDest)) {
   New-Item -ItemType Directory -Path $FileDest
}
Move-Item -Path $File.FullName -Destination $FileDest -Force
}

Ну и не забываем прибрать за собой мусор, удалив уже ненужные пустые каталоги:

$AllDirsAtFinal = Get-ChildItem -Path $curDir -Recurse -Force | Where-Object {$_.PSIsContainer -eq $true} 
foreach ($Dir in $AllDirsAtFinal) {
    $DirLenght = Get-ChildItem -Path $Dir.FullName -Recurse -Force -ErrorAction SilentlyContinue | `
        Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
    if ($null -eq $DirLenght.Sum) {
        Remove-Item $Dir.FullName -Recurse -Force -ErrorAction SilentlyContinue
    }
}

Вот, в общем-то, и всё.

Для работы понадобятся:

PowerShell для Mac
ExifTool by Phil Harvey

Комментарии (22)


  1. Denai
    01.12.2019 20:18

    А дописать хотя бы даты в exif распределённых пользователем файлов?


    1. altollis Автор
      01.12.2019 21:21

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


  1. mSnus
    02.12.2019 01:32

    Я правильно понимаю, что если в системе установлен другой разделитель даты, "файл летит в хаос"? А если кто-то исправит машинально DirLenght на DirLength, то и директории сотрутся, рекурсивно?


    Я бы очень советовал избегать вообще радикальных действий с файлами и директориями, особенно рекурсивно.


    Смысл скрипта не очень понимаю — он даты файлов, собственно, игнорирует? А почему?
    Да ещё и имевшуюся структуру папок сносит без спросу? То есть было папки "котики", "Юлины фото", "сканы документов", а он это все снесет и разложит по датам EXIF, уничтожив имевшиеся структуры?


    1. altollis Автор
      02.12.2019 01:55

      Неправильно — утилита exiftool использует свои разделители, не привязанные к системным настройкам и возвращает строку. К datetime приведения нет.
      Даты файлов игнорируются, т.к. задача — разложить именно по времени съёмки. Условно, рав сделан позавчера, обработан и сохранён в джпег сегодня, время создания файлов будет разным, но им логично быть в каталоге от одной даты, причём от даты съёмки, а не, например, последнего изменения.
      Директории сотрутся рекурсивно, если суммарный размер целевой и всех вложенных равен нулю, т.е. каталог пуст.
      Смысл скрипта как раз в том, чтобы избавиться от нескольких десятков папок «котики», «концерты», «попойки». Внутри условного «Юлины фото», можно организовать свою каталогизацию, при желании. Сканы документов пинать в фото(!)архив по крайней мере, странно, не находите?
      Штука делалась фотографом для фотографа, с учётом специфики.
      Предлагаете в «хаосе» воссоздать исходную структуру каталогов для нераспределённых?


      1. mSnus
        02.12.2019 14:57

        Но если экзифа нет — почему не использовать дату файла вместо того, чтобы отправлять в хаос?


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


        Понимаю, что у вас свой случай и задачи, но все же..


        1. altollis Автор
          02.12.2019 16:08

          Потому что дата файла может очень, очень! отличаться от фактического положения вещей. В итоге вместо упорядоченной области можно получить тот же бардак, но ещё и неожиданный.
          Не трогать — да, может быть это будет логичнее, тем более, что для этого достаточно убрать/закомментить одну строку.


      1. dMac
        02.12.2019 22:50

        >>Предлагаете в «хаосе» воссоздать исходную структуру каталогов для нераспределённых?
        Предложил бы не только в «хаосе» так делать. Почему бы к имени папки, обозначающей номер дня, не приписать пару последних уровней исходных каталогов?
        Например,
        2019\08\21-фотосессия в парке
        2019\08\21-детская площадка
        и т.д.


        1. altollis Автор
          03.12.2019 09:05

          В моём случае (тотального бардака) 90% этих приписок выглядело бы как 2019\08\21-101_0821 — бесполезно, некрасиво и нестандартно.
          Возможно, для кого-то в этом будет резон.


    1. vlivyur
      03.12.2019 19:23

      Дата файла самая ненадёжная штука для определения даты снимка и почти наверняка не имеет к нему отношения.
      По моему мнению все эти скрипты должны только применяться к каталогам «Разобрать!!!» и «New Folder (4)», а в вашем случае какая-то каталогизация уже выполнена.


      1. mSnus
        04.12.2019 00:31

        На мой вкус, подобная каталогизация по датам вообще не имеет смысла. Если подходить к этому серьезно, надо
        хотя бы анализировать дату И время, чтобы понимать "серии", и складывать по сериям.


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


        Но что-то пока таких инструментов не нашел, и они далековато от скрипта автора..


        1. vlivyur
          04.12.2019 17:41

          По первому вопросу я так у себя и делаю: серию завожу если между снимками прошло больше 4 часов.


  1. FilimoniC
    02.12.2019 08:40

    1. -match в powershell регистронезависим. Регистрозависимый называется cmatch.
    2. Можно было обойтись списком и -contains
    3. Для отчленения разрешения [system.io.path]::****, хотя не уверен что на Мак он есть

    Идея хорошая. Коду ближе к индусскому


  1. trapwalker
    02.12.2019 12:00

    О, какую больную тему затронули. Давно у меня зреет в загашниках такой пет-проект. Только вот долбаный перфекционизм не даёт начать с малого, как это сделали вы.
    У меня к такой утилитке, похоже, гораздо больше требований, чем у вас.
    Вот в ридмишке к проекту их можно глянуть.
    Жалко времени на все эти завалы из пет-проектов не хватает.


    Бесит каждый раз вручную раскладывать фотки из нескольких источников, а еще нужно думать об облачных бэкапах в гугле и яндексе, о том, что у меня хранятся и raw и jpeg, о том. что EXIF у них немного разные, о том, что куча сопутствующего хлама рядом валяется из брекетинговых и панорамных серий. Жалко же выбрасывать, "вдруг руки дойдут" (обычно не доходят). Иногда нащёлкаешь как из пулемёта десятки кадров одного сюжета с намерением потом выбрать ОДИН хороший, где никто не моргает, но потом просто забиваешь за неимением времени. Вот такие бы серии как-то группировать и прятать до хороших времен.


  1. VitFiend
    02.12.2019 12:55

    Друже, где ты был раньше? Эту задачу всегда решал вручную. Огромное, тебе, человеческое спасибо!


    1. 3aBulon
      02.12.2019 17:15

      Использую тотал командер с его массовым переименованием через Ctrl+M.

      Там уже есть и exif плагин, имя файлу можно задавать произвольно, при совпадении имен файлов(с разных фотиков бывает например P1003751.jpg) сделает приписку (1).
      Есть откат последних действий(если накосячилось).


  1. felix21
    02.12.2019 12:55

    Как долго этот скрипт будет выполняться, разбирая папку с 80к файлов на слабеньком компе?
    Вот уже двадцать минут вижу только это:

    Каталог: D:\JPG
    Mode LastWriteTime Length Name
    — — — — d----- 12/2/2019 1:09 AM Ordered
    d----- 12/2/2019 1:09 AM Chaos


    1. altollis Автор
      02.12.2019 12:56

      Файлы разные бывают. На 60 гигов с седьмой альфы потребовалось минут 15.


  1. faultedChip
    02.12.2019 12:57

    Пара замечаний вот к этому коду:
    $ExtList = "arw,jpg,jpeg,NEF"
    $FileList = Get-ChildItem $curDir -Recurse | `
    Where-Object {(($ExtList.ToUpper() -match $_.Name.Split('.')[-1]) `
    -or ($ExtList.ToLower() -match $_.Name.Split('.')[-1])) `
    -and ($_.FullName -notmatch "Ordered")
    }


    1) У DirectoryInfo (объект, который возвращает Get-ChildItem) уже есть свойство Extension, соответственно нет нужды разбирать имя.
    2) Операции сравнения в Powershell (т.е. -match, -eq и прочее) по-умолчанию игнорируют регистр, соответственно двойные проверки не нужны.
    3) Вообще есть оператор -in, который проверяет наличие значения в списке.

    В итоге этот код можно упростить примерно до такого:
    $ExtList = '.arw','.jpg','.jpeg','.NEF'

    $FileList = Get-ChildItem $curDir -Recurse | `
    Where-Object { $_.Extension -in $ExtList`
    -and ($_.FullName -notmatch "Ordered")
    }


    1. altollis Автор
      02.12.2019 12:59

      Принимается, спасибо!
      Про регистрозависимость — не в курсе, насколько это справедливо для мака, решил перестраховаться.


  1. SakuradaJun
    02.12.2019 13:34
    +2

    Поскольку у фотографий есть обязательный общий признак, дата создания

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


    1. kyern
      02.12.2019 16:05

      \o


  1. user343
    03.12.2019 10:27

    Я в первую очередь удаляю дубликаты через ccleaner (побитно совпадающие, но в разных папках или с разными именами) и программами типа imagedupeless (похожие изображения). Второе простым скриптом не сделать.
    И ещё у кого смертфоны или продвинутые фотоаппараты, по геотэгам (координатам мест) раскидывать снимки было бы удобнее, чтобы наблюдать за ростом свалок деревьев, зданий, карьеров и т.д.
    Без автораспознавалки лиц — тоже грустно.