在文件处理任务

时间:2018-02-13 03:19:39

标签: clojure functional-programming

我有一个输入csv文件,需要生成一个输出文件,每个输入行都有一行。每个输入行可以是特定类型(例如“旧”或“新”),只能通过处理输入行来确定。

除了生成输出文件之外,我们还要打印输入文件中每种类型的行数的摘要。我的实际任务涉及根据输入行类型生成不同的SQL,但为了保持示例代码集中,我保持函数proc-line中的处理简单。函数func确定输入行的类型 - 再次,我通过随机生成类型使其保持简单。实际的逻辑更为复杂。

我有以下代码,它完成了这项工作。但是,为了保留生成摘要任务的功能样式,我选择返回一个关键字来表示每行的类型,并创建这些序列的延迟序列以生成最终摘要。在命令式样式中,我们只需增加每种线型的计数。仅为汇总而生成可能较大的集合似乎效率低下。我编写它的方式的另一个结果是重复(.write writer ...)部分。理想情况下,我只会编码一次。

有关消除我发现的两个问题(以及其他问题)的建议吗?

(ns file-proc.core
  (:gen-class)
  (:require [clojure.data.csv :as csv]
            [clojure.java.io :as io]))

(defn func [x]
  (rand-nth [true false]))

(defn proc-line [line writer]
  (if (func line)
    (do (.write writer (str line "\n")) :new)
    (do (.write writer (str (reverse line) "\n")) :old)))

