Файл

Файл — именованная область данных на носителе информации. То же: file.

В современных операционных системах файл используется как базовый объект для манипуляции данными. Файл это абстракция над носителями информации. Человеку и программам удобно представлять данные на носителе не как последовательность (очень длинную) нолей и единиц, а как объединение этих нолей и единиц в некую общую самостоятельную сущность. Файлы не пересекаются.

У каждого файла есть имя, например /home/user/.bashrc. Также доступна некоторая другая информация, например время последней модификации. В разных операционных системах может быть разная специфика работы с файлами.

Количество битов в файле всегда делится на 8 (файл состоит из байтов), размер файла поэтому принято считать в байтах.

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

Работа с файлами и термами

В первом случае нам надо сосредоточиться на аккуратной работе с файлами, здесь мы должны предусмотреть разные ситуации (например, что какой-то файл вдруг перестанет быть доступен для чтения). Во втором случае мы решаем главные, то есть творческие задачи. Тут мы стараемся написать как можно более изящный и эффективный код.

Модули для работы с файлами

В Эрланге для работы с файлами предусмотрен ряд модулей.

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

filename предназначен для манипулирования именами файлов, причём в платформо-независимой манере.

filelib это расширение для file — он содержит разные дополнительные функции (например, проверка на тот или иной тип файла).

io нужен для работы над открытыми файлами: разбор данных, запись форматированных данных в файл и др.

Способы чтения файлов

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

Для начала создадим какой-нибудь файл. Воспользуемся уникальной возможностью Эрланга — из текстового файла извлекать термы единой функцией. Это можно сделать не из любого текстового файла: термы должны быть правильно сформированы, после каждого должна стоять точка. Создадим файл cats такого содержания:

#{name => "Вася", age => 5}.
{name, "Муся", age, 3}.
"Где же кошка? Не видна. Ну-ка поглядим-ка. Где же кошка? Где она, кошка-невидимка?".

Раз содержимое любого файла это бинарник, значит, иногда нам будет надо получить целиком этот бинарник (без каких-либо изменений) и привязать этот бинарник в качестве терма переменной. Для этого есть функция file:read_file/1.

1> {ok,Bin} = file:read_file("cats").
{ok,<<35,123,110,97,109,101,32,61,62,32,34,208,146,208,
      176,209,129,209,143,34,44,32,97,103,101,32,61,...>>}
2> Bin.
<<35,123,110,97,109,101,32,61,62,32,34,208,146,208,176,
  209,129,209,143,34,44,32,97,103,101,32,61,62,32,...>>

Часто бывает нужно получить содержимое текстового файла в качестве списка целых чисел, каждое из которых отражает соответствующий юникод-символ. В этом случае содержимое бинарника надо прогнать через unicode:characters_to_list/1.

4> Txt = unicode:characters_to_list(Bin).
[35,123,110,97,109,101,32,61,62,32,34,1042,1072,1089,1103,
 34,44,32,97,103,101,32,61,62,32,53,125,46,10|...]
5> io:format("~ts~n", [Txt]).
#{name => "Вася", age => 5}.
{name, "Муся", age, 3}.
"Где же кошка? Не видна. Ну-ка поглядим-ка. Где же кошка? Где она, кошка-невидимка?".

ok

Файл может быть огромным. Если при этом нас интересует лишь определённый фрагмент, то его можно прочитать с помощью file:pread/3.

6> {ok,F} = file:open("cats", [read,binary,raw]).
{ok,{file_descriptor,prim_file,
                     #{handle => #Ref<0.3533174665.2132672521.84350>,
                       owner => <0.84.0>,
                       r_buffer => #Ref<0.3533174665.2132672513.84574>,
                       r_ahead_size => 0}}}
7> file:pread(F,2,4).
{ok,<<"name">>}
8> file:pread(F,11,8).
{ok,<<208,146,208,176,209,129,209,143>>}
9> file:close(F).
ok

Текстовые файлы часто читают построчно. Для этого есть функция io:get_line/2. Чтобы получить юникод-числа, нам придётся добавить опцию {encoding,utf8} для file:open/2.

