web-dev-qa-db-ja.com

非常に大きなテキストファイルをclojureのリストに読み込みます

非常に大きなファイル(各行に100000の名前を持つテキストファイルなど)をclojureのリストに読み込む(遅延-必要に応じてロードする)ための最良の方法は何ですか?

基本的に、これらのアイテムに対してあらゆる種類の文字列検索を実行する必要があります(現在、シェルスクリプトのgrepおよびreg exを使用して検索しています)。

'(最初と)を最後に追加しようとしましたが、どうやらこのメソッド(静的?/定数リストをロードする)には、何らかの理由でサイズ制限があります。

27
Ali

使用する必要があります line-seq 。 clojuredocsからの例:

;; Count lines of a file (loses head):
user=> (with-open [rdr (clojure.Java.io/reader "/etc/passwd")]
         (count (line-seq rdr)))

しかし、文字列の遅延リストでは、並べ替えなど、リスト全体が存在する必要がある操作を効率的に実行できません。操作をfilterまたはmapとして実装できる場合は、リストを遅延して使用できます。それ以外の場合は、組み込みデータベースを使用することをお勧めします。

また、リストの先頭を保持しないでください。保持しないと、リスト全体がメモリにロードされます。

さらに、複数の操作を行う必要がある場合は、ファイルを何度も読み取る必要があります。注意してください、怠惰は時々物事を困難にする可能性があります。

20
Abhinav Sarkar

これを行うには、必要なものに応じてさまざまな方法があります。

ファイルの各行に適用するfunctionがある場合は、Abhinavの回答と同様のコードを使用できます。

(with-open [rdr ...]
  (doall (map function (line-seq rdr))))

これには、ファイルができるだけ早く開かれ、処理され、閉じられるという利点がありますが、ファイル全体が一度に消費されます。

ファイルの処理を遅らせたい場合は、行を返したくなるかもしれませんが、これは機能しません

(map function ; broken!!!
    (with-open [rdr ...]
        (line-seq rdr)))

with-openが戻るとファイルが閉じられるため、つまりbeforeファイルを遅延処理します。

これを回避する1つの方法は、Slurpを使用してファイル全体をメモリにプルすることです。

(map function (Slurp filename))

これには明らかな欠点(メモリの使用)がありますが、ファイルを開いたままにしないことが保証されます。

別の方法は、レイジーシーケンスを生成しながら、読み取りの最後に到達するまでファイルを開いたままにすることです。

(ns ...
  (:use clojure.test))

(defn stream-consumer [stream]
  (println "read" (count stream) "lines"))

(defn broken-open [file]
  (with-open [rdr (clojure.Java.io/reader file)]
    (line-seq rdr)))

(defn lazy-open [file]
  (defn helper [rdr]
    (lazy-seq
      (if-let [line (.readLine rdr)]
        (cons line (helper rdr))
        (do (.close rdr) (println "closed") nil))))
  (lazy-seq
    (do (println "opening")
      (helper (clojure.Java.io/reader file)))))

(deftest test-open
  (try
    (stream-consumer (broken-open "/etc/passwd"))
    (catch RuntimeException e
      (println "caught " e)))
  (let [stream (lazy-open "/etc/passwd")]
    (println "have stream")
    (stream-consumer stream)))

(run-tests)

どの印刷物:

caught  #<RuntimeException Java.lang.RuntimeException: Java.io.IOException: Stream closed>
have stream
opening
closed
read 29 lines

必要になるまでファイルが開かれなかったことを示します。

この最後のアプローチには、すべてをメモリに保持せずにデータのストリームを「他の場所」で処理できるという利点がありますが、ストリームの最後が読み取られるまでファイルが閉じられないという重要な欠点もあります。注意しないと、多くのファイルを並行して開くか、(ストリームを完全に読み取らないことによって)それらを閉じるのを忘れる可能性があります。

最良の選択は状況によって異なります-それは遅延評価と限られたシステムリソースの間のトレードオフです。

PS:lazy-openはライブラリのどこかに定義されていますか?私はそのような関数を見つけようとしてこの質問にたどり着き、上記のように自分で書くことになりました。

29
andrew cooke

Andrewのソリューションは私にとってはうまく機能しましたが、ネストされたdefnsはそれほど慣用的ではなく、lazy-seqを2回実行する必要はありません。これは、余分な印刷がなく、letfn

(defn lazy-file-lines [file]
  (letfn [(helper [rdr]
                  (lazy-seq
                    (if-let [line (.readLine rdr)]
                      (cons line (helper rdr))
                      (do (.close rdr) nil))))]
         (helper (clojure.Java.io/reader file))))

(count (lazy-file-lines "/tmp/massive-file.txt"))
;=> <a large integer>
21
JohnJ

ここで私の答えを参照してください

(ns user
  (:require [clojure.core.async :as async :refer :all 
:exclude [map into reduce merge partition partition-by take]]))

(defn read-dir [dir]
  (let [directory (clojure.Java.io/file dir)
        files (filter #(.isFile %) (file-seq directory))
        ch (chan)]
    (go
      (doseq [file files]
        (with-open [rdr (clojure.Java.io/reader file)]
          (doseq [line (line-seq rdr)]
            (>! ch line))))
      (close! ch))
    ch))

そう:

(def aa "D:\\Users\\input")
(let [ch (read-dir aa)]
  (loop []
    (when-let [line (<!! ch )]
      (println line)
      (recur))))
1
chen_767