Преобразование файлов CSV в Clojure для новичков

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

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

Вот некоторый фактически работающий код Perl; комментарии включены для не-Perlers:

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

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


person Bill_B    schedule 17.11.2009    source источник
comment
Хороший вопрос - TIMTOWTDI. Спасибо.   -  person Bill_B    schedule 17.11.2009


Ответы (2)


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

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

(defn translate-coords

Строки документации можно запрашивать в REPL через (doc 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 позаботится о том, чтобы файлы были закрыты, когда его тело осталось. Будь то через обычное «выпадение со дна» или через выброшенное исключение.

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

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

    (binding [*out* out]

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

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

println заботится о \n в конце строки. interpose берет последовательность и добавляет первый аргумент (в нашем случае " ") между своими элементами. (apply str [1 2 3]) эквивалентно (str 1 2 3) и полезно для динамического построения вызовов функций. ->> — относительно новый макрос в clojure, который немного улучшает читабельность. Это означает «взять первый аргумент и добавить его в качестве последнего элемента в вызов функции». Данное ->> эквивалентно: (println (apply str (interpose " " (translator coords)))). (Правка: еще одно примечание: поскольку разделителем является \space, мы могли бы здесь написать так же хорошо (apply println (translator coords)), но версия interpose позволяет также параметризовать разделитель, как мы сделали с функцией транслятора, в то время как короткая версия будет жестко привязана к \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")

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

Изменить: для чтения CSV вам, вероятно, понадобится что-то вроде OpenCSV.

person kotarak    schedule 17.11.2009
comment
Спасибо за урок - там много полезной информации, и мне потребуется некоторое время, чтобы ее переварить. Я смоделировал собственную функцию по той, которую вы использовали здесь, и она работала как шарм. Еще раз спасибо! - person Bill_B; 18.11.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))))
person Brian Carper    schedule 17.11.2009
comment
Именно то, что мне было нужно! И сделано тоже элегантно! До сих пор я не имел особого смысла для меня «дозак», и теперь я вижу, что неправильно понял и некоторые другие вещи. Я попробовал ваш код в ClojureBox, и он сработал; Я также смог обернуть это в функцию, и это тоже сработало, так что, похоже, это поставило меня на правильный путь. Еще раз спасибо. - person Bill_B; 18.11.2009