【问题标题】:Processing a file character by character in Clojure在 Clojure 中逐个字符处理文件
【发布时间】:2012-07-26 12:34:19
【问题描述】:

我正在用 Clojure 编写一个函数,它将逐个字符地处理文件。我知道 Java 的 BufferedReader 类具有读取一个字符的 read() 方法,但我是 Clojure 的新手,不知道如何使用它。目前,我只是尝试逐行执行文件,然后打印每个字符。

(defn process_file [file_path]
(with-open [reader (BufferedReader. (FileReader. file_path))]
    (let [seq (line-seq reader)]
        (doseq [item seq]
            (let [words (split item #"\s")]
                (println words))))))

给定一个带有此文本输入的文件:

非常感谢接受国际捐赠,但我们无法提供 任何有关对收到的捐款的税务处理的声明 在美国以外。仅美国法律就淹没了我们的小员工。

我的输出如下所示:

[International donations are gratefully accepted, but we cannot make]
[any statements concerning tax treatment of donations received from]
[outside the United States.  U.S. laws alone swamp our small staff.]

虽然我希望它看起来像:

["international" "donations" "are" .... ]

所以我的问题是,如何将上面的函数转换为逐字符读取?甚至,如何让它像我期望的那样工作?此外,任何使我的 Clojure 代码更好的提示将不胜感激。

【问题讨论】:

  • 如果您使用prn 而不是println,输出会是什么样子? println 不打印引号,因此您实际上可能会得到您期望的结果(例如["International" "donations" "are"...

标签: clojure


【解决方案1】:
(with-open [reader (clojure.java.io/reader "path/to/file")] ...

我更喜欢通过这种方式在 clojure 中获取 reader。而且,character by character 是指在文件访问级别,例如read,它允许您控制要读取多少个bytes

编辑

正如@deterb 指出的,让我们查看line-seq 的源代码

(defn line-seq
  "Returns the lines of text from rdr as a lazy sequence of strings.
   rdr must implement java.io.BufferedReader."
  {:added "1.0"
   :static true}
  [^java.io.BufferedReader rdr]
  (when-let [line (.readLine rdr)]
    (cons line (lazy-seq (line-seq rdr)))))

我伪造了char-seq

 (defn char-seq 
   [^java.io.Reader rdr]
   (let [chr (.read rdr)]
     (if (>= chr 0)
     (cons chr (lazy-seq (char-seq rdr))))))

我知道这个 char-seq 将所有字符读入内存[1],但我认为它表明您可以直接在 BufferedReader 上调用 .read。所以,你可以这样写你的代码:

(let [chr (.read rdr)]
  (if (>= chr 0)
    ;do your work here
  ))

你觉得怎么样?

[1] 根据@dimagog 的评论,char-seq 没有将所有字符读入内存,这要归功于lazy-seq

【讨论】:

  • 是的,在文件访问级别,就像读取一样。一旦我有了“reader”变量,调用它的“read()”方法的最佳方法是什么?
  • 尝试看看 line-seq 是如何做到的 - 只需在 REPL 中输入 (source line-seq)。
  • char-seq 读取内存中的所有字符,感谢lazy-seq 调用。还要将(cons chr ... 更改为(cons (char chr) ...,因为.read 返回int。我认为(但不确定)类型提示可以放宽到java.io.Reader
  • 这实际上会创建一个整数序列。解码没有发生。如何在那里注入解码阶段?
【解决方案2】:

我不熟悉 Java 或 read() 方法,因此无法帮助您实现它。

首先想到的可能是使用slurp 进行简化,这将返回整个文件的文本字符串,仅包含(slurp filename)。但是,这会得到整个文件,这可能是您不想要的。

一旦你有了一个包含整个文件文本的字符串,你就可以通过简单地把它当作一个字符序列来处理任何一个字符一个字符的字符串。例如:

=> (doseq [c "abcd"]
     (prntln c))
a
b
c
d
=> nil

或者:

=> (remove #{\c} "abcd")
=> (\a \b \d)

您可以使用mapreduce 或任何类型的序列操作函数。请注意,在像序列一样对其进行操作之后,它现在将作为序列返回,但您可以轻松地将外部部分包裹在 (reduce str ...) 中以将其返回到最后的字符串——明确地:

=> (reduce str (remove #{\c} "abcd"))
=> "abd"

至于您的特定代码的问题,我认为问题在于words 是:字符串向量。当您打印每个 words 时,您正在打印一个矢量。如果最后你用(doseq [w words] (println w))) 替换了(println words) 行,那么它应该很好用。

此外,根据您所说的您希望输出的样子(文件中所有不同单词的向量),您不希望只在表达式的基础上执行 (println w),因为这将打印值并返回nil。您只需要w。此外,您可能希望再次将您的doseqs 替换为fors,以避免返回nil

此外,在改进您的代码时,它对我来说通常看起来很棒,但是 - 这与我上面建议的所有第一个更改(但不是其他更改,因为我不想绘制它全部明确)——你可以用一个有趣的小技巧来缩短它:

(doseq [item seq]
        (let [words (split item #"\s")]
            (doseq [w words]
              (println w))))

;//Could be rewritten as...

(doseq [item s
        :let [words (split item #"\s")]
        w words]
  (println w))

【讨论】:

  • 哇,谢谢你的详细回复。不幸的是,使用 slurp 对我不起作用,因为我要接收的文件可能太大而无法放入内存。我没有意识到我可以把一个向量做成这样的序列。再次感谢。
【解决方案3】:

您非常接近 - 请记住字符串是一个序列。 (concat "abc" "def") 产生序列(\a \b \c \d \e \f)

mapcat 是另一个非常有用的函数——它会延迟连接应用映射 fn 到序列的结果。这意味着 mapcat 将所有行字符串转换为 seq 的结果将是您所追求的惰性字符序列。

我是用(mapcat seq (line-seq reader)) 做的。

其他建议:

  • 为了创建阅读器,我建议使用clojure.java.io/reader 函数而不是直接创建类。
  • 考虑将文件的读取和字符串的处理(在本例中为打印)分开。虽然将完整文件解析保留在 withopen 子句中很重要,但能够在文件读取代码之外测试实际处理代码非常有用。
  • 导航多个(可能嵌套的)序列时,请考虑使用forfor 在处理嵌套循环类型的情况下做得很好。

    (take 100 (for [line (repeat "abc") char (seq line)] (prn char)))

  • 使用prn 进行调试输出。与用户输出相比,它为您提供了真实的输出(隐藏了用户通常不关心的某些细节)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-08-05
    • 1970-01-01
    • 1970-01-01
    • 2017-07-28
    • 2016-04-08
    • 2011-07-28
    • 1970-01-01
    相关资源
    最近更新 更多