blue white green and red textile

Apache Groovy: 强大的文本文件处理

提升 Apache Groovy 文本文件处理能力:本文探讨了高效的逐行读取以及 Groovy 的简化方法…
首页 » 博客 » Apache Groovy: 强大的文本文件处理

本文深入探讨了使用 Groovy 读取文本文件,探索了其相对于 Java 的独特优势。我们将回顾文件类型,并深入研究 Groovy 处理从文本文件读取行的不同方式。

在 之前的 文章, 我复习了一些 Groovy 的基础知识。如果您想解锁 Groovy 的强大功能,本系列将指导您了解是什么使 Groovy 成为对开发人员如此有价值的语言。  (如果您尚未安装 Groovy,请 阅读本系列的介绍 系列。) 

几年前,我写了一篇关于使用 Groovy 读取和写入文件的文章。这是一个非常简单的演示,说明 Groovy 在处理文本文件方面可以与 Java 有何不同,并且可以说,做得更好。

现在我将更深入地探讨在 Groovy 中读取文件的主题。但首先,这里有一些关于 Groovy(和 Java)对文件内容的看法的背景知识。

在您的 Linux 桌面电脑上,文件通常分为两类:包含人类可读文本的文件和包含二进制数据的文件。二进制数据对于人类来说很难直接理解。

我使用 LibreOffice Writer 编写了这篇文章,它以 ODF 文本 格式存储在我的电脑上。所以,文本格式,一定是你可以阅读和理解的东西,对吧?嗯,不是的。如果我使用 more 命令来显示文件,我可以看到它只是一条很长的行,开头是这样的

P��V^�2^L'mimetypeapplication/vnd.oasis.opendocument.textP��VConf

这没什么用。事实证明,ODF 文本格式是二进制数据,无法在终端中呈现。嗯。但是,如果您深入挖掘,您会发现 ODF 文本格式可以以压缩 zip 格式存储。您可以使用 unzip 命令在 .odt 文件上解压出它的所有组件,其中许多是文本文件。
当我解压这篇文章时,我得到了

unzip ../groo*19.odt
Archive:  ../groovy-advent-19.odt
 extracting: mimetype                
   creating: Configurations2/images/Bitmaps/
   creating: Configurations2/accelerator/
   creating: Configurations2/statusbar/
   creating: Configurations2/menubar/
   creating: Configurations2/popupmenu/
   creating: Configurations2/floater/
   creating: Configurations2/progressbar/
   creating: Configurations2/toolbar/
   creating: Configurations2/toolpanel/
  inflating: manifest.rdf            
  inflating: meta.xml                
  inflating: settings.xml            
 extracting: Thumbnails/thumbnail.png  
  inflating: styles.xml              
  inflating: content.xml             
  inflating: META-INF/manifest.xml

我可以使用 more 命令查看文章文本,它以 XML 格式保存在 content.xml 中。它是可读的,并且确实有意义,因为我了解 XML 是关于什么的

$ more content.xml
<?xml version="1.0" encoding="UTF-8"?>
<office:document-content xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:grddl=…
$

简而言之,您会看到 .odt 文件是一个二进制文件。您可以使用 unzip 实用程序通过将其转换为主要为文本的组件来帮助您理解此文件。但在更深层次上,所有文件都是二进制的。旨在供人类阅读的文件被结构化为文本。旨在供 unzip 或其他实用程序程序读取的文件通常被结构化为其他内容。

two red and blue zippers
照片由 Tomas SobekUnsplash 上拍摄

在 Java 中,因此在 Groovy 中,根据文件是否旨在被解释为文本或某种结构化二进制文件,有不同的处理文件的方式。

专注于文本路径,这种结构化的第一部分是认识到文件中的位旨在被读取为 Unicode 字符或 Unicode 代码点的实例

在 Java SE API 文档中,Unicode 代码点 用于 U+0000 和 U+10FFFF 范围内的字符值,而 Unicode 代码单元 用于 16 位 char 值,这些值是 UTF-16 编码的代码单元(请参阅 Java 语言文档中 Character 类的官方定义)。

Java,因此 Groovy,定义了一个以 java.io.Reader 开头的类层次结构(请参阅 此文档),这些类用于读取字符流,即文本。Reader 是一个抽象类,它被继承并进一步发展为

  • BufferedReader
  • CharArrayReader
  • FilterReader
  • InputStreamReader
  • PipedReader
  • StringReader

根据您要从哪里获取字符流,您可以获得这些各种读取器的专门版本。如果您想从文本文件读取,您应该关注 java.io.FileReader 类(请参阅 此文档),它是 InputStreamReader 的子类。

FileReader 定义了一个方便的构造函数 FileReader(String fileName),它允许您直接从文件名转到准备好让您访问该字符流的读取器。

