Moverse como archivos con nombre a directorios con nombre propio

4

Tengo varios miles de archivos en un directorio que me gustaría clasificar en directorios como este:

De esto:

└── Files
    ├── AAA.mkv
    ├── AAA.nfo
    ├── AAA-picture.jpg
    ├── BBB.mp4
    ├── BBB.srt
    ├── BBB-clip.mp4
    ├── CCC.avi
    ├── CCC.srt
    ├── CCC-clip.mov
    └── CCC.nfo

A esto:

└── Files
    ├── AAA
       ├── AAA.mkv
       ├── AAA.nfo
       └── AAA-picture.jpg
    ├── BBB
       ├── BBB.mp4
       ├── BBB.srt
       └── BBB-clip.mp4
    └── CCC
         ├── CCC.avi
         ├── CCC.srt
         ├── CCC-clip.mov
         └── CCC.nfo

Los nombres de los archivos varían en longitud y número de palabras, a veces separados por espacios y posiblemente algunos con guiones (además de los que terminan en '-short'. Son principalmente archivos de video con una variedad de formatos / contenedores: mov / mpg / mkv / mp4 / avi / ogg. Algunos están subtitulados. Algunos tienen archivos con metadatos asociados (.nfo o -clip)

Editar: Los archivos principales son videos (aquí es donde me gustaría dibujar el nombre del directorio). Los archivos asociados representan metadatos. Algunos diferentes en nombres solo por la extensión. Hay una media docena de otras variaciones en el nombre de archivo base como -clip.mp4 -clip.mov o -picture.jpg Pensé que si se sugería algo con esos pocos, entonces podría (con suerte) resolver el resto. En resumen, AAA.mkv se mueve a un directorio llamado AAA. Luego, todos los archivos de metadatos que comienzan con AAA se unen (es decir, en este ejemplo: AAA-picture.jpg y AAA.nfo). Entonces, el nombre base es de hecho una subcadena en el caso del archivo AAA-picture.jpg. Diría que probablemente es relativamente seguro usar simplemente el guión como factor de delimitación ... aunque '-clip' o '-picture' en su totalidad sería más seguro.

¿Cómo puedo hacer esto sin tener el síndrome del túnel carpiano? Miré esto, pero era lo suficientemente diferente como para que mis débiles habilidades de secuencias de comandos fracasaran.

Gracias.

MrFinn
fuente
Entonces, según su ejemplo, parece que realmente desea crear directorios que comiencen con alguna cadena, en lugar de crear un directorio para cada nombre de archivo. Entonces, ¿cómo quieres que un script maneje cosas como AAA-picture.jpg ? ¿Qué se supone que es la "regla" que determina cómo crear una carpeta? ¿Qué cantidad del nombre de archivo se debe extraer? Por supuesto, podemos dividir nombres de archivos usando .y -como separadores y extraer la primera parte. Pero también dices que pueden separarse por espacios y otros caracteres, entonces pregunta si siguen la misma regla, AAAluego separador y luego otro texto.
Sergiy Kolodyazhnyy
@Serg Creo que simplemente quiere un subdirectorio para cada nombre de archivo (base).
Jacob Vlijm el
@JacobVlijm mira el segundo ejemplo. Si esto fuera una simple extracción de nombre base, tendría un directorio para nombre base AAAy AAA-picture. En este caso, sin embargo, quiere ambos AAA.mkvy AAA-picture.jpgestar debajo de la AAAcarpeta. Esto no es una extracción de nombre base, es una extracción de subcadena, o al menos dividir el nombre de archivo en múltiples separadores y usar la primera cadena como nombre de directorio.
Sergiy Kolodyazhnyy
¡Tienes razón! Me lo perdí. Sin embargo, creo que es un error tipográfico, mirando la segunda carpeta.
Jacob Vlijm
Los errores tipográficos de @JacobVlijm no ocurren 3 veces :) Lo mismo con BBB-clip.mp4yCCC-clip.mov
Sergiy Kolodyazhnyy

Respuestas:

5

Si bien su pregunta está etiquetada bash, sería un tanto problemático (en mi humilde opinión) usarlo bashpara dicha tarea. Sugeriría usar Python porque tiene muchas buenas funciones para tareas complejas y esta respuesta proporciona una solución usando ese lenguaje.

Esencialmente, lo que ocurre aquí es que usamos expresiones regulares para dividir nombres de archivos en múltiples delimitadores, obtener solo la primera parte y usar un conjunto único de esas primeras partes como nombres básicos para nuevos directorios.

