【问题标题】:More idiomatic line-by-line handling of a file in Clojure在 Clojure 中更惯用的逐行处理文件
【发布时间】:2013-08-19 21:57:12
【问题描述】:

我正在尝试使用 Clojure 逐行读取(可能有也可能没有)具有YAML frontmatter 的文件,并返回一个带有两个向量的哈希图,一个包含前端行,一个包含其他所有内容(即, 身体)。

示例输入文件如下所示:

---
key1: value1
key2: value2
---

Body text paragraph 1

Body text paragraph 2

Body text paragraph 3

我有执行此操作的功能代码,但在我(诚然没有使用 Clojure 的经验)看来,它散发出代码气味。

(defn process-file [f]
  (with-open [rdr (java.io.BufferedReader. (java.io.FileReader. f))]
    (loop [lines (line-seq rdr) in-fm 0 frontmatter [] body []]
      (if-not (empty? lines)
        (let [line (string/trim (first lines))]
          (cond
            (zero? (count line))
              (recur (rest lines) in-fm frontmatter body)
            (and (< in-fm 2) (= line "---")) 
              (recur (rest lines) (inc in-fm) frontmatter body)
            (= in-fm 1)  
              (recur (rest lines) in-fm (conj frontmatter line) body)
            :else          
             (recur (rest lines) in-fm frontmatter (conj body line))))
        (hash-map :frontmatter frontmatter :body body)))))

有人可以指出一种更优雅的方法吗?我将在这个项目中进行大量的逐行解析,如果可能的话,我想要一种更惯用的方式。

【问题讨论】:

    标签: clojure


    【解决方案1】:

    首先,我将行处理逻辑放在它自己的函数中,以便从实际读取文件的函数中调用。更好的是,您可以让处理 IO 的函数采用一个函数来映射行作为参数,也许沿着这些行:

    (require '[clojure.java.io :as io])
    
    (defn process-file-with [f filename]
      (with-open [rdr (io/reader (io/file filename))]
        (f (line-seq rdr))))
    

    请注意,这种安排使f 有责任在返回之前尽可能多地实现行序列(因为之后with-open 将关闭行序列的底层读取器)。

    鉴于这种职责分工,行处理函数可能如下所示,假设第一个 --- 必须是第一个非空行并且所有空行都将被跳过(就像使用代码时一样问题文本):

    (require '[clojure.string :as string])
    
    (defn process-lines [lines]
      (let [ls (->> lines
                    (map string/trim)
                    (remove string/blank?))]
        (if (= (first ls) "---")
          (let [[front sep-and-body] (split-with #(not= "---" %) (next ls))]
            {:front (vec front) :body (vec (next sep-and-body))})
          {:body (vec ls)})))
    

    请注意对vec 的调用,这会导致读取所有行并以向量或向量对的形式返回(这样我们就可以将process-linesprocess-file-with 一起使用,而不会过早关闭阅读器)。

    因为从磁盘上的实际文件读取行现在与处理一行行分离,我们可以轻松地在 REPL 测试该过程的后半部分(当然这可以变成一个单元测试):

    ;; could input this as a single string and split, of course
    (def test-lines
      ["---"
       "key1: value1"
       "key2: value2"
       "---"
       ""
       "Body text paragraph 1"
       ""
       "Body text paragraph 2"
       ""
       "Body text paragraph 3"])
    

    现在调用我们的函数:

    user> (process-lines test-lines)
    {:front ("key1: value1" "key2: value2"),
     :body ("Body text paragraph 1"
            "Body text paragraph 2"
            "Body text paragraph 3")}
    

    【讨论】:

    • 这太棒了。我知道我只是错误地看待问题。谢谢!
    【解决方案2】:

    实际上,使用 clojure 的惯用方法是避免返回“带有两个向量的哈希图”,并将文件视为(惰性)行序列

    然后,将处理行序列的函数决定文件是否具有 YAML 前端内容

    类似这样的:

    (use '[clojure.java.io :only (reader)])
    (let [s (line-seq (reader "YOURFILENAMEHERE"))]
      (if (= "---\n" (take 1 (line-seq (reader "YOURFILENAMEHERE"))))
        (process-seq-with-frontmatter s)
        (process-seq-without-frontmatter s))
    

    顺便说一句,这是一个退出且肮脏的解决方案;有两点需要改进:

    1. 请注意,我正在为同一个文件创建两个 seq,最好只创建一个并检查第一行,这样它就不会遍历 seq 的第一个元素(就像偷看一样流行音乐)
    2. 我认为拥有一个基于 seq 第一行内容调度的多方法“process-seq”(当然名称更好)会更简洁

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-02-28
      • 2017-07-28
      • 2011-07-28
      • 2019-09-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多