Продвинутая работа с файлами
Представим ситуацию - у нас есть несколько наборов данных, которые раскинуты по папкам, причем в папках может быть еще и мусор (картинки со смешными енотами, текстовые документы). Хотелось бы иметь возможность найти нужные файлы для последующей работы с ними. Проблема заключается в том, что Julia реализует только базовую работу с файлами - https://engee.com/helpcenter/stable/ru-en/julia/base/file.html, что явно недостаточно для такой задачи.
В этой публикации я сделаю более умный инструмент для работы с файлами, используя структуру данных, известную как дерево.
Создаем дерево
Сделаем наблюдение, что структура каталогов и файлов поразительно напоминает дерево. Деревья в программировании - это структура данных, описывающая специальный вид графов. У таких графов все узлы, кроме "корневого" имеют одного родителя:
Для таких графов известны оптимальные алгоритмы обхода графа и изменения графа и они зачастую уже реализованы. Моя идея следующая: представить содержимое папки и ее подпапок как дерево, каждый узел которого будет файлом или папкой. Каждый узел будет хранить отдельно путь, имя, расширение, дату и время создания и изменения, а так же признак папки. А что бы организовать древовидную структуру я буду хранить "потомков" этого узла:
using Dates
struct FileTreeNode
path::String
name::String
ext::String
isdir::Bool
created::DateTime
modified::DateTime
children::Vector{FileTreeNode}
end
Таким образом, я смогу обходить дерево файлов и папок намного проще и быстрее, чем если бы использовал стандартную библиотеку Julia.
Чтобы получить дату и время создания и изменения файла, а также для получения имени, пути и расширения файла я напишу дополнительные функции:
function get_metadata(path::String)
st = stat(path)
created = unix2datetime(st.ctime)
modified = unix2datetime(st.mtime)
return created, modified
end
function split_name_ext(path::String)
name = basename(path)
base, ext = splitext(name)
return base, ext
end
Растим дерево
Необходимые структуры данных и вспомогательные функции созданы, и можно переходить к реализаци дерева структуры каталогов.
Создадим функцию, которая будет получать время создания и изменения файла или папки, имя и расширения для некоторого пути path. Далее, при помощи readdir получим список файлов и папок внутри текущей папки.
Для каждой из обнаруженных папок повторим такую же операцию. То есть наша функция будет вызывать сама себя. Такой прием называется рекурсией.
Добавим к нашей функции еще одно ограничение: глубину поиска. Такое ограничение ограничит уровень вложенности папок для обхода и ускорит построение дерева.
В результате работы этой функции получим узел дерева.
function build_tree(path::String; maxdepth=typemax(Int), depth=0)
is_dir = isdir(path)
name, ext = split_name_ext(path)
created, modified = get_metadata(path)
if is_dir && depth < maxdepth
entries = readdir(path; join=true)
children = [
build_tree(e; maxdepth, depth=depth+1)
for e in sort(entries)
]
else
children = FileTreeNode[]
end
return FileTreeNode(path, name, ext, is_dir, created, modified, children)
end
Далее, упростим задачу визуализации дерева с помощью библиотеки AbstractTrees.jl. Определим две новые функции для нашего дерева:
- children() - для получения потомков узла дерева
- printnode() - для вывода узла на экран
import Pkg
Pkg.add("AbstractTrees")
import AbstractTrees: children, printnode
children(node::FileTreeNode) = node.children
function printnode(io::IO, node::FileTreeNode)
if node.isdir
print(io, "📁 ", node.name)
else
print(io, "📄 ", node.name, node.ext)
end
end
Готово! Давайте проверим, что дерево собирается:
tree = build_tree(@__DIR__,maxdepth=1)
using AbstractTrees: print_tree
print_tree(tree)
Применяем дерево на практике
Давайте склеим 3 набора данных, расположенных в папке DataDepot.
Сначала, посмотрим что у нас в папке:
tree = build_tree(joinpath(@__DIR__,"DataDepot"),maxdepth=2)
print_tree(tree)
Затем, получим все *.csv-файлы, обойдя созданное выше дерево:
function find_files_by_ext(node::FileTreeNode, ext::String,acc=String[])
if !startswith(ext,".")
ext = "."*ext
end
if isequal(node.ext,ext)
push!(acc, node.path)
println("$(acc)")
end
for c in node.children
accchild = find_files_by_ext(c, ext)
if ~isempty(accchild)
append!(acc,accchild)
end
end
return acc
end
csv_files = find_files_by_ext(tree,"csv")
А теперь загрузим их и склеим:
using CSV
df_v = Vector{DataFrame}()
for f in csv_files
df = CSV.read(joinpath(pwd(),f),DataFrame)
println("Прочитано $(nrow(df)) строк из файла $f")
push!(df_v,df)
end
df_v = reduce(vcat,df_v)
Выводы
В данной публикации мы посмотрели на пример применения классических структур данных из программирования для решения практических инженерных задач. В последующих публикациях будут рассмотрены другие приемы программирования, облегчающие задачи технических расчетов.