Filtra archivos en Ruby: una guía práctica
El filtrado eficiente de archivos es esencial tanto para procesar registros como para gestionar contenido multimedia. Ruby ofrece herramientas sólidas para seleccionar archivos que van más allá de la simple coincidencia de patrones. Exploremos técnicas prácticas que se adaptan a escenarios desde básicos hasta avanzados.
Requisitos
Los siguientes ejemplos se probaron con Ruby 3.4.10 y mime-types 3.6.0, usando el registro mime-types-data 3.2024.0806. Usa una versión de Ruby con mantenimiento activo para proyectos nuevos. Solo el ejemplo de consulta de tipos MIME necesita la gema mime-types. En un proyecto existente con Bundler, añade:
# Add to your Gemfile
gem 'mime-types', '3.6.0'
Ejecuta bundle install y luego ejecuta tus scripts con bundle exec ruby script.rb.
Fuera de Bundler, instala la misma versión directamente:
gem install mime-types -v 3.6.0
Guarda los ejemplos de Ruby en un archivo .rb y ejecútalo con
ruby script.rb. Reemplaza los directorios de ejemplo por tus propias rutas.
Los resultados son arrays o enumeradores; usa p para inspeccionarlos,
por ejemplo, p recent_images después del ejemplo completo de
FileFilter.
La gema mime-types emplea un versionado semántico modificado para registrar tanto los cambios de la API como las actualizaciones de los datos del registro. Para obtener más detalles, consulta la documentación de mime-types.
Métodos básicos de filtrado
La biblioteca estándar de Ruby ofrece varias soluciones directas para filtrar archivos:
# Filter by extension
docs_dir = '/docs'
pdf_files = Dir.children(docs_dir).select do |f|
File.file?(File.join(docs_dir, f)) && File.extname(f) == '.pdf'
end
# Filter by size (1MB threshold)
large_files = Dir.glob('*').select do |f|
begin
File.file?(f) && File.size(f) > 1_000_000
rescue Errno::ENOENT, Errno::EACCES => e
warn "Error accessing #{f}: #{e.message}"
false
end
end
# Filter by modification time (last 24 hours)
recent_files = Dir.glob('*').select do |f|
begin
File.file?(f) && File.mtime(f) > (Time.now - 86400)
rescue Errno::ENOENT, Errno::EACCES => e
warn "Error accessing #{f}: #{e.message}"
false
end
end
Estos métodos usan las utilidades de la clase File para realizar
comprobaciones rápidas sin cargar el contenido de los archivos. La comprobación de extensiones
devuelve nombres relativos a docs_dir y distingue entre mayúsculas y minúsculas.
Los ejemplos con glob devuelven rutas relativas al directorio de trabajo actual y omiten los archivos
cuyo nombre comienza con un punto. File.file? excluye directorios, pero sigue
los enlaces simbólicos a archivos regulares; estas comprobaciones no constituyen un entorno aislado
del sistema de archivos. Los filtros de tiempo usan un límite inferior, por lo que los archivos
con fechas futuras también coinciden.
Coincidencia avanzada de patrones con glob
Dir.glob de Ruby admite la coincidencia de patrones al estilo UNIX con
algunas mejoras específicas de Ruby:
# Match nested Markdown files
markdown_files = Dir.glob('**/*.md').select { |f| File.file?(f) }
# Match names containing a January 2024 date (not modification times)
jan_files = Dir.glob('*').grep(/(2024-01-\d{2})/).select { |f| File.file?(f) }
# Combined size and type filter
big_images = Dir.glob('*.{jpg,png}').select do |f|
begin
File.file?(f) && File.size(f) > 500_000
rescue Errno::ENOENT, Errno::EACCES => e
warn "Error accessing #{f}: #{e.message}"
false
end
end
Usa el asterisco doble (**) para recorrer directorios de forma recursiva
y la expansión de llaves para incluir varias extensiones.
Consulta de tipos MIME
Usa la gema mime-types para consultar el tipo de medio asociado con la extensión
de un nombre de archivo:
require 'mime/types'
def media_files(dir)
Dir.children(dir).select do |f|
begin
next unless File.file?(File.join(dir, f))
MIME::Types.type_for(f).any? do |mime|
mime.media_type == 'image' || mime.media_type == 'video'
end
rescue StandardError => e
warn "Error processing #{f}: #{e.message}"
false
end
end
end
# Usage:
visual_assets = media_files('/content/assets')
Este enfoque usa un registro de extensiones; no inspecciona el contenido de los archivos ni valida
que un archivo sea seguro. Un nombre de archivo puede tener varios tipos registrados:
.mp4, por ejemplo, tiene coincidencias tanto de aplicación como de video
en el registro probado. Comprobar cada coincidencia evita descartarlo solo porque la primera no es
un tipo de video. Aun así, esto no demuestra que el archivo contenga video.
Inspecciona el contenido por separado cuando aceptes subidas no confiables.
Para obtener más detalles, consulta la
documentación de la gema mime-types.
Filtrado por metadatos
Combina varios metadatos para lograr una selección precisa:
def recent_documents(path)
Dir.glob('*', base: path).map { |name| File.join(path, name) }.select do |f|
begin
next unless File.file?(f)
ext = File.extname(f).downcase
size = File.size(f)
modified = File.mtime(f)
(ext == '.pdf' || ext == '.docx') &&
size.between?(10_000, 5_000_000) &&
modified > (Time.now - 7*86400)
rescue StandardError => e
warn "Error processing #{f}: #{e.message}"
false
end
end
end
Esto selecciona archivos PDF/DOCX directamente dentro de path, sin distinguir
entre mayúsculas y minúsculas en las extensiones y con tamaños de 10.000 a 5.000.000 bytes, ambos
inclusive. Se excluyen los archivos modificados hace exactamente siete días.
Pasar el directorio como base: mantiene los caracteres de su nombre,
como [, fuera del patrón glob.
Consideraciones de rendimiento
Al procesar directorios grandes:
-
Evaluación diferida: Pospón las comprobaciones de metadatos con
lazy.Dir.globsigue construyendo su array de resultados.Dir.glob('**/*').lazy .select { |f| File.file?(f) && File.size(f) > 1_000_000 } .first(10) -
Salida anticipada: Interrumpe la ejecución de inmediato con
breakcuando sea posiblelog_dir = '/logs' Dir.children(log_dir).each do |name| f = File.join(log_dir, name) begin next unless File.file?(f) && f.end_with?('.log') break if File.size(f) > 1_000_000_000 # Stop at first huge log process_log(f) rescue StandardError => e warn "Error processing #{f}: #{e.message}" end endDefine
process_log(path)para el procesamiento de tu aplicación antes de ejecutar este fragmento. -
Caché de metadatos: Almacena los datos que se consultan con frecuencia
file_cache = {} Dir.glob('*').each do |f| begin file_cache[f] = { mtime: File.mtime(f), size: File.size(f) } rescue StandardError => e warn "Error caching #{f}: #{e.message}" end end
Ejemplo apto para producción
Este selector reutilizable devuelve un enumerador de rutas dentro del directorio proporcionado:
class FileFilter
def initialize(root_dir)
@root = root_dir
end
def find_files(extensions: [], min_size: 0, max_age: Float::INFINITY)
Dir.glob('**/*', base: @root).lazy.map { |name| File.join(@root, name) }.select do |path|
begin
next unless File.file?(path)
valid_extension = extensions.empty? || extensions.include?(File.extname(path))
valid_size = File.size(path) >= min_size
valid_age = (Time.now - File.mtime(path)) < max_age
valid_extension && valid_size && valid_age
rescue StandardError => e
warn "Error processing #{path}: #{e.message}"
false
end
end
end
end
# Usage:
filter = FileFilter.new('/user/uploads')
recent_images = filter.find_files(
extensions: ['.jpg', '.png'],
min_size: 100_000,
max_age: 3600 # 1 hour
).first(100)
El ejemplo de uso selecciona hasta 100 rutas .jpg o
.png de al menos 100.000 bytes, modificadas hace menos de una hora.
Aquí, la coincidencia de extensiones distingue entre mayúsculas y minúsculas. La búsqueda sigue
construyendo un array de glob; lazy pospone las comprobaciones de metadatos,
no el recorrido. Se omiten los archivos ocultos, se siguen los enlaces simbólicos a archivos y
no se recorren de forma recursiva los directorios enlazados simbólicamente. Si el directorio raíz
no existe, no se obtienen coincidencias. Los archivos pueden cambiar después de la selección,
así que vuelve a manejar los errores cuando los abras.
Manejo de la codificación de caracteres de los archivos
Ruby suele manejar automáticamente la codificación de caracteres de los nombres de archivo cuando se usa UTF-8. Sin embargo, si encuentras problemas con caracteres no ASCII en los nombres de archivo, crea un valor de visualización en UTF-8 de la siguiente manera:
# Convert from Ruby's known source encoding and replace invalid bytes for display
utf8_labels = Dir.glob('*').map do |f|
f.encode('UTF-8', invalid: :replace, undef: :replace).scrub
end
Conserva la ruta original para las operaciones del sistema de archivos. Los caracteres de reemplazo
pueden cambiar un nombre de archivo; estas etiquetas son solo para visualización.
force_encoding únicamente cambia la etiqueta de codificación de los bytes,
sin convertirlos a otra codificación.
Conclusión
A lo largo de esta guía, exploramos técnicas prácticas para filtrar archivos en Ruby, desde comprobaciones simples de extensiones y patrones glob hasta consultas de tipos MIME basadas en nombres de archivo y filtrado por metadatos. Al combinar estos métodos, puedes construir pipelines eficientes de procesamiento de archivos adaptados a tus necesidades. Para soluciones a gran escala listas para producción, considera explorar la sólida API de procesamiento de archivos que ofrece Transloadit. Visita nuestra documentación para obtener más información.
