Читать очень большой текстовый файл в список в ближайшем будущем - PullRequest
26 голосов
/ 07 ноября 2010

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

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

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

Ответы [ 4 ]

29 голосов
/ 05 мая 2012

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

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

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

21 голосов
/ 09 ноября 2012

Решение Эндрю хорошо для меня, но вложенные 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>
20 голосов
/ 07 ноября 2010

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

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

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

1 голос
/ 20 ноября 2014

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

(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))))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...