3

我正在尝试使用 Clojure 逐行读取一个(可能有也可能没有)具有YAML frontmatter的文件,并返回一个带有两个向量的 hashmap,一个包含 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)))))

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

4

2 回答 2

6

首先,我将行处理逻辑放在它自己的函数中,以便从实际读取文件的函数中调用。更好的是,您可以使处理 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在返回之前尽可能多地实现 line seq(因为之后with-open将关闭 line seq 的底层读取器)。

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

(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)})))

请注意调用vecwhich 会导致所有行被读入并以向量或向量对的形式返回(这样我们就可以使用process-lineswithprocess-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")}
于 2013-08-19T23:22:05.860 回答
0

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

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

像这样的东西:

(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 的第一个元素(就像 peek 而不是 pop )
  2. 我认为拥有一个基于 seq 第一行的内容进行调度的多方法“process-seq”(当然名称更好)会更干净
于 2013-08-20T00:58:30.763 回答