如何在 Clojure 中将文件中的数据读入 hash-map(或其他数据结构)?

How to read data from a file into a hash-map (or other data structure) in Clojure?

不太确定从哪里开始。 我有一个大数据文件,其中包含不同的值,这些值都与某个事物相关(即第 1 列中的数据将是小时),该文件有 15 列宽。该文件不包含任何列标题,它只是数字数据。

我需要将此数据读入一种数据类型,例如哈希映射,这样我就可以对其进行排序并使用诸如包含之类的东西查询数据?以及执行计算。

由于我是 Clojure 的新手,我不确定如何执行此操作,我们将不胜感激。

我的文件是一个 txt 文件(另存为 mydata.txt),结构如下:

  1 23 25 -9  -0 1 1
  2 23 25 10 1 2 3

到目前为止我的代码是:

(def filetoanalyse (slurp "mydata.txt"))
(zipmap [:num1 :num2 :num3 :num4 :num5 :num6 :num7] filetoanalyse)

目前似乎将整个文件与 :num1 相关联。

您 运行 遇到的问题是 slurp 将文件作为字符串读入。当您在其上使用 zipmap 时,它会使用字符串中的字符作为映射中的值,从而导致这一混乱:

(zipmap [:num1 :num2 :num3 :num4 :num5 :num6 :num7] (slurp "mydata.txt"))
;;=> {:num1 \space,
      :num2 \space,
      :num3 ,
      :num4 \space,
      :num5 ,
      :num6 ,
      :num7 \space}

最简单的方法是逐行遍历文件,将其拆分为所需的值。请注意此处的 vec,它强制执行(惰性)for 的结果,确保我们在 with-open 关闭 reader.

之前处理整个文件
(with-open [reader (clojure.java.io/reader "mydata.txt")]
  (vec (for [line (line-seq reader)]    ; iterate over each line
         (->> (clojure.string/split line #"\s+") ; split it by whitespace
              (remove empty?)           ; remove any empty entries
              (map #(Long/parseLong %)) ; convert into Longs (change if another format is more suitable)
              (zipmap [:num1 :num2 :num3 :num4 :num5 :num6 :num7]))))) ; turn into a map

您可以使用以下函数来执行您要查找的操作:

(defn map-from-file [field-re column-names filename]
  (let [ lines  (re-seq #"[^\r\n]+" (slurp filename)) ]
    (map #(zipmap column-names (re-seq field-re %)) lines)))

您必须提供三个参数:

  1. 用于分隔每行中的字段的正则表达式。对于您显示的数据,这可以是 #"[^ ]+",或者基本上任何不是空白的都是该字段的一部分。如果你有简单的逗号分隔值,没有复杂的情况,例如在数据或引用字段中嵌入逗号,#"[^,]+" 之类的东西就可以了。或者,如果您只想提取数字字符,则可以使用更复杂的东西,例如“#”[-0-9]+”。

  2. 要分配的列名集合。

  3. 文件名。

因此,如果您在问题中显示的数据存储为 test3.dat 某处,您可以将上述函数调用为

(map-from-file #"[^ ]+" [:c1 :c2 :c3 :c4 :c5 :c6 :c7] "/some-path/test3.dat")

它会 return

({:c1 "1", :c2 "23", :c3 "25", :c4 "-9", :c5 "-0", :c6 "1", :c7 "1"} {:c1 "2", :c2 "23", :c3 "25", :c4 "10", :c5 "1", :c6 "2", :c7 "3"})

或者换句话说,您会得到一系列映射,这些映射按您提供的列名映射值。如果您更喜欢将数据放在向量中,您可以使用

(into [] (map-from-file #"[^ ]+" [:c1 :c2 :c3 :c4 :c5 :c6 :c7] "/some-path/test3.dat"))

主要回答

Slurp 将 return 文件内容作为文本字符串,但您的代码似乎假定该文件已被解析为数字数组。事实并非如此。您仍然可以使用 slurp 但您必须自己解析文件。您可以通过首先使用 split-lines. Each line is valid Clojure syntax for a vector if we surround it by square brackets, and if we do so, we can then parse it into a vector using edn/read-string. We use map to parse each line of the file. The following code will do the job, and uses the ->> 宏按行分隔符拆分文件字符串来解析它,以保持代码的可读性:

(require '[clojure.string :as cljstr])
(require '[clojure.edn :as edn])

(->> "/tmp/mydata.txt"
     slurp
     cljstr/split-lines
     (map #(zipmap
            [:num1 :num2 :num3 :num4 :num5 :num6 :num7]
            (edn/read-string (str "[" % "]")))))
;; => ({:num1 1, :num2 23, :num3 25, :num4 -9, :num5 0, :num6 1, :num7 1} {:num1 2, :num2 23, :num3 25, :num4 10, :num5 1, :num6 2, :num7 3})

Extensions/variations

如果有包含其他元素数量的行,您可能希望仅保留包含七个元素的行,使用 filter。映射和过滤可以组成一个transducer that we pass as argument to into:

(let [columns [:num1 :num2 :num3 :num4 :num5 :num6 :num7]
      n (count columns)]
  (->> "/tmp/mydata.txt"
       slurp
       cljstr/split-lines
       (into [] (comp (map #(zipmap
                             columns
                             (edn/read-string (str "[" % "]"))))
                      (filter #(= n (count %)))))))
;; => [{:num1 1, :num2 23, :num3 25, :num4 -9, :num5 0, :num6 1, :num7 1} {:num1 2, :num2 23, :num3 25, :num4 10, :num5 1, :num6 2, :num7 3}]

如果您希望解析更复杂的文件或真的想 kill/overengineer 它,您可以使用 spec:

(require '[clojure.spec.alpha :as spec])

(->> "/tmp/mydata.txt"
     slurp
     cljstr/split-lines
     (map #(edn/read-string (str "[" % "]")))
     (spec/conform (spec/coll-of (spec/cat :num1 number?
                                           :num2 number?
                                           :num3 number?
                                           :num4 number?
                                           :num5 number?
                                           :num6 number?
                                           :num7 number?))))
;; => ({:num1 1, :num2 23, :num3 25, :num4 -9, :num5 0, :num6 1, :num7 1} {:num1 2, :num2 23, :num3 25, :num4 10, :num5 1, :num6 2, :num7 3})