No Clojure 1.3, como ler e gravar um arquivo

163

Eu gostaria de saber a maneira "recomendada" de ler e gravar um arquivo no clojure 1.3.

  1. Como ler o arquivo inteiro
  2. Como ler um arquivo linha por linha
  3. Como escrever um novo arquivo
  4. Como adicionar uma linha a um arquivo existente
jolly-san
fonte
2
Primeiro resultado do google: lethain.com/reading-file-in-clojure
jcubic 13/10/11
8
Este resultado é de 2009, algumas coisas foram alteradas recentemente.
Sergey
11
De fato. Agora, esta questão do StackOverflow é o primeiro resultado no Google.
Mydoghasworms

Respostas:

273

Supondo que estamos apenas fazendo arquivos de texto aqui e não algumas coisas binárias loucas.

Número 1: como ler um arquivo inteiro na memória.

(slurp "/tmp/test.txt")

Não recomendado quando se trata de um arquivo muito grande.

Número 2: como ler um arquivo linha por linha.

(use 'clojure.java.io)
(with-open [rdr (reader "/tmp/test.txt")]
  (doseq [line (line-seq rdr)]
    (println line)))

A with-openmacro cuida para que o leitor esteja fechado no final do corpo. A função de leitor força uma string (também pode fazer uma URL, etc) em uma BufferedReader. line-seqoferece um seq preguiçoso. Exigir o próximo elemento do preguiçoso seq resulta em uma linha que está sendo lida pelo leitor.

Observe que, a partir do Clojure 1.7, você também pode usar transdutores para ler arquivos de texto.

Número 3: como gravar em um novo arquivo.

(use 'clojure.java.io)
(with-open [wrtr (writer "/tmp/test.txt")]
  (.write wrtr "Line to be written"))

Mais uma vez, with-opencuide para que o mesmo BufferedWriteresteja fechado no final do corpo. O Writer força uma string em a BufferedWriter, que você usa por meio da interoperabilidade java:(.write wrtr "something").

Você também pode usar spito oposto de slurp:

(spit "/tmp/test.txt" "Line to be written")

Número 4: acrescente uma linha a um arquivo existente.

(use 'clojure.java.io)
(with-open [wrtr (writer "/tmp/test.txt" :append true)]
  (.write wrtr "Line to be appended"))

O mesmo que acima, mas agora com a opção de acréscimo.

Ou ainda com spito oposto de slurp:

(spit "/tmp/test.txt" "Line to be written" :append true)

PS: Para ser mais explícito sobre o fato de você estar lendo e gravando em um arquivo e não outra coisa, você pode primeiro criar um objeto File e depois forçá-lo a um BufferedReaderou Writer:

(reader (file "/tmp/test.txt"))
;; or
(writer (file "tmp/test.txt"))

A função de arquivo também está em clojure.java.io.

PS2: Às vezes, é útil poder ver qual é o diretório atual (então "."). Você pode obter o caminho absoluto de duas maneiras:

(System/getProperty "user.dir") 

ou

(-> (java.io.File. ".") .getAbsolutePath)
Michiel Borkent
fonte
1
Muito obrigado pela sua resposta detalhada. É um prazer conhecer a maneira recomendada de File IO (arquivo de texto) na versão 1.3. Parece ter havido algumas bibliotecas sobre o File IO (clojure.contrb.io, clojure.contrib.duck-streams e alguns exemplos diretamente usando o Java BufferedReader FileInputStream InputStreamReader), o que me deixou mais confusa. Além disso, há pouca informação sobre o Clojure 1.3, especialmente em japonês (minha língua natural). Obrigado.
alegre-san
Oi jolly-san, tnx por aceitar minha resposta! Para sua informação, o clojure.contrib.duck-streams agora está obsoleto. Isso possivelmente aumenta a confusão.
Michiel Borkent 14/10
Ou seja, (with-open [rdr (reader "/tmp/test.txt")] (line-seq rdr))produz em IOException Stream closedvez de uma coleção de linhas. O que fazer? Estou obtendo bons resultados com a resposta de @satyagraha, no entanto.
0dB 27/10
4
Isso tem a ver com preguiça. Quando você usa o resultado de line-seq fora do with-open, o que acontece quando você imprime o resultado no REPL, o leitor já está fechado. Uma solução é agrupar a linha-seq dentro de um doall, o que força a avaliação imediatamente. (with-open [rdr (reader "/tmp/test.txt")] (doall (line-seq rdr)))
Michiel Borkent
Para os iniciantes reais como eu, observe que doseqretornos nilque podem levar a tempos tristes sem valor de retorno.
outubro
33

Se o arquivo couber na memória, você pode ler e escrever com slurp e spit:

(def s (slurp "filename.txt"))

(s agora contém o conteúdo de um arquivo como uma string)

(spit "newfile.txt" s)

Isso cria o newfile.txt se ele não sair e grava o conteúdo do arquivo. Se você deseja anexar ao arquivo, pode fazer

(spit "filename.txt" s :append true)

Para ler ou gravar um arquivo em linha, você usaria o leitor e gravador de Java. Eles estão agrupados no namespace clojure.java.io:

(ns file.test
  (:require [clojure.java.io :as io]))

(let [wrtr (io/writer "test.txt")]
  (.write wrtr "hello, world!\n")
  (.close wrtr))

(let [wrtr (io/writer "test.txt" :append true)]
  (.write wrtr "hello again!")
  (.close wrtr))

(let [rdr (io/reader "test.txt")]
  (println (.readLine rdr))
  (println (.readLine rdr)))
; "hello, world!"
; "hello again!"

Observe que a diferença entre os exemplos slurp / spit e leitor / gravador é que o arquivo permanece aberto (nas instruções let) no último e a leitura e gravação são armazenadas em buffer, tornando-se mais eficiente ao ler / gravar repetidamente em um arquivo.

Aqui estão mais informações: slurp spit clojure.java.io BufferedReader do Java Writer de Java

Paulo
fonte
1
Obrigado Paul. Eu poderia aprender mais com seus códigos e seus comentários, que são claros no ponto em focar em responder minha pergunta. Muito obrigado.
91311 jolly-san
Obrigado por adicionar informações sobre métodos de nível ligeiramente inferior, não fornecidas na (excelente) resposta de Michiel Borkent sobre os melhores métodos para casos típicos.
Marte
@Mars Obrigado. Na verdade, eu respondi a essa pergunta primeiro, mas a resposta de Michiel tem mais estrutura e isso parece ser muito popular.
Paul
Ele faz um bom trabalho com os casos usuais, mas você forneceu outras informações. É por isso que é bom que o SE permita várias respostas.
Marte
6

Em relação à pergunta 2, às vezes se deseja que o fluxo de linhas retorne como um objeto de primeira classe. Para obter isso como uma sequência lenta, e ainda ter o arquivo fechado automaticamente no EOF, usei esta função:

(use 'clojure.java.io)

(defn read-lines [filename]
  (let [rdr (reader filename)]
    (defn read-next-line []
      (if-let [line (.readLine rdr)]
       (cons line (lazy-seq (read-next-line)))
       (.close rdr)))
    (lazy-seq (read-next-line)))
)

(defn echo-file []
  (doseq [line (read-lines "myfile.txt")]
    (println line)))
satyagraha
fonte
7
Eu não acho que aninhar defné Clojure ideomatic. Seu read-next-line, pelo que entendi, é visível fora de sua read-linesfunção. Você pode ter usado um em (let [read-next-line (fn [] ...))vez disso.
precisa
Eu acho que sua resposta seria um pouco melhor se ela retornasse a função criada (fechando sobre o leitor aberto) em vez de vincular globalmente.
Ward
1

É assim que se lê o arquivo inteiro.

Se o arquivo estiver no diretório de recursos, você poderá fazer isso:

(let [file-content-str (slurp (clojure.java.io/resource "public/myfile.txt")])

lembre-se de exigir / usar clojure.java.io.

Joshua
fonte
0
(require '[clojure.java.io :as io])
(io/copy (io/file "/etc/passwd") \*out*\)
Dima Fomin
fonte
0

Para ler um arquivo linha por linha, você não precisa mais recorrer à interoperabilidade:

(->> "data.csv"
      io/resource
      io/reader
      line-seq
      (drop 1))

Isso pressupõe que seu arquivo de dados seja mantido no diretório de recursos e que a primeira linha seja informações de cabeçalho que podem ser descartadas.

Chris Murphy
fonte