elixir的 文件编程 file
在Elixir中使用文件系统与使用其他流行的编程语言进行操作实际上并没有什么不同。 有三个模块可以解决此任务: IO
, File
和Path
。 它们提供了打开,创建,修改,读取和销毁文件,扩展路径等功能。但是,您应该注意一些有趣的陷阱。
在本文中,我们将在看一些代码示例的同时谈论在Elixir中使用文件系统的问题。
路径模块
顾名思义,“ 路径”模块用于处理文件系统路径。 该模块的功能始终返回UTF-8编码的字符串。
例如,您可以扩展路径,然后轻松生成绝对路径:
-
Path.expand('./text.txt') |> Path.absname
-
# => "f:/elixir/text.txt"
请注意,顺便说一句,在Windows中,反斜杠会自动替换为正斜杠。 生成的路径可以传递给File
模块的功能,例如:
-
Path.expand('./text.txt') |> Path.absname |> File.write("new content!", [:write])
-
# => :ok
在这里,我们正在构建文件的完整路径,然后向其中写入一些内容。
总而言之,使用Path
模块非常简单,并且其大多数功能都不会与文件系统交互。 我们将在本文后面看到此模块的一些用例。
IO和文件模块
顾名思义, IO是用于输入和输出的模块。 例如,它提供puts
和inspect
等功能。 IO具有设备的概念,它可以是进程标识符(PID)或原子。 例如,有:stdio
和:stderr
通用设备(实际上是快捷方式)。 Elixir中的设备保持其位置,因此后续的读取或写入操作将从先前访问该设备的位置开始。
反过来,“ 文件”模块允许我们将文件作为IO设备进行访问。 默认情况下,文件以二进制模式打开; 但是,您可以通过:utf8
作为选项。 同样,当文件名指定为字符列表( 'some_name.txt'
)时,它始终被视为UTF-8。
现在,让我们来看一些使用上述模块的示例。
使用IO打开和读取文件
当然,最常见的任务是打开和读取文件。 要打开文件,可以使用称为open / 2的函数。 它接受文件的路径和模式的可选列表。 例如,让我们尝试打开一个文件进行读写:
-
{:ok, file} = File.open("test.txt", [:read, :write])
-
file |> IO.inspect
-
# => #PID<0.72.0>
然后,您也可以使用IO
模块中的read / 2函数来读取此文件:
-
{:ok, file} = File.open("test.txt", [:read, :write])
-
IO.read(file, :line) |> IO.inspect
-
# => "test"
-
IO.read(file, :line) |> IO.inspect
-
# => :eof
在这里,我们正在逐行读取文件。 注意:eof
原子,它表示“文件结尾” 。
您也可以通过:all
而不是:line
来一次读取整个文件:
-
{:ok, file} = File.open("test.txt", [:read, :write])
-
IO.read(file, :all) |> IO.inspect
-
# => "test"
-
IO.read(file, :all) |> IO.inspect
-
# => ""
在这种情况下, :eof
不会被返回,相反,我们得到一个空字符串。 为什么? 好吧,因为正如我们之前所说,设备保持其位置,并且我们从先前访问的位置开始读取。
还有一个open / 3函数 ,它接受一个函数作为第三个参数。 传递的函数完成其工作之后,该文件将自动关闭:
-
File.open "test.txt", [:read], fn(file) ->
-
IO.read(file, :all) |> IO.inspect
-
end
使用文件模块读取文件
在上一节中,我展示了如何使用IO.read
来读取文件,但是看来File
模块实际上具有一个同名的函数 :
-
File.read "test.txt"
-
# => {:ok, "test"}
该函数返回一个包含操作结果和二进制数据对象的元组。 在此示例中,它包含“ test”,即文件的内容。
如果操作失败,则该元组将包含:error
原子和错误的原因:
-
File.read("non_existent.txt")
-
在这里, :enoent
表示该文件不存在。 还有其他一些原因,例如:eacces
(没有权限)。
返回的元组可用于模式匹配以处理不同的结果:
-
case File.read("test.txt") do
-
{:ok, body} -> IO.puts(body)
-
{:error, reason} -> IO.puts("There was an error: #{reason}")
-
end
在此示例中,我们要么打印出文件的内容,要么显示错误原因。
读取文件的另一个功能称为read!/ 1 。 如果您来自Ruby世界 ,那么您可能已经猜到了它的作用。 基本上,此函数打开文件并以字符串形式(不是元组!)返回其内容:
-
File.read!("test.txt")
-
# => "test"
但是,如果出现问题并且无法读取文件,则会引发错误:
-
File.read!("non_existent.txt")
-
# => (File.Error) could not read file "non_existent.txt": no such file or directory
因此,为了安全起见,您可以例如使用exist?/ 1函数来检查文件是否实际存在:
-
defmodule Example do
-
def read_file(file) do
-
if File.exists?(file) do
-
File.read!(file) |> IO.inspect
-
end
-
end
-
end
-
-
Example.read_file("non_existent.txt")
太好了,现在我们知道如何读取文件了。 但是,我们还有很多事情可以做,所以让我们继续下一节!
写入文件
要将某些内容写入文件,请使用write / 3函数 。 它接受文件的路径,内容和模式的可选列表。 如果文件不存在,将自动创建。 但是,如果确实存在,则默认情况下将覆盖其所有内容。 为了防止这种情况发生,请设置:append
模式:
-
File.write("new.txt", "update!", [:append]) |> IO.inspect
-
# => :ok
在这种情况下,内容将被附加到文件中,并且:ok
将作为结果返回。 如果出现问题,您将得到一个元组{:error, reason}
,就像使用read
函数一样。
另外,有写! 函数的功能几乎相同,但是如果无法编写内容,则会引发异常。 例如,我们可以编写一个Elixir程序,该程序创建一个Ruby程序,该程序依次输出“ hello!”:
File.write!("test.rb", "puts \"hello!\"")
流文件
这些文件确实可以很大,并且在使用read
功能时,您会将所有内容加载到内存中。 好消息是文件可以很容易地流式传输 :
-
File.open!("test.txt")
-
|> IO.stream(:line)
-
|> Enum.each(&IO.inspect/1)
在此示例中,我们打开一个文件,逐行对其进行流处理,然后检查每一行。 结果将如下所示:
-
"test\n"
-
"line 2\n"
-
"line 3\n"
-
"some other line...\n"
请注意,新的线符号不会自动删除,因此您可能需要使用String.replace / 4函数将其删除。
如前面的示例所示,逐行流式传输文件有点麻烦。 相反,您可以依赖stream!/ 3函数 ,该函数接受文件路径和两个可选参数:模式列表和解释如何读取文件的值(默认值为:line
):
-
File.stream!("test.txt") |>
-
Stream.map( &(String.replace(&1, "\n", "")) ) |>
-
Enum.each(&IO.inspect/1)
在这段代码中,我们在流文件的同时删除换行符,然后打印出每一行。 File.stream!
比File.read
慢,但是我们不必等到所有行都可用File.read
可以立即开始处理内容。 当您需要从远程位置读取文件时,此功能特别有用。
让我们看一个稍微复杂的例子。 我想使用Elixir脚本流式传输文件,删除换行符,并在每行旁边显示一个行号:
-
File.stream!("test.exs") |>
-
Stream.map( &(String.replace(&1, "\n", "")) ) |>
-
Stream.with_index |>
-
Enum.each( fn({contents, line_num}) ->
-
IO.puts "#{line_num + 1} #{contents}"
-
end)
Stream.with_index / 2接受一个枚举并返回一个元组的集合,其中每个元组都包含一个值及其索引。 接下来,我们仅遍历此集合并打印出行号和行本身。 结果,您将看到带有行号的相同代码:
-
1 File.stream!("test.exs") |>
-
2 Stream.map( &(String.replace(&1, "\n", "")) ) |>
-
3 Stream.with_index |>
-
4 Enum.each( fn({contents, line_num}) ->
-
5 IO.puts "#{line_num + 1} #{contents}"
-
6 end)
移动和删除文件
现在,让我们简要介绍一下如何处理文件-具体来说就是移动和删除文件。 我们感兴趣的函数是named / 2和rm / 1 。 我不会通过描述它们接受的所有参数来让您感到厌烦,因为您可以自己阅读文档,而且它们绝对没有什么复杂的。 相反,让我们看一些示例。
首先,我想编写一个函数,该函数根据条件从当前目录中获取所有文件,然后将其移至另一个目录。 该函数应这样调用:
Copycat.transfer_to "texts", fn(file) -> Path.extname(file) == ".txt" end
因此,在这里我要获取所有.txt文件并将其移至texts目录。 我们如何解决这个任务? 好吧,首先,让我们定义一个模块和一个私有函数来准备目标目录:
-
defmodule Copycat do
-
def transfer_to(dir, fun) do
-
prepare_dir! dir
-
end
-
-
defp prepare_dir!(dir) do
-
unless File.exists?(dir) do
-
File.mkdir!(dir)
-
end
-
end
-
end
mkdir! ,您已经猜到了,尝试创建目录并在此操作失败的情况下返回错误。
接下来,我们需要从当前目录中获取所有文件。 可以使用ls完成! 函数,该函数返回文件名列表:
File.ls!
最后,我们需要根据提供的功能过滤结果列表并重命名每个文件,这实际上意味着将其移动到另一个目录。 这是程序的最终版本:
-
defmodule Copycat do
-
def transfer_to(dir, fun) do
-
prepare_dir!(dir)
-
File.ls! |>
-
Stream.filter( &( fun.(&1) ) ) |>
-
Enum.each( &(File.rename(&1, "#{dir}/#{&1}")) )
-
end
-
-
defp prepare_dir!(dir) do
-
unless File.exists?(dir) do
-
File.mkdir!(dir)
-
end
-
end
-
end
现在,通过编码类似的函数来查看rm
的作用,该函数将根据条件删除所有文件。 该函数将通过以下方式调用:
Copycat.remove_if fn(file) -> Path.extname(file) == ".csv" end
这是相应的解决方案:
-
defmodule Copycat do
-
def remove_if(fun) do
-
File.ls! |>
-
Stream.filter( &( fun.(&1) ) ) |>
-
Enum.each( &File.rm!/1 )
-
end
-
end
如果无法删除文件, rm!/ 1将引发错误。 和往常一样,它有一个rm / 1对应项,如果出现问题,它将返回一个元组,并带有错误的原因。
您可能会注意到remove_if
和transfer_to
函数非常相似。 那么,为什么不删除代码重复作为练习呢? 我将添加另一个私有函数,该私有函数接受所有文件,根据提供的条件过滤它们,然后对它们执行操作:
-
defp filter_and_process_files(condition, operation) do
-
File.ls! |>
-
Stream.filter( &(condition.(&1)) ) |>
-
Enum.each( &(operation.(&1)) )
-
end
现在,只需使用此功能:
-
defmodule Copycat do
-
def transfer_to(dir, fun) do
-
prepare_dir!(dir)
-
filter_and_process_files(fun, fn(file) -> File.rename(file, "#{dir}/#{file}") end)
-
end
-
-
def remove_if(fun) do
-
filter_and_process_files(fun, fn(file) -> File.rm!(file) end)
-
end
-
# ...
-
end
第三方解决方案
Elixir的社区正在增长,并且正在兴起可解决各种任务的新图书馆。 令人敬畏的Elixir GitHub存储库列出了一些受欢迎的解决方案,当然还有一个包含用于处理文件和目录的库的部分。 有一些用于文件上载,监视,文件名清理等的实现。
例如,有一个有趣的解决方案,称为Librex,用于在LibreOffice的帮助下转换文档。 要查看其实际效果,可以创建一个新项目:
$ mix new converter
然后将新的依赖项添加到mix.exs文件中:
-
defp deps do
-
[{:librex, "~> 1.0"}]
-
end
之后,运行:
$ mix do deps.get, deps.compile
接下来,您可以包括库并执行转换:
-
defmodule Converter do
-
import Librex
-
-
def convert_and_remove(dir) do
-
convert "some_path/file.odt", "other_path/1.pdf"
-
end
-
end
为了使其正常工作, PATH
必须存在LibreOffice可执行文件( soffice.exe )。 否则,您需要提供此文件的路径作为第三个参数:
-
defmodule Converter do
-
import Librex
-
-
def convert_and_remove(dir) do
-
convert "some_path/file.odt", "other_path/1.pdf", "path/soffice"
-
end
-
end
结论
今天就这些! 在本文中,我们已经看到了IO
, File
和Path
模块的实际作用,并讨论了一些有用的功能,例如open
, read
, write
和其他功能。
还有许多其他功能可供使用,因此请务必浏览Elixir的文档 。 另外,在官方网站上也有该语言的入门教程 ,也可以派上用场。
参考链接:Working With the File System in Elixir (tutsplus.com)
在Elixir中使用文件系统_cunjie3951的博客-CSDN博客