1> {ok,F} = file:open("cats", [read,{encoding,utf8}]).
{ok,<0.86.0>}
2> io:get_line(F, '').
[35,123,110,97,109,101,32,61,62,32,34,1042,1072,1089,1103,
 34,44,32,97,103,101,32,61,62,32,53,125,46,10]
3> io:get_line(F, '').
[123,110,97,109,101,44,32,34,1052,1091,1089,1103,34,44,32,
 97,103,101,44,32,51,125,46,10]
4> io:get_line(F, '').
[34,1043,1076,1077,32,1078,1077,32,1082,1086,1096,1082,1072,
 63,32,1053,1077,32,1074,1080,1076,1085,1072,46,32,1053,1091,
 45,1082|...]
5> io:get_line(F, '').
eof
7> file:close(F).
ok

Мы специально создали текстовый файл, в котором перечислены термы. Это может быть удобно для ввода некоторых данных в нашу программу. Автор текстового файла (человек или другая программа) будет в привычной манере определять, где просто строка или число, где кортеж, где список, где карта. Для разбора таких текстовых файлов существует file:consult/1.

1> file:consult("cats").
{ok,[#{name => "Вася",age => 5},
     {name,"Муся",age,3},
     "Где же кошка? Не видна. Ну-ка поглядим-ка. Где же кошка? Где она, кошка-невидимка?"]}

В результате мы получили список из трёх термов. Этот список вместе с атомом ok завёрнут в кортеж.

Опять же, файл с термами может быть огромным, и нам тогда для экономии памяти захочется читать строки с термами по одной. Для этих целей есть io:read/2. Перед его использование файл также надо открыть, а потом закрыть.

