Подтвердить что ты не робот

Прочитайте очень большой текстовый файл в списке в clojure

Каков наилучший способ чтения очень большого файла (например, текстового файла, имеющего 100 000 имен по одному в каждой строке) в список (лениво - загрузка по мере необходимости) в clojure?

В основном мне нужно делать всевозможные строковые запросы по этим элементам (теперь я делаю это с помощью grep и reg ex в сценариях оболочки).

Я попробовал добавить '(в начале и) в конце, но, по-видимому, этот метод (загрузка статического?/константного списка по какой-то причине имеет ограничение по размеру.

4b9b3361

Ответ 1

Вам нужно использовать 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, вы можете использовать этот список лениво. В противном случае будет лучше использовать встроенную базу данных.

Также обратите внимание, что вы не должны удерживать головку списка, иначе весь список будет загружен в память.

Кроме того, если вам нужно выполнить более одной операции, вам нужно будет прочитать файл снова и снова. Будьте осторожны, ленивость иногда может осложнить ситуацию.

Ответ 2

Существуют различные способы сделать это, в зависимости от того, что вы хотите.

Если у вас есть function, который вы хотите применить к каждой строке в файле, вы можете использовать код, похожий на ответ Abhinav:

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

Это имеет то преимущество, что файл открывается, обрабатывается и закрывается как можно быстрее, но заставляет весь файл потреблять сразу.

Если вы хотите отложить обработку файла, у вас может возникнуть соблазн вернуть строки, но это не сработает:

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

потому что файл закрывается при возврате with-open, который перед вами лениво обрабатывает файл.

Один из способов - вытащить весь файл в память с помощью 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 где-то в библиотеках? Я пришел к этому вопросу, пытаясь найти такую ​​функцию, и в итоге написал свой собственный, как описано выше.

Ответ 3

Решение Andrew работало хорошо для меня, но вложенный defn не настолько идиоматичен, и вам не нужно делать lazy-seq дважды: вот обновленная версия без дополнительных отпечатков и использование 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>

Ответ 4

см. мой ответ здесь

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