(defn generate-report [from to]
  (with-open
     [reader (io/reader from)
      writer (io/writer to)]
     (->> (csv/read-csv reader)
          (rest)
          (map #(proc-line % writer))
          (frequencies)
          (doall))))

4 个答案:

答案 0 :(得分:1)

我尝试将数据处理与读取/写入文件等副作用分开。希望这将允许IO操作保持在管道的相对边界,并且“中间”处理逻辑不知道输入来自何处以及输出的位置。

(defn rand-bool [] (rand-nth [true false]))
(defn proc-line [line]
  (if (rand-bool)
    [line :new]
    [(reverse line) :old]))

proc-line不再需要作家,它只关心line并且它返回处理行的向量/ 2元组以及关键字。它也不关心字符串格式 - 我们应该让csv/write-csv这样做。现在你可以这样做:

(defn process-lines [reader]
  (->> (csv/read-csv reader)
       (rest)
       (map proc-line)))

(defn generate-report [from to]
  (with-open [reader (io/reader from)
              writer (io/writer to)]
    (let [lines (process-lines reader)]
      (csv/write-csv writer (map first lines))
      (frequencies (map second lines)))))

这将有效,但它将实现/保留整个输入序列在内存中,您不需要大文件。我们需要一种方法来保持这个管道的惰性/高效,但是我们还必须在一次传递中从一个生成两个“流”:处理的行是发送到write-csv,每行都有用于计算频率的元数据。一种“简单”的方法是引入一些可变性来跟踪元数据频率,因为write-csv消耗了懒惰序列:

(defn generate-report [from to]
  (with-open [reader (io/reader from)
              writer (io/writer to)]
    (let [freqs (atom {})]
      (->> (csv/read-csv reader)
           ;; processing starts
           (rest)
           (map (fn [line]
                  (let [[row tag] (proc-line line)]
                    (swap! freqs update tag (fnil inc 0))
                    row)))
           ;; processing ends
           (csv/write-csv writer))
      @freqs)))

我删除了process-lines调用,以使整个管道更加明显。当write-csv完全(并且懒惰地)消耗其有效负载时,freqs将是{:old 23, :new 31}之类的地图,它将是generate-report的返回值。还有改进/推广的空间,但我认为这是一个开始。

答案 1 :(得分:0)

正如其他人所说,分离写作和处理工作将是理想的。以下是我通常如何做到这一点:

(defn product-type [p]
  (rand-nth [:new :old]))

(defn row->product [row]
  (let [p (zipmap [:id :name :price] row)]
    (assoc p :type (product-type p))))

(defmulti to-csv :type)
(defmethod to-csv :new [product] ...)
(defmethod to-csv :old [product] ...)

(defn generate-report [from to]
  (with-open [rdr (io/reader from)
              wrtr (io/writer to)]
    (->> (rest (csv/read-csv rdr))
         (map row->product)
         (map #(do (.write wrtr (to-csv %)) %))
         (map :type)
         (frequencies)
         (doall))))

(代码可能不起作用 - 没有运行它,抱歉。)

当然,构造哈希映射并使用多方法是可选的,但最好先为产品分配类型。这样,它的数据决定了管道正在做什么,而不是proc-line

答案 2 :(得分:0)

要重构代码,我们需要至少一个generate-report特征测试的安全网。由于该函数执行文件I / O(稍后我们将使代码独立于I / O),我们将使用此示例CSV文件f1.csv

Year,Code
1997,A
2000,B
2010,C
1996,D
2001,E

我们还不能编写测试因为函数func使用RNG,所以我们通过实际查看输入将其重写为确定性的。在那里,我们将其重命名为new?,这更能代表问题:

(defn new? [row]
  (>= (Integer/parseInt (first row)) 2000))

其中,为了练习,我们假设一行是" new"如果Year列是> = 2000。

我们现在可以编写测试并看到它通过(为简洁起见,我们只关注频率计算,而不是输出转换):

(deftest characterization-as-posted
  (is (= {:old 2, :new 3}
         (generate-report "f1.csv" "f1.tmp"))))

现在进行重构。主要想法是要意识到我们需要一个累加器,用map替换reduce并删除frequenciesdoall。此外,我们重命名" line"使用" row",因为这是以CSV格式调用行的方式:

(defn generate-report [from to]                         ; 1
  (let [[old new _]                                     ; 2
        (with-open [reader (io/reader from)             ; 3
                    writer (io/writer to)]              ; 4
          (->> (csv/read-csv reader)                    ; 5
               (rest)                                   ; 6
               (reduce process-row [0 0 writer])))]     ; 7
    {:old old :new new}))                               ; 8

process-row(原process-line)成为:

(defn process-row [[old new writer] row]
  (if (new? row)
    (do (.write writer (str row "\n")) [old (inc new) writer])
    (do (.write writer (str (reverse row) "\n")) [(inc old) new writer])))

函数process-row,作为要传递给reduce的任何函数,有两个参数:第一个参数[old new writer]是两个累加器和I / O编写器的向量(向量)是破坏的);第二个参数row是正在减少的集合的一个元素。它返回累加器的新向量,在集合的末尾在generate-report的第2行中进行解构,并在第8行使用,以创建等效于frequencies之前返回的散列映射。

我们可以进行最后一次重构:将文件I / O与业务逻辑分开,这样我们就可以编写测试而无需使用预先准备好的输入文件的脚手架,如下所示。

函数process-row变为:

(defn process-row [[old-cnt new-cnt writer] row]
  (let [[out-row old new] (process-row-pure old-cnt new-cnt row)]
    (do (.write writer out-row)
        [old new writer])))

并且业务逻辑可以通过纯(以及如此容易测试的)函数来完成:

(defn process-row-pure [old new row]
  (if (new? row)
    [(str row "\n") old (inc new)]
    [(str (reverse row) "\n") (inc old) new]))

这一切都没有改变任何事情。

答案 3 :(得分:-1)

恕我直言,我将两个不同方面分开:计算频率并写入文件:

(defn count-lines
   ([lines] (count-lines lines 0 0))
   ([lines count-old count-new]
     (if-let [line (first lines)]
        (if (func line)
           (recur count-old (inc count-new) (rest lines))
           (recur (inc count-old) count-new (rest lines)))
        {:new count-new :old count-old})))


 (defn generate-report [from to]
   (with-open [reader (io/reader from)
               writer (io/writer to)]
     (let [lines (rest (csv/read-csv reader))
           frequencies (count-lines lines)]
         (doseq [line lines]
            (.write writer (str line "\n"))))))