【问题标题】:Split lines in clojure while reading from file从文件读取时在clojure中拆分行
【发布时间】:2018-04-30 05:28:35
【问题描述】:

我正在学校学习 clojure,马上就要考试了。我只是在做一些事情以确保我掌握了它。

我正在尝试逐行读取文件,并且正如我所做的那样,只要有“;”,我就想分割该行。

这是我目前的代码

(defn readFile []
  (map (fn [line] (clojure.string/split line #";"))
  (with-open [rdr (reader "C:/Users/Rohil/Documents/work.txt.txt")]
    (doseq [line (line-seq rdr)]
      (clojure.string/split line #";")
        (println line)))))

当我这样做时,我仍然得到输出:

"I;Am;A;String;"

我错过了什么吗?

【问题讨论】:

  • 您打印该行,但不打印拆分结果。使用(doto (split...) println) 进行调试。
  • 伙计,这太愚蠢了。非常感谢!
  • 补充 cfrick 所说的,这是因为你的分割结果被丢弃了,因为 line 是不可变的。您需要将结果传递给 println。更多详情见我的回答

标签: file-io clojure split


【解决方案1】:

TL;DR 拥抱 REPL 并拥抱不变性

您的问题是“我错过了什么?”对此我想说你错过了 Clojure 的最佳功能之一,即 REPL。

编辑您可能还错过了 Clojure 使用不可变数据结构,所以

考虑一下这段代码 sn-p:

(doseq [x [1 2 3]]
   (inc x)
   (prn x))

此代码不打印“2 3 4”

它打印“1 2 3”,因为 x 不是可变变量。

在第一次迭代期间,(inc x) 被调用,返回 2,然后因为没有传递给任何东西而被丢弃,然后(prn x) 打印出 x 的值,它仍然是 1。

现在考虑这段代码 sn-p:

(doseq [x [1 2 3]] (prn (inc x)))

在第一次迭代期间,inc 将其返回值传递给 prn,因此您得到 2

长示例:

我不想剥夺你自己解决问题的机会,所以我将使用不同的问题作为示例。

给定文件"birds.txt" 数据"1chicken\n 2duck\n 3Larry" 你想写一个函数来接收一个文件并返回一系列鸟的名字

让我们把这个问题分解成更小的块:

首先让我们读取文件并将其分成几行

(slurp "birds.txt") 会给我们整个文件一个字符串

clojure.string/split-lines 会给我们一个集合,每一行作为集合中的一个元素

(clojure.string/split-lines (slurp "birds.txt")) 得到我们["1chicken" "2duck" "3Larry"]

此时我们可以在该集合上映射一些函数,以去除像(map #(clojure.string/replace % #"\d" "") birds-collection)这样的数字

或者当整个文件是一个字符串时,我们可以将这一步移到管道上。

现在我们已经拥有了所有的片段,我们可以将它们放在一个功能管道中,其中一个片段的结果馈送到下一个片段

在 Clojure 中,有一个很好的宏来提高可读性,->

它获取一个计算的结果并将其作为第一个参数注入下一个

所以我们的管道看起来像这样:

(-> "C:/birds.txt"
     slurp
     (clojure.string/replace #"\d" "") 
     clojure.string/split-lines)

关于样式的最后一点说明,对于 Clojure 函数,您要坚持使用 kebab case 所以 readFile 应该是 read-file

【讨论】:

    【解决方案2】:

    我会保持简单,并像这样编码:

    (ns tst.demo.core
      (:use tupelo.test)
      (:require [tupelo.core :as t]
                [clojure.string :as str] ))
    (def text
     "I;am;a;line;
      This;is;another;one
      Followed;by;this;")
    
    (def tmp-file-name "/tmp/lines.txt")
    
    (dotest
      (spit tmp-file-name text) ; write it to a tmp file
      (let [lines       (str/split-lines (slurp tmp-file-name))
            result      (for [line lines]
                          (for [word (str/split line #";")]
                            (str/trim word)))
            result-flat (flatten result)]
    (is= result
      [["I" "am" "a" "line"]
       ["This" "is" "another" "one"]
       ["Followed" "by" "this"]])
    

    请注意,result 是一个双嵌套 (2D) 单词矩阵。最简单的撤消方法是使用flatten 函数生成result-flat

    (is= result-flat
      ["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))
    

    你也可以使用apply concat

    (is= (apply concat result) result-flat)
    

    如果您想避免首先构建二维矩阵,您可以通过lazy-genyield from the Tupelo library 使用generator function(类似于Python):

    (dotest
      (spit tmp-file-name text) ; write it to a tmp file
      (let [lines  (str/split-lines (slurp tmp-file-name))
            result (t/lazy-gen
                     (doseq [line lines]
                       (let [words (str/split line #";")]
                         (doseq [word words]
                           (t/yield (str/trim word))))))]
    
    (is= result
      ["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))
    

    在这种情况下,lazy-gen 创建生成器函数。 请注意,for 已替换为 doseqyield 函数将每个单词放入输出惰性序列中。

    【讨论】:

    • 生成器可能对某些事情很方便,但我认为在这种情况下我会坚持使用mapcat 以避免嵌套序列。
    【解决方案3】:

    我不确定你在学校是否需要这个,但由于 Gary 已经给出了很好的答案,所以把它当作奖励。

    您可以使用转换器对文本行进行优雅的转换。您需要的成分是允许您将行视为可约集合并在您完成约简后关闭阅读器的东西:

    (defn lines-reducible [^BufferedReader rdr]
      (reify clojure.lang.IReduceInit
        (reduce [this f init]
          (try
            (loop [state init]
              (if (reduced? state)
                @state
                (if-let [line (.readLine rdr)]
                  (recur (f state line))
                  state)))
            (finally
              (.close rdr))))))
    

    现在您可以根据输入 work.txt 执行以下操作:

    I;am;a;string
    Next;line;please
    

    计算每个'split'的长度

    (require '[clojure.string :as str])
    (require '[clojure.java.io :as io])
    
    (into []
          (comp
           (mapcat #(str/split % #";"))
           (map count))
          (lines-reducible (io/reader "/tmp/work.txt")))
    ;;=> [1 2 1 6 4 4 6]
    

    所有'splits'的长度之和

    (transduce
     (comp
      (mapcat #(str/split % #";"))
      (map count))
     +
     (lines-reducible (io/reader "/tmp/work.txt")))
    ;;=> 24
    

    将所有单词的长度相加,直到找到一个大于 5 的单词

    (transduce
     (comp
      (mapcat #(str/split % #";"))
      (map count))
     (fn
       ([] 0)
       ([sum] sum)
       ([sum l]
        (if (> l 5)
          (reduced sum)
          (+ sum l))))
     (lines-reducible (io/reader "/tmp/work.txt")))
    

    take-while:

    (transduce
     (comp
      (mapcat #(str/split % #";"))
      (map count)
      (take-while #(> 5 %)))
     +
     (lines-reducible (io/reader "/tmp/work.txt")))
    

    阅读https://tech.grammarly.com/blog/building-etl-pipelines-with-clojure了解更多详情。

    【讨论】:

    • 我只使用line-seq 而不是lines-reducible 有什么区别?也许提前终止?
    • 好问题。使用line-seq 时,您将创建中间聚合并因此进行更多垃圾收集。此外,您必须自己关闭阅读器(通常通过 with-open 完成)。
    • 这是为什么呢?我认为transduce 将确保不会创建任何中间集合。通过使用 line-seq 而不是 lines-reducible 我的意思是使用(几乎)与您写的完全相同的表达式:(transduce (comp ... (line-seq ...
    • 现在我明白你的意思了。 (transduce (comp ...) + (line-seq (io/reader "/tmp/work.txt"))) 也有效,但需要考虑两件事:1)您必须自己关闭阅读器 2)行- seq 仍然会创建一个中间惰性序列。使用 lines-reducible 您将不会创建此序列。
    猜你喜欢
    • 1970-01-01
    • 2020-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-10
    • 1970-01-01
    • 2023-04-08
    • 2014-01-17
    相关资源
    最近更新 更多