第十三章 对文件编程
Table of Contents
第十三章 对文件编程
13.1 库的组织结构
四个操作文件的模块
file模块
包含了用于文件打开、关闭、读取、写入和目录列表等功能的函数。
filename模块
以平*立的方式提供了一套操作文件名的函数。
filelib模块
是file模块的扩展, 提供了一套辅助函数用于生成文件列表、检验文件类型等操作。
io模块
提供了一系列对已打开的文件进行操作的函数。
13.2 读取文件的不同方法
测试数据 data1.dat
{person, "joe", "armstrong", [{occupation, programmer}, {favoriteLanguage, erlang}]}. {cat, {name, "zorro"}, {owner, "joe"}}.
file模块API
函数 | 描述 |
---|---|
change_group | 修改一个文件的群组 |
change_owner | 修改一个文件的所有者 |
change_time | 修改一个文件的最近访问或者最近更新时间 |
close | 关闭一个文件 |
consult | 从一个文件中读取Erlang项 |
copy | 复制文件内容 |
del_dir | 删除一个目录 |
delete | 删除一个文件 |
eval | 在文件中对一个Erlang表达式求值 |
format_error | 返回一个描述错误原因的字符串 |
get_cwd | 获取当前工作目录 |
list_dir | 获取一个目录中的文件列表 |
make_dir | 创建一个目录 |
make_link | 为一个文件创建一个硬链接 |
make_symlink | 为一个文件或目录创建符号链接 |
open | 打开一个文件 |
position | 设置一个文件的访问位置 |
pread | 在一个特定的文件访问位置读取文件 |
pwrite | 在一个特定的文件访问位置写入文件 |
read | 从文件中读取内容 |
read_file | 读取整个文件 |
read_file_info | 获取一个文件的信息 |
read_link | 查看一个文件的链接指向 |
read_link_info | 获得一个文件或者链接的信息 |
rename | 重命名一个文件 |
script | 对一个文件中的Erlang表达式求值并返回结果 |
set_cwd | 设定当前的工作目录 |
sync | 把一个文件的内存状态同步到该文件的物理存储上 |
truncate | 截断一个文件 |
write | 向一个文件写入数据 |
write_file | 写入整个文件 |
write_file_info | 修改一个文件的信息 |
13.2.1 从文件中读取所有Erlang数据项
# 读取成功返回{ok, [Term]}, 否则返回{error, Reason} 1> file:consult("data1.dat"). {ok,[{person,"joe","armstrong", [{occupation,programmer},{favoriteLanguage,erlang}]}, {cat,{name,"zorro"},{owner,"joe"}}]}
13.2.2 从文件的数据项中一次读取一项
# 以只读方式打开文件, 成功则返回{ok, IoDevice}, 否则返回{error, Reason} 1> {ok, S} = file:open("data1.dat", read). {ok,<0.35.0>} # 读取数据, 返回{ok, Term} | {error, Why} | eof 2> io:read(S, ''). {ok,{person,"joe","armstrong", [{occupation,programmer},{favoriteLanguage,erlang}]}} 3> io:read(S, ''). {ok,{cat,{name,"zorro"},{owner,"joe"}}} 4> io:read(S, ''). eof # 关闭IoDevice, 返回 ok | {error, Why} 5> file:close(S). ok
利用以上函数实现file模块中的consult函数
consult(File) -> case file:open(File, read) of {ok, S} -> %% 成功打开文件后将IoDevice交由consult1函数处理 Val = consult1(S), file:close(S), {ok, Val}; {error, Why} -> {error, Why} end. consult1(S) -> case io:read(S, '') of %% 循环使用io:read逐项读取IoDevice中的数据并将结果拼接成列表 {ok, Term} ->[Term|consult1(S)]; eof ->[]; Error ->Error end.
如果想查看标准库中的实现, 可以使用
1> code:which(file). "/usr/local/Cellar/erlang/R15B01/lib/erlang/lib/kernel-2.15.1/ebin/file.beam"code:which(file) #从而找到源码文件 /usr/local/Cellar/erlang/R15B01/lib/erlang/lib/kernel-2.15.1/src/file.erl
13.2.3 从文件中一次读取一行数据
1> {ok, S} = file:open("data1.dat", read). {ok,<0.49.0>} 2> io:get_line(S, ''). "{person, \"joe\", \"armstrong\",\n" 3> io:get_line(S, ''). " [{occupation, programmer},\n" 4> io:get_line(S, ''). " {favoriteLanguage, erlang}]}.\n" 5> io:get_line(S, ''). "{cat, {name, \"zorro\"},\n" 6> io:get_line(S, ''). " {owner, \"joe\"}}.\n" 7> io:get_line(S, ''). eof 8> file:close(S). ok
13.2.4 将整个文件的内容读入到一个二进制数据中
# 读取成功返回 {ok, Bin}, 否则返回 {error, Why} 1> file:read_file("data1.dat"). {ok,<<"{person, \"joe\", \"armstrong\",\n [{occupation, programmer},\n {favoriteLanguage, erlang}]}.\n{cat, {name, "...>>}}}
13.2.5 随机读取一个文件
1> {ok, S} = file:open("data1.dat", [read, binary, raw]). {ok,{file_descriptor,prim_file,{#Port<0.582>,11}}} # file:pread(IoDevice, Start, Len) # 从第N个字节开始, 读取长度为Len字节的数据 2> file:pread(S, 22, 46). {ok,<<"rong\",\n [{occupation, programmer},\n {fa">>} 3> file:pread(S, 1, 10). {ok,<<"person, \"j">>}
13.2.6 读取ID3标记
MP3文件本身不存储有关音乐属性的信息, 程序员Eric Kemp发明了ID3标记, 将文件内容信息存储在MP3文件的一个标记格式区块中。
ID3v1标记的结构:
长度 | 内容 |
---|---|
3 | TAG |
30 | 标题 |
30 | 艺术家 |
30 | 专辑 |
4 | 年度 |
30 | 注释 |
1 | 其它 |
长度 | 内容 |
---|---|
28 | 注释 |
1 | 0 |
1 | 音轨号 |
-module(id3_v1). -import(lists, [filter/2, map/2, reverse/1]). -export([test/0, dir/1, read_id3_tag/1]). test() ->dir("/Users/matrix/Music/Enigma/"). dir(Dir) -> %% 递归搜索MP3文件 Files = lib_find:files(Dir, "*.mp3", true), L1 = map(fun(I) -> %% 解析每个MP3文件 {I, (catch read_id3_tag(I))} end, Files), L2 = filter(fun({_, error}) ->false; (_) ->true end, L1), lib_misc:dump("mp3data", L2). read_id3_tag(File) -> case file:open(File, [read, binary, raw]) of {ok, S} -> Size = filelib:file_size(File), {ok, B2} = file:pread(S, Size-128, 128), Result = parse_v1_tag(B2), file:close(S), Result; Error -> {File, Error} end. %% 按照ID3v1的两个版本进行解析 parse_v1_tag(<<$T, $A, $G, Title:30/binary, Artist:30/binary, Album:30/binary, _Year:4/binary, _Comment:28/binary, 0:8, Track:8, _Genre:8>>) -> {"ID3v1.1", [{track, Track}, {title, trim(Title)}, {artist, trim(Artist)}, {album, trim(Album)}]}; parse_v1_tag(<<$T, $A, $G, Title:30/binary, Artist:30/binary, Album:30/binary, _Year:4/binary, _Comment:30/binary, _Genre:8>>) -> {"ID3v1", [{title, trim(Title)}, {artist, trim(Artist)}, {album, trim(Album)}]}; parse_v1_tag(_) ->error. trim(Bin) -> list_to_binary(trim_blanks(binary_to_list(Bin))). trim_blanks(X) ->reverse(skip_blanks_and_zero(reverse(X))). %% 进行位匹配, 删除空格和0 skip_blanks_and_zero([$\s|T]) ->skip_blanks_and_zero(T); skip_blanks_and_zero([0|T]) ->skip_blanks_and_zero(T); skip_blanks_and_zero(X) ->X.
13.3 写入文件的不同方法
13.3.1 向一个文件中写入一串Erlang数据项
unconsult(File, L) -> {ok, S} = file:open(File, write), %% io:format(IoDevice, Format, Args) %% Format是一个包含了格式化代码的字符串 %% ~p 完整打印参数 %% ~s 字符串参数 %% ~w 标准语法写入数据 %% ~n 换行符 %% Args为数据项 lists:foreach(fun(X) ->io:format(S, "~p.~n", [X]) end, L), file:close(S).
运行结果:
$ cat test1.dat {cats,["zorrow","daisy"]}. {weather,snowing}.
13.3.2 向文件中写入一行
1> {ok, S} = file:open("test2.dat", write). {ok,<0.33.0>} 2> io:format(S, "~s~n", ["Hello readers"]). ok 3> io:format(S, "~w~n", [123]). ok 4> io:format(S, "~s~n", ["that's it"]). ok 5> file:close(S). ok
查看结果:
$ cat test2.dat Hello readers 123 that's it
13.3.3 一步操作写入整个文件
-module(scavenge_urls). -export([urls2htmlFile/2, bin2urls/1]). -import(lists, [reverse/1, reverse/2, map/2]). %% 将列表写入到文件 urls2htmlFile(Urls, File) -> file:write_file(File, urls2html(Urls)). %% 将二进制数据转成列表 bin2urls(Bin) ->gather_urls(binary_to_list(Bin), []). %% 根据列表中的数据构建html代码 urls2html(Urls) ->[h1("Urls"), make_list(Urls)]. h1(Title) ->["<h1>", Title, "</h1>\n"]. make_list(L) -> ["<ul>/n", map(fun(I) ->["<li>", I, "</li>\n"] end, L), "</ul>\n"]. %% 匹配链接, 拼接成列表 gather_urls("<a href" ++T, L) -> {Url, T1} = collect_url_body(T, reverse("<a href")), gather_urls(T1, [Url|L]); gather_urls([_|T], L) ->gather_urls(T, L); gather_urls([], L) ->L. %% 匹配链接结束符以获取链接实体 collect_url_body("</a>" ++ T, L) ->{reverse(L, "</a>"), T}; collect_url_body([H|T], L) ->collect_url_body(T, [H|L]); collect_url_body([], _) ->{[],[]}.
运行结果:
1> B = socket_examples:nano_get_url("www.erlang.org"). <<"HTTP/1.0 200 OK\r\nServer: inets/5.7.1\r\nDate: Sun, 03 Nov 2013 03:10:14 GMT\r\nSet-Cookie: eptic_cookie=erlangorg@hades-"...>> 2> L = scavenge_urls:bin2urls(B). ["<a href=\"https://github.com/esl/erlang-web\">Erlang Web</a>", "<a href=\"http://www.twitter.com/erlang_org\"><img src=\"/icons/twitter.png\" width=\"32\"/></a>", "<a href=\"http://www.github.com/erlang/otp\"><img src=\"/images/GitHub-Mark-32px.png\"/></a>", "<a href=\"/download.html\" title=\"DOWNLOAD\"><img src=\"/icons/download.png\"/></a>", "<a href=\"/event\">More events...</a>", "<a href=\"/mailman/listinfo/erlang-questions\">\n\t\tListinfo & subscription...\n\t </a>", "<a href=\"/pipermail/erlang-questions/2013-November/075894.html\" target=\"_blank\">Arch Linux patches?\n</a>", "<a href=\"/pipermail/erlang-questions/2013-November/075903.html\" target=\"_blank\">ETS-TRANSFER\n</a>", "<a href=\"/pipermail/erlang-questions/2013-November/075904.html\" target=\"_blank\">emysql\n</a>", "<a href=\"/pipermail/erlang-questions/2013-November/075905.html\" target=\"_blank\">Mysterious gen_server timeouts in MIX\n</a>", "<a href=\"/pipermail/erlang-questions/2013-November/075910.html\" target=\"_blank\">On Pull Requests Comments\n</a>", "<a href=\"/news/59\">Erlang/OTP R16B02 has been released! </a>", "<a href=\"/news/60\">Erlang talks at Code Mesh 3-5 December 2013: the Alternative Programming Conference</a>", "<a href=\"/news/61\">Toronto Erlang Factory Lite 23 November 2013</a>", "<a href=\"http://www.github.com/erlang/otp\"><img src=\"/images/GitHub-Mark-32px.png\" width=\"35\"/></a>", "<a href=\"/download.html\" class=\"btn btn-success\">Download Erlang/OTP</a>"] 3> scavenge_urls:urls2htmlFile(L, "gathered.html"). ok $ cat gathered.html <h1>Urls</h1> <ul>/n<li><a href="https://github.com/esl/erlang-web">Erlang Web</a></li> <li><a href="http://www.twitter.com/erlang_org"><img src="/icons/twitter.png" width="32"/></a></li> <li><a href="http://www.github.com/erlang/otp"><img src="http://images.cnblogs.com/GitHub-Mark-32px.png"/></a></li> <li><a href="/download.html" title="DOWNLOAD"><img src="/icons/download.png"/></a></li> <li><a href="/event">More events...</a></li> <li><a href="/mailman/listinfo/erlang-questions"> Listinfo & subscription... </a></li> <li><a href="/pipermail/erlang-questions/2013-November/075894.html" target="_blank">Arch Linux patches? </a></li> <li><a href="/pipermail/erlang-questions/2013-November/075903.html" target="_blank">ETS-TRANSFER </a></li> <li><a href="/pipermail/erlang-questions/2013-November/075904.html" target="_blank">emysql </a></li> <li><a href="/pipermail/erlang-questions/2013-November/075905.html" target="_blank">Mysterious gen_server timeouts in MIX </a></li> <li><a href="/pipermail/erlang-questions/2013-November/075910.html" target="_blank">On Pull Requests Comments </a></li> <li><a href="/news/59">Erlang/OTP R16B02 has been released! </a></li> <li><a href="/news/60">Erlang talks at Code Mesh 3-5 December 2013: the Alternative Programming Conference</a></li> <li><a href="/news/61">Toronto Erlang Factory Lite 23 November 2013</a></li> <li><a href="http://www.github.com/erlang/otp"><img src="http://images.cnblogs.com/GitHub-Mark-32px.png" width="35"/></a></li> <li><a href="/download.html" class="btn btn-success">Download Erlang/OTP</a></li> </ul>$
13.3.4 在随机访问模式下写入文件
# 写入前 $ cat test2.dat Hello readers 123 that's it # 执行写入操作 1> {ok, S} = file:open("test2.dat", [raw, write, binary]). {ok,{file_descriptor,prim_file,{#Port<0.582>,11}}} # file:pwrite(IoDevice, Position, Bin) 2> file:pwrite(S, 10, <<"new data\n">>). ok 3> file:close(S). ok # 写入后 $ cat test2.dat new data
13.4 目录操作
list_dir(Dir)
用于生成Dir目录下的文件列表
make_dir(Dir)
用于创建一个新的目录
del_dir(Dir)
用于删除目录
13.5 查询文件属性
使用read_file_info(File)来查询文件的属性, 查询成功则返回{ok, Info}, 其中Info为#file_info类型。
file_info的结构:
属性 | 含义 |
---|---|
size | 文件大小(字节) |
type | 文件类型(设备, 目录, 常规文件, 其它) |
access | 读, 写, 读写, 其它 |
atime | 最后一次读时间{{Year, Mon, Day}, {Hour, Min, Sec}} |
ctime | 最后修改时间(Unix); 创建时间(Windows) |
mode | 数字表示的文件权限 |
links | 指向当前文件的链接数量 |
major_device | 表示文件系统编号 |
利用它实现输出更多信息的ls函数
%% file_info结构的定义在这个文件中 -include_lib("kernel/include/file.hrl"). %% 提取文件信息 file_size_and_type(File) -> case file:read_file_info(File) of {ok, Facts} -> {Facts#file_info.type, Facts#file_info.size}; _ ->error end. ls(Dir) -> {ok, L} = file:list_dir(Dir), lists:map(fun(I) ->{I, file_size_and_type(I)} end, lists:sort(L)).
运行结果:
1> lib_misc:ls("."). [{"data1.dat",{regular,141}}, {"gathered.html",{regular,1551}}, {"id3_v1.beam",{regular,1968}}, {"id3_v1.erl",{regular,1763}}, {"lib_find.beam",{regular,1460}}, {"lib_find.erl",{regular,1744}}, {"lib_misc.beam",{regular,1540}}, {"lib_misc.erl",{regular,1092}}, {"scavenge_urls.beam",{regular,1444}}, {"scavenge_urls.erl",{regular,998}}, {"socket_examples.beam",{regular,2692}}, {"socket_examples.erl",{regular,2651}}, {"test1.dat",{regular,46}}, {"test2.dat",{regular,18}}]
13.6 复制和删除文件
copy(Source, Destination)
复制Source到Destination
delete(File)
删除File
13.7 小知识
文件模式
open(File)可以使用多种模式
修改文件属性
可以使用file模块中的功能函数实现修改文件的时间, 群组, 系统链接
错误代码
所有错误都是形如{error, Why}的元组
filename模块
从路径中提取文件名, 扩展名, 具有平台无关性
filelib模块
ensure_dir(Name)判断目录(及其父目录)是否存在, 不存在则创建
13.8 一个搜索小程序
-module(lib_find_my). -export([files/3, files/5]). -import(lists, [reverse/1]). -include_lib("kernel/include/file.hrl"). files(Dir, Re, Flag) -> reverse(files(Dir, Re, Flag, fun(File, Acc) ->[File|Acc] end, [])). %% Dir 执行搜索的路径名 %% Reg 匹配文件名的正则 %% Recursive 是否对子目录递归搜索 %% Fun 正则匹配后调用的函数 %% Acc 累加器 files(Dir, Reg, Recursive, Fun, Acc) -> case file:list_dir(Dir) of {ok, Files} ->find_files(Files, Dir, Reg, Recursive, Fun, Acc); {error, _} ->Acc end. find_files([File|T], Dir, Reg, Recursive, Fun, Acc0) -> FullName = filename:join([Dir, File]), case file_type(FullName) of regular -> %% 新版本的Erlang中regexp模块由re模块取代 case re:run(FullName, Reg) of {match, _} -> Acc = Fun(FullName, Acc0), find_files(T, Dir, Reg, Recursive, Fun, Acc); _ -> find_files(T, Dir, Reg, Recursive, Fun, Acc0) end; directory -> %% 对于目录只有当Recursive设置为true时才递归处理 case Recursive of true -> Acc1 = files(FullName, Reg, Recursive, Fun, Acc0), find_files(T, Dir, Reg, Recursive, Fun, Acc1); false -> find_files(T, Dir, Reg, Recursive, Fun, Acc0) end; error -> find_files(T, Dir, Reg, Recursive, Fun, Acc0) end; find_files([], _, _, _, _, A) ->A. file_type(File) -> case file:read_file_info(File) of {ok, Facts} -> case Facts#file_info.type of regular ->regular; directory ->directory; _ ->error end; _ ->error end.
运行结果:
1> lib_find_my:files(".", ".*[.](erl).*$", false). ["./socket_examples.erl","./scavenge_urls.erl", "./lib_misc.erl","./lib_find.erl","./id3_v1.erl"] 2> lib_find_my:files(".", ".*[.](beam).*$", false). ["./socket_examples.beam","./scavenge_urls.beam", "./lib_misc.beam","./lib_find.beam","./id3_v1.beam"]