FileReader 的问题在于它没有定义方便的 readLine() 方法。要将字符流视为具有面向行的结构,您必须自己注意行尾。这是危险的,因为行尾结构往往是操作系统相关的。这需要您缓冲字符流,然后搜索行尾。

FileReader 的另一个问题是您必须将 FileReader 包装在 BufferedReader 中。这提供了一个 readLine() 方法,以及一个 lines() 方法,该方法返回一个 java.util.Stream<String> 实例。

从接近 Java 的方式开始,这是它的样子

1   if (args.length != 1) {
2   System.err.println "Usage: groovy Groovy19a.groovy           input-file"
3   System.exit(0)
4   }
       
5   def reader = new BufferedReader(new FileReader(args[0]))
6   def line
       
7   while ((line = reader.readLine()) != null) {
8   println "line = $line"
9   }
       
10   reader.close()

第一到第四行处理用法。

第五行定义了您需要的读取器,它是一个 BufferedReader 实例,包装了一个 FileReader 实例,该实例附加到命令行上作为第一个参数提供的文件名。

第六行定义了字符串行,您将在其中读取文件的每一行。

第七到第九行循环遍历文件的行,方法是在读取器上调用 readLine() 方法,并将返回的值放入 line 变量中,直到遇到文件末尾。文件末尾用 null 行表示。读取的每一行都会打印出来,前缀为字符串“line = “。

第十行关闭读取器。

让我们运行它,使用文本/面向行的文件 /etc/group 作为输入

$ groovy Groovy19a.groovy /etc/group
line = root:x:0:
line = daemon:x:1:
line = bin:x:2:
line = sys:x:3:
…
$

这曾经是我早期 Java 时代读取文件的首选方法。当 Java 1.7 出现时,它带来了 java.nio.file. Files 类,它消除了用 BufferedReader 包装 FileReader 的需要。它通过提供 newBufferedReader() 工厂方法来实现这一点。我不能说我立即接受了这一点,因为它通过增加一个全新的类的复杂性来简化了操作。但是,随着我对 Files 类越来越熟悉,我可以看到它将一大堆相关实用程序整合到一个地方,这使其值得学习。例如,Files 提供了 lines() 方法,该方法将文件的 java.nio.file.Path 作为参数。消除 reader 变量、BufferedReader()FileReader() 大大简化了 Java 代码。while {} 命令、测试和 line 变量也被消除了。最后的润色是让它完成自己的 close()。然而,本着给予的同时也拿走的精神,它也需要学习整个 Streams 的东西……无论如何,这是一个很好的实践

1   import java.nio.file.Files
2   import java.nio.file.Path
       
3   if (args.length != 1) {
4   System.err.println "Usage: groovy Groovy19b.groovy input-file"
5   System.exit(0)
6   }
       
7   Files.lines(Path.of(args[0])).each { line -> println "line = $line" }

事实证明,它也简化了 Groovy 代码。
运行它

$ groovy Groovy19b.groovy /etc/group
line = root:x:0:
line = daemon:x:1:
line = bin:x:2:
line = sys:x:3:
…
$

解决此问题的另一种方法是将 Groovy 增强功能应用于 File

1   if (args.length != 1) {
2   System.err.println "Usage: groovy Groovy19b.groovy input-file"
3   System.exit(0)
4   }
       
5   new File(args[0]).eachLine { line -> println "line = $line" }

这可能是我最喜欢的,因为它不涉及学习几个新的类层次结构,而且它既简洁又紧凑。

当您运行它时,您会看到

$ groovy Groovy19c.groovy /etc/group
line = root:x:0:
line = daemon:x:1:
line = bin:x:2:
line = sys:x:3:
…

Groovy File 类还具有一个 withReader() 方法,该方法调用一个闭包,并将 BufferedReader 实例传递给它。我发现这不如使用上面描述的 eachLine() 方法方便。但是,当我从一个文件读取并写入另一个文件时,我肯定会使用 withWriter() 方法,以便在我的 BufferedReader 处理闭包中可以使用 writer。

但我会留到下次再说。

我在这里不打算介绍读取二进制文件,因为您需要知道如何处理二进制信息才能对其进行结构化和解释。在任何情况下,我通常更喜欢尽可能使用文本文件。当文本文件用于在步骤之间进行通信时,解耦处理步骤和查看中间结果要容易得多。

结论

虽然 Java 早期对 Readers 的看法似乎有点笨拙,需要将 FileReader 包装在 BufferedReader 中,使用循环来迭代文件中的行,并记住在最后关闭整个东西,但情况已经有所改善。现在从文件读取可以是一个单行程序,而该行不是 1,000 个字符长。

再次,您会看到 Groovy 的方法 是通过向您已经知道的类(如 File)添加新行为来使其更有用。将此与现代 Java 方法进行比较,后者是添加新的类层次结构,这些层次结构在整合旧行为的同时添加新行为,从而带来了更陡峭的学习曲线。

作者

如果您喜欢这篇文章,您可能也会喜欢这些