Luego recorremos el directorio superior nuevamente y clasificamos los archivos en sus lugares apropiados.

El script no hace nada espectacular, y en realidad en el análisis de algoritmos esto no funcionaría demasiado bien, debido a los bucles anidados, pero para una solución "rápida y sucia, pero viable" está bien. Si está interesado en lo que hace cada línea, hay muchos comentarios agregados para explicar la funcionalidad

Tenga en cuenta que la demostración solo muestra la impresión de los nuevos nombres de archivo solo con fines de prueba. Descomente la os.rename()parte para mover realmente el archivo.

La demo

bash-4.3$ # Same directory structure as in OP example
bash-4.3$ ls TESTDIR
bash-4.3$ # now run script
AAA  AAA.mkv  AAA.nfo  AAA-picture.jpg  BBB  BBB-clip.mp4  BBB.mp4  BBB.srt
bash-4.3$ ./collate_files.py ./TESTDIR
/home/xieerqi/TESTDIR/AAA/AAA-picture.jpg
/home/xieerqi/TESTDIR/AAA/AAA.mkv
/home/xieerqi/TESTDIR/AAA/AAA.nfo
/home/xieerqi/TESTDIR/BBB/BBB.srt
/home/xieerqi/TESTDIR/BBB/BBB.mp4
/home/xieerqi/TESTDIR/BBB/BBB-clip.mp4

Script en sí

#!/usr/bin/env python
import re,sys,os

top_dir = os.path.realpath(sys.argv[1])

# Create list of items in directory first
# splitting names at multiple separators
dir_list = [os.path.join(top_dir,re.split("[.-]",f)[0])
            for f in os.listdir(top_dir)
]
# Creating set ensures we will have unique
# directory namings
dir_set = set(dir_list)

# Make these directories first
for dir in dir_set:
    if not os.path.exists(dir):
        os.mkdir(dir)

# now get all files only, no directories
files_list = [f for f in os.listdir(top_dir)
              if os.path.isfile(os.path.join(top_dir,f))
]

# Traverse lists of directories and files,
# check if a filename starts with directory
# that we're testing now, and if it does - move
# the file to that directory
for dir in dir_set:
    id_string = os.path.basename(dir)
    for f in files_list:
        filename = os.path.basename(f)
        if filename.startswith(id_string):
           new_path = os.path.join(dir,filename)
           print(new_path)
           #os.rename(f,new_path)

Notas adicionales:

  • El script se puede adaptar para dividir archivos en otros separadores múltiples (en la re.split()función): agregue corchetes internos (es decir "[.-]") agregue los caracteres que desee.
  • La parte móvil se realiza con os.rename()función. Alternativamente, podría import shutily usar la shutil.move()función. Ver https://stackoverflow.com/a/8858026/3701431
Sergiy Kolodyazhnyy
fuente
1
Reemplacé os.mkdir (full_path) con os.mkdir (dir) y eso pareció funcionar y parece funcionar. Luego rehice los comentarios y (como en el enlace que proporcionó) proporcioné el nombre completo de la ruta a os.rename cambiándolo a os.rename ("/ full / path / to / files /" + f, new_path). Esto funcionó en mi directorio de prueba. Ahora solo tengo que trabajar las agallas para aplicarlo en masa a mi directorio completo.
MrFinn
@ MrFinn oops, gracias por detectar el error épico. Eso es lo que sucede cuando escribo guiones en la cabeza privada de sueño. Lo editaré una vez que esté en mi computadora portátil nuevamente.
Sergiy Kolodyazhnyy
Aprecio tu trabajo. Reemplacé mi ruta codificada con os.path.abspath (top_dir) + "/" + f ... que probablemente todavía no sea tan elegante como podría ser, pero parece funcionar.
MrFinn
9

Hice un pequeño script bash para hacer esto, simplificado y mejorado gracias a los comentarios de OP, @dannysauer, @Arronical y @Scott

#!/bin/bash
for file in *
  do mkdir -p "${file%%[.-]*}" 2>/dev/null
    if [[ -d "${file%%[.-]*}" ]]; then
       if [[ -f "$file" ]]; then
         echo mv -v -- "$file" "${file%%[.-]*}"
       fi
    fi
done

Ejecutar con echoprimero y luego eliminar echopara mover realmente los archivos. La secuencia de comandos debe ejecutarse desde el directorio donde desea mover los archivos. Si lo prefiere, aquí está como un comando de una línea:

