Как перемещать файлы на основе некоторых столбцов .csv? - PullRequest
1 голос
/ 09 марта 2020

У меня есть папка с обучающими наборами (/ train) с более чем 100K изображениями разных классов. Класс изображения указан в отдельном CSV-файле, который выглядит следующим образом:

hashed_id,country,continent,scientific_name,filename 
fd148672d8,United States of America,North America,nerodia-sipedon,fd148672d8.jpg

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

awk -F "," 'NR>1 {print $4}' train_labels.csv | head -1| xargs mkdir -p 

Этот код создает подпапки для каждого научного имени, если подпапка еще не существует. Он использует запятую в качестве разделителя и игнорирует заголовок.

cp train/$(awk -F "," 'NR>1 {print $5}' train_labels.csv | head -1) $(awk -F "," 'NR>1 {print $4}' train_labels.csv | head -1)

Этот код копирует изображение из папки / обучает соответствующую подпапку. Обратите внимание, что здесь я делаю это только для одного изображения (голова -1), чтобы проверить это.

Я счастлив, что сделал это довольно далеко, но ... это лучший способ сделать это? Я чувствую, что повторений слишком много, и я хотел бы получить более краткий bash сценарий.

Ответы [ 3 ]

2 голосов
/ 09 марта 2020

Вы можете сделать все это с помощью awk, используя команду system, чтобы создать каталог и переместить файл. Вы можете сделать это с помощью:

awk -F, 'FNR>1{ system("mkdir -p \"" $4 "\" && mv \"" $5 "\" \""$4"\"")}' manyfiles.csv

Дополнительное экранирование кавычек делает его более сложным. Дополнительные кавычки просто гарантируют, что имена файлов и каталогов заключены в кавычки, чтобы избежать проблем с именами, содержащими пробелы. Команды, сформированные и используемые с командой system(), например:

mkdir -p "nerodia-sipedon" && mv "fd148672d8.jpg" "nerodia-sipedon"

Если вам легче понять, что происходит без всех дополнительных экранированных кавычек, то вы получите что-то вроде следующее:

awk -F, 'FNR>1{ system("mkdir -p " $4 " && mv " $5 " "$4)}' manyfiles.csv

( примечание: всегда использовать указанную версию)

Где FNR>1 simple говорит awk игнорировать первую запись (нет необходимости передать head/tail, а остальное - просто конкатенация строк в команде system() для создания команды для создания каталога и перемещения файла.

Это должно уменьшить время для значительного перемещения файлов (при работе с любым большим количеством файлов - всегда сохраняйте первозданную резервную копию на случай, если что-то пойдет не так или вы захотите сделать что-то еще позже - - " вы были предупреждены ... " :)

0 голосов
/ 09 марта 2020

Ваша попытка создать папки хороша, хотя я бы сделал немного иначе:

tail -n +2 train_labels.csv | cut -d, -f4 | sort -u | xargs mkdir -p

Я использую tail и cut для вывода списка папок, который должен быть быстрее чем awk для очень больших файлов. Тестируя с файлом из 120 тыс. Строк, я получаю около 150 мс для awk против 100 мс для tail|cut.

Но наиболее важной точкой импорта является сортировка списка с sort -u перед подачей mkdir чтобы минимизировать количество системных вызовов. Это правда, что mkdir -p не будет создавать один и тот же каталог дважды, но ему все равно необходимо проверить, существует ли каталог, что требует значительных затрат.

Для копирования / перемещения файлов ваше решение работает просто оборачивая его вокруг простого синтаксического анализатора CSV, используя read с IFS=,

while IFS=, read -r hashed_id country continent scientific_name filename
do
  cp train/"$filename" "$scientific_name"/
done < train_labels.csv

Вы можете группировать вызовы копирования / перемещения для строк с одинаковыми scientific_name, например, cp train/file1 train/file2 train/file3 my_scientific_name/, но это немного более сложный, он не обязательно быстрее, и вы можете в конечном итоге достичь предела аргумента командной строки.

Создание папок

Если вы хотите заполнить свои папки, например, 1000 файлами, вы Сначала нужно определить синтаксис для ваших сегментов, например, $scientific_name.$index, где $index для вашего сегмента. Например, первые 1000 файлов nerodia-sipedon будут в конечном итоге в nerodia-sipedon.0, затем следующие 1000 файлов в nerodia-sipedon.1 и так далее. Соглашение об именах является произвольным, и вы можете изменить его, если подумаете о более хорошем соглашении.

Тогда вы можете использовать ассоциативный массив, который подсчитывает количество раз, которое использовалось каждое научное имя c.

declare -A names=()
while IFS=, read -r hashed_id country continent scientific_name filename
do
  ((index=names[$scientific_name]/1000))
  ((names[$scientific_name]%1000==0)) && mkdir -p "$scientific_name"."$index"
  cp train/"$filename" "$scientific_name"."$index"/
  ((names[$scientific_name]++))
done < train_labels.csv

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

Давайте разберем алгоритм для имени:

  ((index=names[$scientific_name]/1000))

Поскольку имена еще не вставлены, names[$scientific_name] эквивалентно 0, что означает, что эта строка присваивает 0 index.

  ((names[$scientific_name]%1000==0)) && mkdir -p "$scientific_name"."$index"

Поскольку имена не были вставленный, names[$scientific_name]%1000 будет равен 0, что означает, что каталог будет создан с использованием синтаксиса $scientific_name.$index.

  cp train/"$filename" "$scientific_name"."$index"/

Это просто копирует файл в новый каталог.

  ((names[$scientific_name]++))

Увеличивает количество записей для этого научного c имени. Поскольку никакие имена еще не были вставлены, число будет увеличено с 0, что приведет к значению 1.

В следующий раз, когда будет найдена запись с тем же научным именем c, names[$scientific_name] вернет 1:

  ((index=names[$scientific_name]/1000))

Назначит 0 на index.

  ((names[$scientific_name]%1000==0)) && mkdir -p "$scientific_name"."$index"

Будет не создать каталог из-за остатка из 1%1000 не ноль.

  cp train/"$filename" "$scientific_name"."$index"/

Копирует файл в существующий каталог.

  ((names[$scientific_name]++))

Увеличивает число с 1 до 2.

Это продолжается до 999-й записи для того же научного c имени. Для 1000-й записи index будет 1, и будет создан каталог. 1001-й файл, 1002-й файл, et c. будет скопирован в этот новый каталог. Затем 2000-й файл создаст другой каталог. Промойте и повторите.

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

declare -A names=()
while IFS= read -r -d '' folder
do
  scientific_name=${folder%.*}
  (( names[$scientific_name]+=$(find "$folder" -type f | wc -l) ))
done < <(find . -mindepth 1 -maxdepth 1 -type d -name '?*.?*' -printf '%f\0')
0 голосов
/ 09 марта 2020

Вы можете перебрать файл CSV и установить IFS=,:

while IFS=, read -r v v v type filename; do
    mkdir -p "$type"
    mv -n "$filename" "$type"
done < <(sed 1d images.csv)

sed 1d file удаляет строку заголовка из файла CSV. Первые три столбца выделены красным цветом в фиктивные переменные v v v.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...