Новичок, преобразовывающий файлы CSV в Clojure - PullRequest
9 голосов
/ 17 ноября 2009

Я и новичок, и старый в программировании - в основном я просто пишу много маленьких скриптов на Perl на работе. Clojure появился как раз тогда, когда я хотел изучить Lisp, поэтому я пытаюсь изучать Clojure, не зная и Java. Это тяжело, но пока весело.

Я видел несколько примеров подобных мне проблем, но ничего, что вполне соответствовало бы моему проблемному пространству. Существует ли канонический способ извлечения списков значений для каждой строки файла CSV в Clojure?

Вот немного действующего кода Perl; комментарии для неперлеров:

# convert_survey_to_cartography.pl
open INFILE, "< coords.csv";       # Input format "Northing,Easting,Elevation,PointID"
open OUTFILE, "> coords.txt";      # Output format "PointID X Y Z".
while (<INFILE>) {                 # Read line by line; line bound to $_ as a string.
    chomp $_;                      # Strips out each line's <CR><LF> chars.
    @fields = split /,/, $_;       # Extract the line's field values into a list.
    $y = $fields[0];               # y = Northing
    $x = $fields[1];               # x = Easting
    $z = $fields[2];               # z = Elevation
    $p = $fields[3];               # p = PointID
    print OUTFILE "$p $x $y $z\n"  # New file, changed field order, different delimiter.
}

Я немного озадачился в Clojure и попытался собрать его вместе в императивном стиле:

; convert-survey-to-cartography.clj
(use 'clojure.contrib.duck-streams)
(let
   [infile "coords.csv" outfile "coords.txt"]
   (with-open [rdr (reader infile)]
     (def coord (line-seq rdr))
     ( ...then a miracle occurs... )
     (write-lines outfile ":x :y :z :p")))

Я не ожидаю, что последняя строка на самом деле сработает, но это все понятно. Я ищу что-то вроде:

(def values (interleave (:p :y :x :z) (re-split #"," coord)))

Спасибо, Билл

Ответы [ 2 ]

15 голосов
/ 17 ноября 2009

Пожалуйста, не используйте вложенные определения. Это не так, как вы думаете. def всегда глобален! Для местных жителей используйте вместо этого пусть. Хотя функции библиотеки приятно знать, здесь есть версия, в которой описаны некоторые функции функционального программирования в целом и clojure в частности.

(import 'java.io.FileWriter 'java.io.FileReader 'java.io.BufferedReader)

(defn translate-coords

Строки документации могут быть запрошены в REPL через (doc translate -ordins). Работает например для всех основных функций. Так что снабжение - хорошая идея.

  "Reads coordinates from infile, translates them with the given
  translator and writes the result to outfile."

переводчик - это (возможно, анонимная) функция, которая извлекает перевод из окружающего шаблона. Таким образом, мы можем повторно использовать эти функции с различными правилами преобразования. Здесь подсказки типов избегают отражения для конструкторов.

  [translator #^String infile #^String outfile]

Откройте файлы. with-open позаботится о том, чтобы файлы были закрыты, когда его тело осталось. Будь то через обычное «падение с дна» или через исключение.

  (with-open [in  (BufferedReader. (FileReader. infile))
              out (FileWriter. outfile)]

Мы временно связываем поток *out* с выходным файлом. Так что любая печать внутри переплета будет печататься в файл.

    (binding [*out* out]

map означает: возьмите seq и примените данную функцию к каждому элементу и верните seq результатов. #() является сокращенной записью для анонимной функции. Требуется один аргумент, который заполняется в %. doseq - это в основном цикл ввода. Поскольку мы делаем это для побочных эффектов (а именно, для печати в файл), doseq - правильная конструкция. Основное правило: map: ленивый => для результата, doseq: нетерпеливый => для побочных эффектов.

      (doseq [coords (map #(.split % ",") (line-seq in))]

println заботится о \n в конце строки. interpose берет seq и добавляет первый аргумент (в нашем случае "") между его элементами. (apply str [1 2 3]) эквивалентно (str 1 2 3) и полезно для динамического создания вызовов функций. ->> - относительно новый макрос в clojure, который немного помогает с удобочитаемостью. Это означает «взять первый аргумент и добавить его как последний элемент к вызову функции». Указанное ->> эквивалентно: (println (apply str (interpose " " (translator coords)))). (Редактировать: еще одно примечание: поскольку разделитель \space, мы могли бы также написать (apply println (translator coords)), но версия interpose позволяет также параметризовать разделитель, как мы делали с функцией переводчика, в то время как у короткой версии hardwire \space.)

        (->> (translator coords)
          (interpose " ")
          (apply str)
          println)))))

(defn survey->cartography-format
  "Translate coords in survey format to cartography format."

Здесь мы используем деструктурирование (обратите внимание на двойное [[]]). Это означает, что аргумент функции - это то, что можно превратить в последовательность, например. вектор или список. Привязать первый элемент к y, второй к x и т. Д.

  [[y x z p]]
  [p x y z])

(translate-coords survey->cartography-format "survey_coords.txt" "cartography_coords.txt")

Здесь опять менее изменчиво:

(import 'java.io.FileWriter 'java.io.FileReader 'java.io.BufferedReader)

(defn translate-coords
  "Reads coordinates from infile, translates them with the given
  translator and writes the result to outfile."
  [translator #^String infile #^String outfile]
  (with-open [in  (BufferedReader. (FileReader. infile))
              out (FileWriter. outfile)]
    (binding [*out* out]
      (doseq [coords (map #(.split % ",") (line-seq in))]
        (->> (translator coords)
          (interpose " ")
          (apply str)
          println)))))

(defn survey->cartography-format
  "Translate coords in survey format to cartography format."
  [[y x z p]]
  [p x y z])

(translate-coords survey->cartography-format "survey_coords.txt" "cartography_coords.txt")

Надеюсь, это поможет.

Edit: для чтения CSV вы, вероятно, хотите что-то вроде OpenCSV.

8 голосов
/ 17 ноября 2009

Вот один из способов:

(use '(clojure.contrib duck-streams str-utils))                 ;;'
(with-out-writer "coords.txt"
  (doseq [line (read-lines "coords.csv")]
    (let [[x y z p] (re-split #"," line)]
      (println (str-join \space [p x y z])))))

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

Использование def в том виде, в каком вы его используете, не является идиоматическим. Лучше использовать let . Я использую деструктуризацию, чтобы назначить 4 поля каждой строки именам, связанным с 4 let; тогда вы можете делать с ними что хотите.

Если вы перебираете что-то с целью побочных эффектов (например, ввода / вывода), вам обычно следует выбрать doseq. Если вы хотите собрать каждую строку в хэш-карту и что-нибудь сделать с ними позже, вы можете использовать for:

(with-out-writer "coords.txt"
  (for [line (read-lines "coords.csv")]
    (let [fields (re-split #"," line)]
      (zipmap [:x :y :z :p] fields))))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...