1> {ok,F} = file:open("cats", read).
{ok,<0.86.0>}
2> io:read(F, '').
{ok,#{name => [208,146,208,176,209,129,209,143],age => 5}}
3> io:read(F, '').
{ok,{name,[208,156,209,131,209,129,209,143],age,3}}
4> io:read(F, '').
{ok,[208,147,208,180,208,181,32,208,182,208,181,32,208,186,
     208,190,209,136,208,186,208,176,63,32,208,157,208|...]}
5> io:read(F, '').
eof
6> file:close(F).
ok

Если передают данные друг другу две Эрланг-программы, более эффективным является использовать для этих целей специальные бинарники — с типом ext_binary(). Одна программа создаёт расширенный бинарник с помощью бифа term_to_binary/1, сохраняет его в файл. Другая программа читает файл в бинарник, а потом этот бинарник преобразует в единый терм с помощью бифа binary_to_term/1. Предположим, у нас есть специальный такой файл cats.bin…

1> {ok,Bin} = file:read_file("cats.bin").
{ok,<<131,108,0,0,0,3,116,0,0,0,2,119,4,110,97,109,101,
      108,0,0,0,4,98,0,0,4,18,...>>}
2> binary_to_term(Bin).
[#{name => [1042,1072,1089,1103],age => 5},
 {name,[1052,1091,1089,1103],age,3},
 [1043,1076,1077,32,1078,1077,32,1082,1086,1096,1082,1072,63,
  32,1053,1077,32,1074,1080,1076,1085,1072,46,32,1053,1091|...]]

Мы получили терм, полностью готовый к дальнейшему употреблению. В частности, юникод-символы отображаются как им положено — в виде чисел вроде 1042 или 1072.

Способы записи файлов

Запись файлов, можно сказать, это процесс, обратный чтению файлов. Можно ожидать наличия зеркальных функций, но это не всегда так. Есть стандартная функция file:consult/1, но нет обратной. Этот механизм в отличие от пары бифов term_to_binary и binary_to_term не является универсальным для обмена термами через файлы. С помощью file:consult/1 можно прочитать не все термы из текстового файла.

Если, тем не менее, есть желание задействовать механизм consult, можно для записи использовать io:format/3, которая как раз умеет выводить структурированное содержание терма. После ~p надо не забыть поставить точку.

1> Cats = {first, "Вася", second, "Мурзик"}.
{first,[1042,1072,1089,1103],
       second,
       [1052,1091,1088,1079,1080,1082]}
2> {ok,F} = file:open("cats2", write).
{ok,<0.89.0>}
3> io:format(F, "~p.~n", [Cats]).
ok

Проверим содержимое файла.

$ cat cats2
{first,[1042,1072,1089,1103],second,[1052,1091,1088,1079,1080,1082]}.

Это, может быть, не совсем то, что мы ожидали увидеть. Однако другой программе на Эрланге будет всё равно, в каком виде мы задаём юникод-текст.

$ erl +pc unicode
1> file:consult("cats2").
{ok,[{first,"Вася",second,"Мурзик"}]}

Содержимое любого файла, как мы уже говорили, это бинарник, поэтому логичным и часто самым эффективным способом (если мы не записываем чрезвычайно огромный файл) является сначала породить терм-бинарник, а потом уже записать его в файл с помощью file:write/2.

Перед тем как породить терм-бинарник, хорошо бы знать, в каких целях мы будем потом использовать полученный файл. Если нам нужен рядовой файл, то это одно, если мы желаем сохранить (передать) файл с термами, лучше воспользоваться расширенным бинарным форматом.

1> Text = "Мама мыла раму.".
[1052,1072,1084,1072,32,1084,1099,1083,1072,32,1088,1072,
 1084,1091,46]
2> Bin1 = unicode:characters_to_binary(Text).
<<208,156,208,176,208,188,208,176,32,208,188,209,139,208,
  187,208,176,32,209,128,208,176,208,188,209,131,46>>
3> Bin2 = term_to_binary(Text).
<<131,108,0,0,0,15,98,0,0,4,28,98,0,0,4,48,98,0,0,4,60,98,
  0,0,4,48,97,32,98,...>>
4> file:write_file("file1", Bin1).
ok
5> file:write_file("file2", Bin2).
ok

Размер первого файла будет 27 байтов, второго (расширенного) — 73.

$ cat file1
Мама мыла раму.
$ cat file2
�lbb0b<b0a b<bKb;b0a b@b0b<bCa.j

Для записи бинарника в файл мы использовали предназначенную для этих целей file:write_file/2.

Чтобы произвести запись в файл в определённой позиции, можно использовать file:pwrite/3.

Работа с каталогами

В модуле file есть три полезные функции для работы с каталогами:

Если операция не может быть совершена, будет сообщение об ошибке с указанием её кода.

1> file:make_dir("tmp").
ok
2> file:list_dir("tmp").
{ok,[]}
3> file:make_dir("tmp/hello").
ok
4> file:make_dir("tmp/hello").
{error,eexist}
5> file:del_dir("tmp").
{error,eexist}
6> file:del_dir("tmp/hello/").
ok
7> file:del_dir("tmp").
ok

Как мы видим, нельзя создать каталог, если он уже существует. Также будет ошибка, если мы попытаемся удалить каталог, содержащий файлы. Перед удалением всего каталога эти файлы надо удалить.

Получение информации о файле

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

1> file:read_file_info("cats").
{ok,{file_info,26,regular,read_write,
               {{2025,3,11},{11,13,48}},
               {{2025,3,11},{11,13,41}},
               {{2025,3,11},{11,13,41}},
               33188,1,65026,0,4726443,1000,1000}}

Чтобы узнать (вспомнить), что это значит, придётся обратиться к справочнику по функции.

Копирование и удаление файлов

Копированию файлов служит функция file:copy с арностью 2 или 3. В последнем случае можно задать, сколько байтов мы желаем скопировать из одного файла в другой.

$ echo "Раз, два, три, четыре, пять, вышел зайчик погулять." > text
$ erl
1> file:copy("text", "text2").
{ok,90}
2> file:copy("text", "text3", 22).
{ok,22}
3> q().
ok
$ cat text2
Раз, два, три, четыре, пять, вышел зайчик погулять.
$ cat text3
Раз, два, три

Для удаления файлов применяется file:delete(File).

1> file:delete("text").
ok
2> file:delete("text2").
ok
3> file:delete("text3").
ok
4> file:delete("text3").
{error,enoent}

Как мы видим, нельзя удалить то, чего нет.


Copyright © 2025 Алексей Карманов