Документация Engee
Notebook

Продвинутая работа с файлами

Представим ситуацию - у нас есть несколько наборов данных, которые раскинуты по папкам, причем в папках может быть еще и мусор (картинки со смешными енотами, текстовые документы). Хотелось бы иметь возможность найти нужные файлы для последующей работы с ними. Проблема заключается в том, что Julia реализует только базовую работу с файлами - https://engee.com/helpcenter/stable/ru-en/julia/base/file.html, что явно недостаточно для такой задачи.

В этой публикации я сделаю более умный инструмент для работы с файлами, используя структуру данных, известную как дерево.

Создаем дерево

Сделаем наблюдение, что структура каталогов и файлов поразительно напоминает дерево. Деревья в программировании - это структура данных, описывающая специальный вид графов. У таких графов все узлы, кроме "корневого" имеют одного родителя:

image.png

Для таких графов известны оптимальные алгоритмы обхода графа и изменения графа и они зачастую уже реализованы. Моя идея следующая: представить содержимое папки и ее подпапок как дерево, каждый узел которого будет файлом или папкой. Каждый узел будет хранить отдельно путь, имя, расширение, дату и время создания и изменения, а так же признак папки. А что бы организовать древовидную структуру я буду хранить "потомков" этого узла:

In [ ]:
using Dates

struct FileTreeNode
    path::String
    name::String
    ext::String
    isdir::Bool
    created::DateTime
    modified::DateTime
    children::Vector{FileTreeNode}
end

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

Чтобы получить дату и время создания и изменения файла, а также для получения имени, пути и расширения файла я напишу дополнительные функции:

In [ ]:
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
Out[0]:
split_name_ext (generic function with 1 method)

Растим дерево

Необходимые структуры данных и вспомогательные функции созданы, и можно переходить к реализаци дерева структуры каталогов.

Создадим функцию, которая будет получать время создания и изменения файла или папки, имя и расширения для некоторого пути path. Далее, при помощи readdir получим список файлов и папок внутри текущей папки.

Для каждой из обнаруженных папок повторим такую же операцию. То есть наша функция будет вызывать сама себя. Такой прием называется рекурсией.

Добавим к нашей функции еще одно ограничение: глубину поиска. Такое ограничение ограничит уровень вложенности папок для обхода и ускорит построение дерева.

В результате работы этой функции получим узел дерева.

In [ ]:
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
Out[0]:
build_tree (generic function with 1 method)

Далее, упростим задачу визуализации дерева с помощью библиотеки AbstractTrees.jl. Определим две новые функции для нашего дерева:

  • children() - для получения потомков узла дерева
  • printnode() - для вывода узла на экран
In [ ]:
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
   Resolving package versions...
   Installed FiniteDiff ──────────── v2.29.0
   Installed LazyArrays ──────────── v2.9.5
   Installed LineSearch ──────────── v0.1.6
   Installed DiffEqCallbacks ─────── v4.12.0
   Installed SparseMatrixColorings ─ v0.4.26
   Installed DiffEqNoiseProcess ──── v5.27.0
     Project No packages added to or removed from `~/.project/Project.toml`
    Manifest No packages added to or removed from `~/.project/Manifest.toml`
Out[0]:
printnode (generic function with 5 methods)

Готово! Давайте проверим, что дерево собирается:

In [ ]:
tree = build_tree(@__DIR__,maxdepth=1)
using AbstractTrees: print_tree
print_tree(tree)
📁 FileOps
├─ 📁 .git
├─ 📁 DataDepot
├─ 📄 dirprint.ngscript
├─ 📄 filetree_ops.ngscript
└─ 📄 filetree_ops_1.ipynb

Применяем дерево на практике

Давайте склеим 3 набора данных, расположенных в папке DataDepot.

Сначала, посмотрим что у нас в папке:

In [ ]:
tree = build_tree(joinpath(@__DIR__,"DataDepot"),maxdepth=2)
print_tree(tree)
📁 DataDepot
├─ 📁 S1
│  ├─ 📄 data1.csv
│  └─ 📄 trash.rc
├─ 📁 S2
│  ├─ 📄 data2.csv
│  └─ 📄 notsotrash.dc
└─ 📁 S3
   └─ 📄 data3.csv

Затем, получим все *.csv-файлы, обойдя созданное выше дерево:

In [ ]:
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")
["/user/work/FileOps/DataDepot/S1/data1.csv"]
["/user/work/FileOps/DataDepot/S2/data2.csv"]
["/user/work/FileOps/DataDepot/S3/data3.csv"]
Out[0]:
3-element Vector{String}:
 "/user/work/FileOps/DataDepot/S1/data1.csv"
 "/user/work/FileOps/DataDepot/S2/data2.csv"
 "/user/work/FileOps/DataDepot/S3/data3.csv"

А теперь загрузим их и склеим:

In [ ]:
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)
Прочитано 10 строк из файла /user/work/FileOps/DataDepot/S1/data1.csv
Прочитано 10 строк из файла /user/work/FileOps/DataDepot/S2/data2.csv
Прочитано 10 строк из файла /user/work/FileOps/DataDepot/S3/data3.csv
Out[0]:
30×3 DataFrame
5 rows omitted
Rowx1x2x3
Float64Float64Float64
10.8947660.736590.0567356
20.7408720.4223710.447714
30.6577770.8259970.168484
40.8940870.02343830.256112
50.8466650.03092550.895526
60.4627590.8838610.8525
70.1848760.4524160.432112
80.6785190.3619010.00114721
90.07336790.1528410.837117
100.5375860.1761290.0318991
110.8328410.3048720.843521
120.2476430.6342890.639223
130.3063910.0744850.85542
190.7236510.01892840.845076
200.288120.9466720.700205
210.8161040.01173130.145698
220.2942450.2190640.343588
230.3882120.5163990.235681
240.4011220.4960150.420165
250.1296090.7331280.098028
260.659690.1508910.102492
270.9379920.9765670.137387
280.8437420.8598980.522218
290.2764890.880830.271357
300.6450550.9117670.108152

Выводы

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