for file in *; do mkdir -p "${file%%[.-]*}"; if [[ -d "${file%%[.-]*}" ]]; then if [[ -f "$file" ]]; then echo mv -v -- "$file" "${file%%[.-]*}"; fi ; fi ; done

(de nuevo, eliminar echodespués de la prueba)

Explicación:

  • for file in *; do mkdir -p "${file%%[.-]*}"haga un directorio con el nombre de la primera parte del nombre de cada archivo (hasta el primer guión o punto) La -pbandera es muy importante aquí; sin ella, el script moverá solo el primer archivo coincidente (gracias a Arronical por señalar eso -pevitará que mkdirintentes crear directorios existentes y te quejes de ello )
  • 2>/dev/null el script se queja de que no puede crear un directorio con el mismo nombre que él mismo (pero aún funciona), por lo que descartamos el error; esto no es necesario cuando se ejecuta como una línea
  • if [[ -d "${file%%[.-]*}" ]]; thensi hay un directorio con ese nombre (si mkdirfue exitoso) entonces ...
  • if [[ -f "$file" ]] si estamos tratando con un archivo (no un directorio u otra cosa) entonces ...
  • mv -v -- "$file" "${file%%[.-]*}" moverlo al directorio correspondiente.
Zanna
fuente
Para evitar crear un directorio para el script en sí, también se podría convertir esto en una ~/.bashrcfunción;)
Sergiy Kolodyazhnyy
1
Puede usar mkdir -pque no se queja de directorios ya existentes.
Arronico
Algunos de los nombres de archivo tienen espacios y / o guiones bajos. Después de un poco de expresiones regulares en Google, reemplacé [: alnum:] con [a-zA-Z0-9 \ _] pero luego parece que retoma la extensión (¿incluye el punto?) Y me encuentro perdida (ya que no puede crear un directorio con el mismo nombre que el archivo).
MrFinn
(1) Para evitar crear un directorio para la secuencia de comandos en sí (y mover la secuencia de comandos allí), coloque la secuencia de comandos en un directorio diferente o asígnele un nombre .. (2) La línea 7 de la secuencia de comandos se refiere a filenames, que, en ese punto, es igual al último nombre de archivo (lexicográficamente) en el directorio. ¿A qué te refieres files? (3) En la línea 6, ¿por qué no solo decir for files in "$dirs".* "$dirs"-*? (4) Como cuestión de estilo, prefiero decir for dir in …y for file in …; es decir, use palabras singulares para la variable de índice de bucle (que toma solo un valor a la vez).
Scott
1
@dannysauer jaja tan cierto, estoy pensando "NO intente usar expresiones regulares en el caparazón" pero realmente, no hay excusa para no pensar en una clase de char. He editado, y la primera prueba es probablemente exagerada (y tal vez inútil), pero estoy muy feliz de que funcione mejor y no se analice find. Te debo un trago :)
Zanna
5

En un pequeño script de Python:

#!/usr/bin/env python3
import shutil
import os
import sys

dr = sys.argv[1]

for f in os.listdir(dr):
    split = f.rfind("."); short = f.find("-")
    if split != -1:
        extension = f[split:]
        newname = f[:short] if short != -1 else f[:split]
        target = os.path.join(dr, newname)
        if not os.path.exists(target):
            os.mkdir(target)
        shutil.move(os.path.join(dr, f), os.path.join(target, f))

Para usarlo:

  • copiarlo en un archivo vacío
  • Guárdalo como move_into.py
  • Ejecútelo con el directorio como argumento:

    python3 /path/to/move_into.py /path/to/directory

El script asume que todos los archivos (relevantes) tienen extensiones. Si un archivo no tiene extensión, no pasa nada con él. Si eso es un problema, por favor mencione que se puede cambiar fácilmente.

Explicación

  • El script busca una posible extensión.
  • Si no está presente, el script deja el archivo (o directorio) solo.
  • De lo contrario, el archivo se dividirá por "-", si está presente, la primera sección se utilizará posteriormente para crear carpetas (si es necesario)
  • Si no, el nombre base del archivo se usa para nombrar la carpeta.

Posteriormente, el archivo se mueve a la carpeta correspondiente.

Jacob Vlijm
fuente
@Serg es por eso que utilicé rfind, solo para encontrar el último punto, separando la extensión.
Jacob Vlijm el
1
@Serg editó mi respuesta.
Jacob Vlijm el